From fac876457226ef8463a4c7bf9b2ff16b864cfaeb Mon Sep 17 00:00:00 2001 From: static Date: Thu, 26 Dec 2024 16:50:13 +0900 Subject: [PATCH] =?UTF-8?q?/api/auth/login=20Endpoint=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 8 +- package.json | 6 +- pnpm-lock.yaml | 117 ++++++++++++++++++++++++++- src/lib/server/auth.ts | 27 +++++++ src/lib/server/db/drizzle.ts | 7 ++ src/lib/server/db/index.ts | 6 -- src/lib/server/db/user.ts | 8 ++ src/lib/server/loadenv.ts | 12 +++ src/routes/api/auth/login/+server.ts | 22 +++++ tsconfig.json | 1 + 10 files changed, 205 insertions(+), 9 deletions(-) create mode 100644 src/lib/server/auth.ts create mode 100644 src/lib/server/db/drizzle.ts delete mode 100644 src/lib/server/db/index.ts create mode 100644 src/lib/server/db/user.ts create mode 100644 src/lib/server/loadenv.ts create mode 100644 src/routes/api/auth/login/+server.ts diff --git a/.env.example b/.env.example index d59bf33..e76f3cd 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,7 @@ -DATABASE_URL=local.db +# Required environment variables +JWT_SECRET= + +# Optional environment variables +DATABASE_URL= +JWT_ACCESS_TOKEN_EXPIRES= +JWT_REFRESH_TOKEN_EXPIRES= diff --git a/package.json b/package.json index 66d9525..010856a 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@sveltejs/kit": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^4.0.0", "@types/better-sqlite3": "^7.6.11", + "@types/jsonwebtoken": "^9.0.7", "autoprefixer": "^10.4.20", "drizzle-kit": "^0.22.0", "eslint": "^9.7.0", @@ -39,7 +40,10 @@ "vite": "^5.4.11" }, "dependencies": { + "argon2": "^0.41.1", "better-sqlite3": "^11.1.2", - "drizzle-orm": "^0.33.0" + "drizzle-orm": "^0.33.0", + "jsonwebtoken": "^9.0.2", + "zod": "^3.24.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 59254a4..0a0e593 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,12 +5,21 @@ settings: excludeLinksFromLockfile: false dependencies: + argon2: + specifier: ^0.41.1 + version: 0.41.1 better-sqlite3: specifier: ^11.1.2 version: 11.7.0 drizzle-orm: specifier: ^0.33.0 version: 0.33.0(@types/better-sqlite3@7.6.12)(better-sqlite3@11.7.0) + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.2 + zod: + specifier: ^3.24.1 + version: 3.24.1 devDependencies: '@eslint/compat': @@ -28,6 +37,9 @@ devDependencies: '@types/better-sqlite3': specifier: ^7.6.11 version: 7.6.12 + '@types/jsonwebtoken': + specifier: ^9.0.7 + version: 9.0.7 autoprefixer: specifier: ^10.4.20 version: 10.4.20(postcss@8.4.49) @@ -887,6 +899,11 @@ packages: fastq: 1.18.0 dev: true + /@phc/format@1.0.0: + resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==} + engines: {node: '>=10'} + dev: false + /@pkgjs/parseargs@0.11.0: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -1205,6 +1222,12 @@ packages: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: true + /@types/jsonwebtoken@9.0.7: + resolution: {integrity: sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==} + dependencies: + '@types/node': 22.10.2 + dev: true + /@types/node@22.10.2: resolution: {integrity: sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==} dependencies: @@ -1399,6 +1422,16 @@ packages: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} dev: true + /argon2@0.41.1: + resolution: {integrity: sha512-dqCW8kJXke8Ik+McUcMDltrbuAWETPyU6iq+4AhxqKphWi7pChB/Zgd/Tp/o8xRLbg8ksMj46F/vph9wnxpTzQ==} + engines: {node: '>=16.17.0'} + requiresBuild: true + dependencies: + '@phc/format': 1.0.0 + node-addon-api: 8.3.0 + node-gyp-build: 4.8.4 + dev: false + /argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} dev: true @@ -1495,6 +1528,10 @@ packages: update-browserslist-db: 1.1.1(browserslist@4.24.3) dev: true + /buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + dev: false + /buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} dev: true @@ -1761,6 +1798,12 @@ packages: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} dev: true + /ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + dependencies: + safe-buffer: 5.2.1 + dev: false + /electron-to-chromium@1.5.76: resolution: {integrity: sha512-CjVQyG7n7Sr+eBXE86HIulnL5N8xZY1sgmOPGuq/F0Rr0FJq63lg0kEtOIDfZBk44FnDLf6FUJ+dsJcuiUDdDQ==} dev: true @@ -2371,6 +2414,37 @@ packages: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} dev: true + /jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.6.3 + dev: false + + /jwa@1.4.1: + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + dev: false + + /jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + dependencies: + jwa: 1.4.1 + safe-buffer: 5.2.1 + dev: false + /keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} dependencies: @@ -2419,10 +2493,38 @@ packages: p-locate: 5.0.0 dev: true + /lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + dev: false + + /lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + dev: false + + /lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + dev: false + + /lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + dev: false + + /lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + dev: false + + /lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + dev: false + /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true + /lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + dev: false + /lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} dev: true @@ -2489,7 +2591,6 @@ packages: /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - dev: true /mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -2520,6 +2621,16 @@ packages: semver: 7.6.3 dev: false + /node-addon-api@8.3.0: + resolution: {integrity: sha512-8VOpLHFrOQlAH+qA0ZzuGRlALRA6/LVh8QJldbrC4DY0hXoMP0l4Acq8TzFC018HztWiRqyCEj2aTWY2UvnJUg==} + engines: {node: ^18 || ^20 || >= 21} + dev: false + + /node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + dev: false + /node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} dev: true @@ -3411,3 +3522,7 @@ packages: /zimmerframe@1.1.2: resolution: {integrity: sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==} dev: true + + /zod@3.24.1: + resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==} + dev: false diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts new file mode 100644 index 0000000..85efdff --- /dev/null +++ b/src/lib/server/auth.ts @@ -0,0 +1,27 @@ +import argon2 from "argon2"; +import jwt from "jsonwebtoken"; +import { getUserByEmail } from "$lib/server/db/user"; +import env from "$lib/server/loadenv"; + +const verifyPassword = async (hash: string, password: string) => { + return await argon2.verify(hash, password); +}; + +const issueToken = (id: number, type: "access" | "refresh") => { + return jwt.sign({ id, type }, env.jwt.secret, { + expiresIn: type === "access" ? env.jwt.accessExp : env.jwt.refreshExp, + }); +}; + +export const login = async (email: string, password: string) => { + const user = await getUserByEmail(email); + if (!user) return null; + + const valid = await verifyPassword(user.password, password); + if (!valid) return null; + + return { + accessToken: issueToken(user.id, "access"), + refreshToken: issueToken(user.id, "refresh"), + }; +}; diff --git a/src/lib/server/db/drizzle.ts b/src/lib/server/db/drizzle.ts new file mode 100644 index 0000000..385ac23 --- /dev/null +++ b/src/lib/server/db/drizzle.ts @@ -0,0 +1,7 @@ +import Database from "better-sqlite3"; +import { drizzle } from "drizzle-orm/better-sqlite3"; +import env from "$lib/server/loadenv"; + +const client = new Database(env.databaseUrl); + +export default drizzle(client); diff --git a/src/lib/server/db/index.ts b/src/lib/server/db/index.ts deleted file mode 100644 index 8093e19..0000000 --- a/src/lib/server/db/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { drizzle } from "drizzle-orm/better-sqlite3"; -import Database from "better-sqlite3"; -import { env } from "$env/dynamic/private"; -if (!env.DATABASE_URL) throw new Error("DATABASE_URL is not set"); -const client = new Database(env.DATABASE_URL); -export const db = drizzle(client); diff --git a/src/lib/server/db/user.ts b/src/lib/server/db/user.ts new file mode 100644 index 0000000..38a53f0 --- /dev/null +++ b/src/lib/server/db/user.ts @@ -0,0 +1,8 @@ +import { eq } from "drizzle-orm"; +import db from "./drizzle"; +import { user } from "./schema"; + +export const getUserByEmail = async (email: string) => { + const users = await db.select().from(user).where(eq(user.email, email)).execute(); + return users[0] ?? null; +}; diff --git a/src/lib/server/loadenv.ts b/src/lib/server/loadenv.ts new file mode 100644 index 0000000..83f4514 --- /dev/null +++ b/src/lib/server/loadenv.ts @@ -0,0 +1,12 @@ +import { env } from "$env/dynamic/private"; + +if (!env.JWT_SECRET) throw new Error("JWT_SECRET is not set"); + +export default { + databaseUrl: env.DATABASE_URL || "local.db", + jwt: { + secret: env.JWT_SECRET, + accessExp: env.JWT_ACCESS_TOKEN_EXPIRES || "5m", + refreshExp: env.JWT_REFRESH_TOKEN_EXPIRES || "14d", + }, +}; diff --git a/src/routes/api/auth/login/+server.ts b/src/routes/api/auth/login/+server.ts new file mode 100644 index 0000000..09bcdcf --- /dev/null +++ b/src/routes/api/auth/login/+server.ts @@ -0,0 +1,22 @@ +import { error, json } from "@sveltejs/kit"; +import { z } from "zod"; +import { login } from "$lib/server/auth"; +import type { RequestHandler } from "./$types"; + +export const POST: RequestHandler = async ({ request }) => { + const zodRes = z + .object({ + email: z.string().email().nonempty(), + password: z.string().nonempty(), + }) + .safeParse(await request.json()); + if (!zodRes.success) error(400, zodRes.error.message); + + const { email, password } = zodRes.data; + const loginRes = await login(email.trim(), password.trim()); + + if (!loginRes) error(401, "Invalid email or password"); + const { accessToken, refreshToken } = loginRes; + + return json({ accessToken, refreshToken }); +}; diff --git a/tsconfig.json b/tsconfig.json index f4d0a0e..33a6652 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,7 @@ "skipLibCheck": true, "sourceMap": true, "strict": true, + "noUncheckedIndexedAccess": true, "moduleResolution": "bundler" } // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias