데모용 임시 회원가입 구현

This commit is contained in:
static
2025-05-28 18:00:17 +09:00
parent 36006a9b72
commit 451dd3c129
8 changed files with 134 additions and 5 deletions

View File

@@ -7,6 +7,15 @@ interface User {
password: string;
}
export const createUser = async (email: string, nickname: string, password: string) => {
const { id } = await db
.insertInto("user")
.values({ email, nickname, password })
.returning("id")
.executeTakeFirstOrThrow();
return { id, email, nickname, password } satisfies User;
};
export const getUser = async (userId: number) => {
const user = await db
.selectFrom("user")

View File

@@ -3,7 +3,7 @@ import { authenticate, AuthenticationError } from "$lib/server/modules/auth";
export const authenticateMiddleware: Handle = async ({ event, resolve }) => {
const { pathname, search } = event.url;
if (pathname === "/api/auth/login") {
if (pathname === "/api/auth/login" || pathname === "/api/auth/register") {
return await resolve(event);
}

View File

@@ -12,6 +12,13 @@ export const loginRequest = z.object({
});
export type LoginRequest = z.infer<typeof loginRequest>;
export const registerRequest = z.object({
email: z.string().email(),
nickname: z.string().trim().min(2).max(8),
password: z.string().trim().nonempty(),
});
export type RegisterRequest = z.infer<typeof registerRequest>;
export const sessionUpgradeRequest = z.object({
encPubKey: z.string().base64().nonempty(),
sigPubKey: z.string().base64().nonempty(),

View File

@@ -9,7 +9,7 @@ import {
registerSessionUpgradeChallenge,
consumeSessionUpgradeChallenge,
} from "$lib/server/db/session";
import { getUser, getUserByEmail, setUserPassword } from "$lib/server/db/user";
import { createUser, getUser, getUserByEmail, setUserPassword } from "$lib/server/db/user";
import env from "$lib/server/loadenv";
import { startSession } from "$lib/server/modules/auth";
import { verifySignature, generateChallenge } from "$lib/server/modules/crypto";
@@ -65,6 +65,27 @@ export const logout = async (sessionId: string) => {
await deleteSession(sessionId);
};
export const register = async (
email: string,
nickname: string,
password: string,
ip: string,
userAgent: string,
) => {
if (password.length < 8) {
error(400, "Too short password");
}
const existingUser = await getUserByEmail(email);
if (existingUser) {
error(409, "Email already registered");
}
const hashedPassword = await hashPassword(password);
const { id } = await createUser(email, nickname, hashedPassword);
return { sessionIdSigned: await startSession(id, ip, userAgent) };
};
export const createSessionUpgradeChallenge = async (
sessionId: string,
userId: number,

View File

@@ -3,13 +3,21 @@
import { BottomDiv, Button, FullscreenDiv, TextButton, TextInput } from "$lib/components/atoms";
import { TitledDiv } from "$lib/components/molecules";
import { clientKeyStore, masterKeyStore } from "$lib/stores";
import { requestLogin, requestSessionUpgrade, requestMasterKeyDownload } from "./service";
import NicknameModal from "./NicknameModal.svelte";
import {
requestLogin,
requestRegister,
requestSessionUpgrade,
requestMasterKeyDownload,
} from "./service";
let { data } = $props();
let email = $state("");
let password = $state("");
let isNicknameModalOpen = $state(false);
const redirect = async (url: string) => {
return await goto(`${url}?redirect=${encodeURIComponent(data.redirectPath)}`);
};
@@ -40,6 +48,34 @@
throw e;
}
};
const register = async (nickname: string) => {
// TODO: Validation
try {
if (!(await requestRegister(email, nickname, password)))
throw new Error("Failed to register");
if (!$clientKeyStore) return await redirect("/key/generate");
if (!(await requestSessionUpgrade($clientKeyStore)))
throw new Error("Failed to upgrade session");
// TODO: Multi-user support
if (
$masterKeyStore ||
(await requestMasterKeyDownload($clientKeyStore.decryptKey, $clientKeyStore.verifyKey))
) {
await goto(data.redirectPath);
} else {
await redirect("/client/pending");
}
} catch (e) {
// TODO: Alert
throw e;
}
};
</script>
<svelte:head>
@@ -60,6 +96,8 @@
</TitledDiv>
<BottomDiv class="flex flex-col items-center gap-y-2">
<Button onclick={login} class="w-full">로그인</Button>
<TextButton>계정이 없어요</TextButton>
<TextButton onclick={() => (isNicknameModalOpen = true)}>계정이 없어요</TextButton>
</BottomDiv>
</FullscreenDiv>
<NicknameModal bind:isOpen={isNicknameModalOpen} onSubmitClick={register} />

View File

@@ -0,0 +1,18 @@
<script lang="ts">
import { TextInputModal } from "$lib/components/organisms";
interface Props {
isOpen: boolean;
onSubmitClick: (nickname: string) => void;
}
let { isOpen = $bindable(), onSubmitClick }: Props = $props();
</script>
<TextInputModal
bind:isOpen
title="닉네임 설정하기"
placeholder="닉네임"
submitText="새 계정 만들기"
{onSubmitClick}
/>

View File

@@ -1,6 +1,6 @@
import { callPostApi } from "$lib/hooks";
import { exportRSAKeyToBase64 } from "$lib/modules/crypto";
import type { LoginRequest } from "$lib/server/schemas";
import type { LoginRequest, RegisterRequest } from "$lib/server/schemas";
import { requestSessionUpgrade as requestSessionUpgradeInternal } from "$lib/services/auth";
import { requestClientRegistration } from "$lib/services/key";
import type { ClientKeys } from "$lib/stores";
@@ -12,6 +12,15 @@ export const requestLogin = async (email: string, password: string) => {
return res.ok;
};
export const requestRegister = async (email: string, nickname: string, password: string) => {
const res = await callPostApi<RegisterRequest>("/api/auth/register", {
email,
nickname,
password,
});
return res.ok;
};
export const requestSessionUpgrade = async ({
encryptKey,
decryptKey,

View File

@@ -0,0 +1,27 @@
import { error, text } from "@sveltejs/kit";
import env from "$lib/server/loadenv";
import { registerRequest } from "$lib/server/schemas";
import { register } from "$lib/server/services/auth";
import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ locals, request, cookies }) => {
const zodRes = registerRequest.safeParse(await request.json());
if (!zodRes.success) error(400, "Invalid request body");
const { email, nickname, password } = zodRes.data;
const { sessionIdSigned } = await register(
email,
nickname,
password,
locals.ip,
locals.userAgent,
);
cookies.set("sessionId", sessionIdSigned, {
path: "/",
maxAge: env.session.exp / 1000,
secure: true,
sameSite: "strict",
});
return text("Registered and logged in", { headers: { "Content-Type": "text/plain" } });
};