From fafb7fb1264db879713ac899f515d71e0e63a133 Mon Sep 17 00:00:00 2001 From: nakaterm <104970808+nakaterm@users.noreply.github.com> Date: Sun, 23 Nov 2025 08:45:44 +0900 Subject: [PATCH] =?UTF-8?q?Express=20=E3=81=A7=E4=BD=9C=E3=82=89=E3=82=8C?= =?UTF-8?q?=E3=81=9F=20Cookie=20=E3=81=AE=E5=88=87=E3=82=8A=E6=9B=BF?= =?UTF-8?q?=E3=81=88=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/main.ts | 8 ++- server/src/middleware/browserId.ts | 72 +++++++++++++++++++++++ server/src/routes/projects.ts | 91 ++++++++---------------------- 3 files changed, 101 insertions(+), 70 deletions(-) create mode 100644 server/src/middleware/browserId.ts diff --git a/server/src/main.ts b/server/src/main.ts index 5813fd3..2898e46 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -3,6 +3,7 @@ import { PrismaClient } from "@prisma/client"; import dotenv from "dotenv"; import { Hono } from "hono"; import { cors } from "hono/cors"; +import { browserIdMiddleware } from "./middleware/browserId.js"; import projectsRoutes from "./routes/projects.js"; dotenv.config(); @@ -11,7 +12,11 @@ export const prisma = new PrismaClient(); const port = process.env.PORT || 3000; const allowedOrigins = process.env.CORS_ALLOW_ORIGINS?.split(",") || []; -const app = new Hono() +type AppVariables = { + browserId: string; +}; + +const app = new Hono<{ Variables: AppVariables }>() .use( "*", cors({ @@ -19,6 +24,7 @@ const app = new Hono() credentials: true, }), ) + .use("*", browserIdMiddleware) .get("/", (c) => { return c.json({ message: "Hello! イツヒマ?" }); }) diff --git a/server/src/middleware/browserId.ts b/server/src/middleware/browserId.ts new file mode 100644 index 0000000..cdde0c0 --- /dev/null +++ b/server/src/middleware/browserId.ts @@ -0,0 +1,72 @@ +import crypto from "node:crypto"; +import type { Context, MiddlewareHandler } from "hono"; +import { getCookie, getSignedCookie, setSignedCookie } from "hono/cookie"; +import { cookieOptions } from "../main.js"; + +const COOKIE_NAME = "browserId"; + +/** + * Express の署名付きクッキー(s:value.signature 形式)を検証して値を取り出す + */ +function unsignExpressCookie(signedValue: string, secret: string): string | null { + if (!signedValue.startsWith("s:")) return null; + + const raw = signedValue.slice(2); // `s:` を削除 + const lastDotIndex = raw.lastIndexOf("."); + if (lastDotIndex === -1) return null; + + const value = raw.slice(0, lastDotIndex); // 署名前の値(uuid) + const signature = raw.slice(lastDotIndex + 1); + + // Express と同じアルゴリズムで署名を検証 + const expectedSignature = crypto.createHmac("sha256", secret).update(value).digest("base64").replace(/=+$/, ""); + + if (signature === expectedSignature) { + return value; + } + + return null; +} + +/** + * Express → Hono の移行で signed cookie 形式が変わったため両方に対応するミドルウェア + */ +export const browserIdMiddleware: MiddlewareHandler = async (c: Context, next) => { + const cookieSecret = process.env.COOKIE_SECRET; + if (!cookieSecret) { + console.error("COOKIE_SECRET is not set"); + return c.json({ message: "サーバー設定エラー" }, 500); + } + + let browserId: string | undefined; + let needsReissue = false; + + // 新形式 (Hono) を試す + browserId = (await getSignedCookie(c, cookieSecret, COOKIE_NAME)) || undefined; + + if (!browserId) { + const rawCookie = getCookie(c, COOKIE_NAME); + + if (rawCookie?.startsWith("s:")) { + const legacy = unsignExpressCookie(rawCookie, cookieSecret); + if (legacy) { + browserId = legacy; + needsReissue = true; + } + } + } + + if (browserId && needsReissue) { + await setSignedCookie(c, COOKIE_NAME, browserId, cookieSecret, cookieOptions); + } + + if (!browserId) { + browserId = crypto.randomUUID(); + await setSignedCookie(c, COOKIE_NAME, browserId, cookieSecret, cookieOptions); + } + + // コンテキストに保存(後続のハンドラで c.get('browserId') で取得可能) + c.set("browserId", browserId); + + await next(); +}; diff --git a/server/src/routes/projects.ts b/server/src/routes/projects.ts index a74c6bc..89de447 100644 --- a/server/src/routes/projects.ts +++ b/server/src/routes/projects.ts @@ -1,25 +1,23 @@ import { zValidator } from "@hono/zod-validator"; import dotenv from "dotenv"; import { Hono } from "hono"; -import { getSignedCookie, setSignedCookie } from "hono/cookie"; import { nanoid } from "nanoid"; import { z } from "zod"; import { editReqSchema, projectReqSchema, submitReqSchema } from "../../../common/validators.js"; -import { cookieOptions, prisma } from "../main.js"; +import { prisma } from "../main.js"; dotenv.config(); const projectIdParamsSchema = z.object({ projectId: z.string().length(21) }); -const router = new Hono() +type AppVariables = { + browserId: string; +}; + +const router = new Hono<{ Variables: AppVariables }>() // プロジェクト作成 .post("/", zValidator("json", projectReqSchema), async (c) => { - const cookieSecret = process.env.COOKIE_SECRET; - if (!cookieSecret) { - console.error("COOKIE_SECRET is not set"); - return c.json({ message: "サーバー設定エラー" }, 500); - } - const browserId = (await getSignedCookie(c, cookieSecret, "browserId")) || undefined; + const browserId = c.get("browserId"); try { const data = c.req.valid("json"); @@ -51,9 +49,7 @@ const router = new Hono() }, include: { hosts: true, participationOptions: true }, }); - const host = event.hosts[0]; - await setSignedCookie(c, "browserId", host.browserId, cookieSecret, cookieOptions); return c.json({ id: event.id, name: event.name }, 201); } catch (_err) { return c.json({ message: "イベント作成時にエラーが発生しました" }, 500); @@ -62,16 +58,7 @@ const router = new Hono() // 自分が関連するプロジェクト取得 .get("/mine", async (c) => { - const cookieSecret = process.env.COOKIE_SECRET; - if (!cookieSecret) { - console.error("COOKIE_SECRET is not set"); - return c.json({ message: "サーバー側でエラーが発生しています。" }, 500); - } - - const browserId = await getSignedCookie(c, cookieSecret, "browserId"); - if (!browserId) { - return c.json({ message: "認証されていません。" }, 401); - } + const browserId = c.get("browserId"); try { const involvedProjects = await prisma.project.findMany({ @@ -118,13 +105,7 @@ const router = new Hono() // プロジェクト取得 .get("/:projectId", zValidator("param", projectIdParamsSchema), async (c) => { - const cookieSecret = process.env.COOKIE_SECRET; - if (!cookieSecret) { - console.error("COOKIE_SECRET is not set"); - return c.json({ message: "サーバー側でエラーが発生しています。" }, 500); - } - - const browserId = await getSignedCookie(c, cookieSecret, "browserId"); + const browserId = c.get("browserId"); try { const { projectId } = c.req.valid("param"); @@ -157,8 +138,8 @@ const router = new Hono() const { browserId: _, ...rest } = g; return rest; }), - isHost: browserId ? projectRow.hosts.some((h) => h.browserId === browserId) : false, - meAsGuest: browserId ? (projectRow.guests.find((g) => g.browserId === browserId) ?? null) : null, + isHost: projectRow.hosts.some((h) => h.browserId === browserId), + meAsGuest: projectRow.guests.find((g) => g.browserId === browserId) ?? null, }; return c.json(data, 200); } catch (error) { @@ -169,12 +150,7 @@ const router = new Hono() // プロジェクト編集 .put("/:projectId", zValidator("param", projectIdParamsSchema), zValidator("json", editReqSchema), async (c) => { - const cookieSecret = process.env.COOKIE_SECRET; - if (!cookieSecret) { - console.error("COOKIE_SECRET is not set"); - return c.json({ message: "サーバー設定エラー" }, 500); - } - const browserId = (await getSignedCookie(c, cookieSecret, "browserId")) || undefined; + const browserId = c.get("browserId"); try { const { projectId } = c.req.valid("param"); const data = c.req.valid("json"); @@ -278,15 +254,7 @@ const router = new Hono() // プロジェクト削除 .delete("/:projectId", zValidator("param", projectIdParamsSchema), async (c) => { - const cookieSecret = process.env.COOKIE_SECRET; - if (!cookieSecret) { - console.error("COOKIE_SECRET is not set"); - return c.json({ message: "サーバー設定エラー" }, 500); - } - const browserId = await getSignedCookie(c, cookieSecret, "browserId"); - if (!browserId) { - return c.json({ message: "認証されていません。" }, 401); - } + const browserId = c.get("browserId"); try { const { projectId } = c.req.valid("param"); // Host 認証 @@ -314,26 +282,20 @@ const router = new Hono() zValidator("param", projectIdParamsSchema), zValidator("json", submitReqSchema), async (c) => { - const cookieSecret = process.env.COOKIE_SECRET; - if (!cookieSecret) { - console.error("COOKIE_SECRET is not set"); - return c.json({ message: "サーバー設定エラー" }, 500); - } const { projectId } = c.req.valid("param"); - const browserId = (await getSignedCookie(c, cookieSecret, "browserId")) || undefined; + const browserId = c.get("browserId"); - if (browserId) { - const existingGuest = await prisma.guest.findFirst({ - where: { projectId, browserId }, - }); - if (existingGuest) { - return c.json({ message: "すでに登録済みです" }, 403); - } + const existingGuest = await prisma.guest.findFirst({ + where: { projectId, browserId }, + }); + if (existingGuest) { + return c.json({ message: "すでに登録済みです" }, 403); } + const { name, slots } = c.req.valid("json"); try { - const guest = await prisma.guest.create({ + await prisma.guest.create({ data: { name, browserId, @@ -349,7 +311,6 @@ const router = new Hono() }, include: { slots: true }, }); - await setSignedCookie(c, "browserId", guest.browserId, cookieSecret, cookieOptions); return c.json("日時が登録されました!", 201); } catch (error) { console.error("登録エラー:", error); @@ -364,17 +325,9 @@ const router = new Hono() zValidator("param", projectIdParamsSchema), zValidator("json", submitReqSchema), async (c) => { - const cookieSecret = process.env.COOKIE_SECRET; - if (!cookieSecret) { - console.error("COOKIE_SECRET is not set"); - return c.json({ message: "サーバー設定エラー" }, 500); - } const { projectId } = c.req.valid("param"); - const browserId = (await getSignedCookie(c, cookieSecret, "browserId")) || undefined; + const browserId = c.get("browserId"); - if (!browserId) { - return c.json({ message: "認証されていません。" }, 401); - } const { name, slots } = c.req.valid("json"); try {