diff options
| author | nsfisis <nsfisis@gmail.com> | 2025-12-03 05:34:44 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2025-12-04 23:26:27 +0900 |
| commit | 742c3e55b08b37d0eb031f72a6d952bc7b7164b3 (patch) | |
| tree | 119f091be511a9d870815fed9a1b98b86d638376 /pkgs/server/src/routes/auth.ts | |
| parent | 950217ed3ca93a0aa0e964c2a8474ffc13c71912 (diff) | |
| download | kioku-742c3e55b08b37d0eb031f72a6d952bc7b7164b3.tar.gz kioku-742c3e55b08b37d0eb031f72a6d952bc7b7164b3.tar.zst kioku-742c3e55b08b37d0eb031f72a6d952bc7b7164b3.zip | |
feat(auth): add login endpoint with JWT
Implement POST /api/auth/login endpoint that validates credentials
and returns a JWT access token on successful authentication.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Diffstat (limited to 'pkgs/server/src/routes/auth.ts')
| -rw-r--r-- | pkgs/server/src/routes/auth.ts | 66 |
1 files changed, 65 insertions, 1 deletions
diff --git a/pkgs/server/src/routes/auth.ts b/pkgs/server/src/routes/auth.ts index 3906d65..ed497b1 100644 --- a/pkgs/server/src/routes/auth.ts +++ b/pkgs/server/src/routes/auth.ts @@ -1,10 +1,17 @@ -import { createUserSchema } from "@kioku/shared"; +import { createUserSchema, loginSchema } from "@kioku/shared"; import * as argon2 from "argon2"; import { eq } from "drizzle-orm"; import { Hono } from "hono"; +import { sign } from "hono/jwt"; import { db, users } from "../db"; import { Errors } from "../middleware"; +const JWT_SECRET = process.env.JWT_SECRET; +if (!JWT_SECRET) { + throw new Error("JWT_SECRET environment variable is required"); +} +const ACCESS_TOKEN_EXPIRES_IN = 60 * 15; // 15 minutes + const auth = new Hono(); auth.post("/register", async (c) => { @@ -47,4 +54,61 @@ auth.post("/register", async (c) => { return c.json({ user: newUser }, 201); }); +auth.post("/login", async (c) => { + const body = await c.req.json(); + + const parsed = loginSchema.safeParse(body); + if (!parsed.success) { + throw Errors.validationError(parsed.error.issues[0]?.message); + } + + const { username, password } = parsed.data; + + // Find user by username + const [user] = await db + .select({ + id: users.id, + username: users.username, + passwordHash: users.passwordHash, + }) + .from(users) + .where(eq(users.username, username)) + .limit(1); + + if (!user) { + throw Errors.unauthorized( + "Invalid username or password", + "INVALID_CREDENTIALS", + ); + } + + // Verify password + const isPasswordValid = await argon2.verify(user.passwordHash, password); + if (!isPasswordValid) { + throw Errors.unauthorized( + "Invalid username or password", + "INVALID_CREDENTIALS", + ); + } + + // Generate JWT access token + const now = Math.floor(Date.now() / 1000); + const accessToken = await sign( + { + sub: user.id, + iat: now, + exp: now + ACCESS_TOKEN_EXPIRES_IN, + }, + JWT_SECRET, + ); + + return c.json({ + accessToken, + user: { + id: user.id, + username: user.username, + }, + }); +}); + export { auth }; |
