From 451dd3c1296dd571e8eb87d378369d9d2a1450bc Mon Sep 17 00:00:00 2001 From: static Date: Wed, 28 May 2025 18:00:17 +0900 Subject: [PATCH] =?UTF-8?q?=EB=8D=B0=EB=AA=A8=EC=9A=A9=20=EC=9E=84?= =?UTF-8?q?=EC=8B=9C=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/server/db/user.ts | 9 ++++ src/lib/server/middlewares/authenticate.ts | 2 +- src/lib/server/schemas/auth.ts | 7 ++++ src/lib/server/services/auth.ts | 23 +++++++++- .../(fullscreen)/auth/login/+page.svelte | 42 ++++++++++++++++++- .../auth/login/NicknameModal.svelte | 18 ++++++++ src/routes/(fullscreen)/auth/login/service.ts | 11 ++++- src/routes/api/auth/register/+server.ts | 27 ++++++++++++ 8 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 src/routes/(fullscreen)/auth/login/NicknameModal.svelte create mode 100644 src/routes/api/auth/register/+server.ts diff --git a/src/lib/server/db/user.ts b/src/lib/server/db/user.ts index 3964a94..1804144 100644 --- a/src/lib/server/db/user.ts +++ b/src/lib/server/db/user.ts @@ -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") diff --git a/src/lib/server/middlewares/authenticate.ts b/src/lib/server/middlewares/authenticate.ts index 8880f1a..37ce672 100644 --- a/src/lib/server/middlewares/authenticate.ts +++ b/src/lib/server/middlewares/authenticate.ts @@ -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); } diff --git a/src/lib/server/schemas/auth.ts b/src/lib/server/schemas/auth.ts index e3d6264..2a9fcec 100644 --- a/src/lib/server/schemas/auth.ts +++ b/src/lib/server/schemas/auth.ts @@ -12,6 +12,13 @@ export const loginRequest = z.object({ }); export type LoginRequest = z.infer; +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; + export const sessionUpgradeRequest = z.object({ encPubKey: z.string().base64().nonempty(), sigPubKey: z.string().base64().nonempty(), diff --git a/src/lib/server/services/auth.ts b/src/lib/server/services/auth.ts index 81f0333..e9fd91e 100644 --- a/src/lib/server/services/auth.ts +++ b/src/lib/server/services/auth.ts @@ -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, diff --git a/src/routes/(fullscreen)/auth/login/+page.svelte b/src/routes/(fullscreen)/auth/login/+page.svelte index ac3b1a5..b68c83a 100644 --- a/src/routes/(fullscreen)/auth/login/+page.svelte +++ b/src/routes/(fullscreen)/auth/login/+page.svelte @@ -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; + } + }; @@ -60,6 +96,8 @@ - 계정이 없어요 + (isNicknameModalOpen = true)}>계정이 없어요 + + diff --git a/src/routes/(fullscreen)/auth/login/NicknameModal.svelte b/src/routes/(fullscreen)/auth/login/NicknameModal.svelte new file mode 100644 index 0000000..de71042 --- /dev/null +++ b/src/routes/(fullscreen)/auth/login/NicknameModal.svelte @@ -0,0 +1,18 @@ + + + diff --git a/src/routes/(fullscreen)/auth/login/service.ts b/src/routes/(fullscreen)/auth/login/service.ts index 2d267e1..56921fc 100644 --- a/src/routes/(fullscreen)/auth/login/service.ts +++ b/src/routes/(fullscreen)/auth/login/service.ts @@ -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("/api/auth/register", { + email, + nickname, + password, + }); + return res.ok; +}; + export const requestSessionUpgrade = async ({ encryptKey, decryptKey, diff --git a/src/routes/api/auth/register/+server.ts b/src/routes/api/auth/register/+server.ts new file mode 100644 index 0000000..a1fe339 --- /dev/null +++ b/src/routes/api/auth/register/+server.ts @@ -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" } }); +};