Skip to content
File

Blob: src/worker/routes/auth.ts

typescript90 lines
1import { Hono } from "hono";
2import { getCookie } from "hono/cookie";
3import { eq } from "drizzle-orm";
4import { jwtVerify } from "jose";
5 
6import type { AppContext } from "@/worker/app-context";
7import { users } from "@/worker/db/d1/schema";
8import { requireAuth } from "@/worker/middleware/auth";
9import { rateLimit } from "@/worker/middleware/rate-limit";
10import { createAccessToken, clearRefreshCookie, toUserResponse, getJwtSecret, REFRESH_COOKIE } from "@/worker/lib/auth";
11import { parseBody } from "@/worker/lib/validate";
12import { createLogger } from "@/worker/lib/logger";
13import { JWT_ALGORITHM } from "@/worker/lib/constants";
14import { UpdateProfileRequest } from "@/shared/types";
15 
16const auth = new Hono<AppContext>();
17const log = createLogger("auth");
18 
19// POST /auth/refresh
20auth.post("/auth/refresh", rateLimit("RL_AUTH"), async (c) => {
21 const refreshToken = getCookie(c, REFRESH_COOKIE);
22 log.debug("refresh_attempt");
23 
24 if (!refreshToken) {
25 log.info("refresh_failed", { reason: "no_token" });
26 return c.json({ error: "unauthorized", message: "No refresh token" }, 401);
27 }
28 
29 try {
30 const { payload } = await jwtVerify(refreshToken, getJwtSecret(c.env), {
31 algorithms: [JWT_ALGORITHM],
32 });
33 
34 if (!payload.sub || payload.type !== "refresh") {
35 log.info("refresh_failed", { reason: "invalid_token" });
36 return c.json({ error: "unauthorized", message: "Invalid refresh token" }, 401);
37 }
38 
39 const db = c.get("db");
40 const user = await db.select().from(users).where(eq(users.id, payload.sub)).get();
41 
42 if (!user) {
43 log.info("refresh_failed", { reason: "user_not_found" });
44 clearRefreshCookie(c);
45 return c.json({ error: "unauthorized", message: "User not found" }, 401);
46 }
47 const accessToken = await createAccessToken(user.id, c.env);
48 log.info("refresh_success", { userId: user.id });
49 
50 return c.json({ user: toUserResponse(user), accessToken });
51 } catch {
52 log.info("refresh_failed", { reason: "expired_or_invalid" });
53 clearRefreshCookie(c);
54 return c.json({ error: "unauthorized", message: "Invalid or expired refresh token" }, 401);
55 }
56});
57 
58// POST /auth/logout
59auth.post("/auth/logout", rateLimit("RL_API"), (c) => {
60 log.debug("logout");
61 clearRefreshCookie(c);
62 return c.json({ ok: true });
63});
64 
65// GET /auth/me
66auth.get("/auth/me", requireAuth, rateLimit("RL_API"), async (c) => {
67 const user = c.get("user")!;
68 return c.json({ user: toUserResponse(user) });
69});
70 
71// PATCH /auth/me - Update profile
72auth.patch("/auth/me", requireAuth, rateLimit("RL_API"), async (c) => {
73 const user = c.get("user")!;
74 const db = c.get("db");
75 
76 const data = await parseBody(c, UpdateProfileRequest);
77 if (data instanceof Response) return data;
78 
79 const updates: Record<string, string | null> = { updated_at: new Date().toISOString() };
80 if (data.name !== undefined) updates.name = data.name;
81 if (data.avatar_url !== undefined) updates.avatar_url = data.avatar_url;
82 
83 await db.update(users).set(updates).where(eq(users.id, user.id));
84 const updated = await db.select().from(users).where(eq(users.id, user.id)).get();
85 
86 return c.json({ user: toUserResponse(updated!) });
87});
88 
89export { auth };