Skip to content

Commit 269bf09

Browse files
authored
Express で作られた Cookie の切り替え対応 (#56)
1 parent 8b03763 commit 269bf09

File tree

3 files changed

+101
-70
lines changed

3 files changed

+101
-70
lines changed

server/src/main.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { PrismaClient } from "@prisma/client";
33
import dotenv from "dotenv";
44
import { Hono } from "hono";
55
import { cors } from "hono/cors";
6+
import { browserIdMiddleware } from "./middleware/browserId.js";
67
import projectsRoutes from "./routes/projects.js";
78

89
dotenv.config();
@@ -11,14 +12,19 @@ export const prisma = new PrismaClient();
1112
const port = process.env.PORT || 3000;
1213
const allowedOrigins = process.env.CORS_ALLOW_ORIGINS?.split(",") || [];
1314

14-
const app = new Hono()
15+
type AppVariables = {
16+
browserId: string;
17+
};
18+
19+
const app = new Hono<{ Variables: AppVariables }>()
1520
.use(
1621
"*",
1722
cors({
1823
origin: allowedOrigins,
1924
credentials: true,
2025
}),
2126
)
27+
.use("*", browserIdMiddleware)
2228
.get("/", (c) => {
2329
return c.json({ message: "Hello! イツヒマ?" });
2430
})

server/src/middleware/browserId.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import crypto from "node:crypto";
2+
import type { Context, MiddlewareHandler } from "hono";
3+
import { getCookie, getSignedCookie, setSignedCookie } from "hono/cookie";
4+
import { cookieOptions } from "../main.js";
5+
6+
const COOKIE_NAME = "browserId";
7+
8+
/**
9+
* Express の署名付きクッキー(s:value.signature 形式)を検証して値を取り出す
10+
*/
11+
function unsignExpressCookie(signedValue: string, secret: string): string | null {
12+
if (!signedValue.startsWith("s:")) return null;
13+
14+
const raw = signedValue.slice(2); // `s:` を削除
15+
const lastDotIndex = raw.lastIndexOf(".");
16+
if (lastDotIndex === -1) return null;
17+
18+
const value = raw.slice(0, lastDotIndex); // 署名前の値(uuid)
19+
const signature = raw.slice(lastDotIndex + 1);
20+
21+
// Express と同じアルゴリズムで署名を検証
22+
const expectedSignature = crypto.createHmac("sha256", secret).update(value).digest("base64").replace(/=+$/, "");
23+
24+
if (signature === expectedSignature) {
25+
return value;
26+
}
27+
28+
return null;
29+
}
30+
31+
/**
32+
* Express → Hono の移行で signed cookie 形式が変わったため両方に対応するミドルウェア
33+
*/
34+
export const browserIdMiddleware: MiddlewareHandler = async (c: Context, next) => {
35+
const cookieSecret = process.env.COOKIE_SECRET;
36+
if (!cookieSecret) {
37+
console.error("COOKIE_SECRET is not set");
38+
return c.json({ message: "サーバー設定エラー" }, 500);
39+
}
40+
41+
let browserId: string | undefined;
42+
let needsReissue = false;
43+
44+
// 新形式 (Hono) を試す
45+
browserId = (await getSignedCookie(c, cookieSecret, COOKIE_NAME)) || undefined;
46+
47+
if (!browserId) {
48+
const rawCookie = getCookie(c, COOKIE_NAME);
49+
50+
if (rawCookie?.startsWith("s:")) {
51+
const legacy = unsignExpressCookie(rawCookie, cookieSecret);
52+
if (legacy) {
53+
browserId = legacy;
54+
needsReissue = true;
55+
}
56+
}
57+
}
58+
59+
if (browserId && needsReissue) {
60+
await setSignedCookie(c, COOKIE_NAME, browserId, cookieSecret, cookieOptions);
61+
}
62+
63+
if (!browserId) {
64+
browserId = crypto.randomUUID();
65+
await setSignedCookie(c, COOKIE_NAME, browserId, cookieSecret, cookieOptions);
66+
}
67+
68+
// コンテキストに保存(後続のハンドラで c.get('browserId') で取得可能)
69+
c.set("browserId", browserId);
70+
71+
await next();
72+
};

server/src/routes/projects.ts

Lines changed: 22 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,23 @@
11
import { zValidator } from "@hono/zod-validator";
22
import dotenv from "dotenv";
33
import { Hono } from "hono";
4-
import { getSignedCookie, setSignedCookie } from "hono/cookie";
54
import { nanoid } from "nanoid";
65
import { z } from "zod";
76
import { editReqSchema, projectReqSchema, submitReqSchema } from "../../../common/validators.js";
8-
import { cookieOptions, prisma } from "../main.js";
7+
import { prisma } from "../main.js";
98

109
dotenv.config();
1110

1211
const projectIdParamsSchema = z.object({ projectId: z.string().length(21) });
1312

14-
const router = new Hono()
13+
type AppVariables = {
14+
browserId: string;
15+
};
16+
17+
const router = new Hono<{ Variables: AppVariables }>()
1518
// プロジェクト作成
1619
.post("/", zValidator("json", projectReqSchema), async (c) => {
17-
const cookieSecret = process.env.COOKIE_SECRET;
18-
if (!cookieSecret) {
19-
console.error("COOKIE_SECRET is not set");
20-
return c.json({ message: "サーバー設定エラー" }, 500);
21-
}
22-
const browserId = (await getSignedCookie(c, cookieSecret, "browserId")) || undefined;
20+
const browserId = c.get("browserId");
2321
try {
2422
const data = c.req.valid("json");
2523

@@ -51,9 +49,7 @@ const router = new Hono()
5149
},
5250
include: { hosts: true, participationOptions: true },
5351
});
54-
const host = event.hosts[0];
5552

56-
await setSignedCookie(c, "browserId", host.browserId, cookieSecret, cookieOptions);
5753
return c.json({ id: event.id, name: event.name }, 201);
5854
} catch (_err) {
5955
return c.json({ message: "イベント作成時にエラーが発生しました" }, 500);
@@ -62,16 +58,7 @@ const router = new Hono()
6258

6359
// 自分が関連するプロジェクト取得
6460
.get("/mine", async (c) => {
65-
const cookieSecret = process.env.COOKIE_SECRET;
66-
if (!cookieSecret) {
67-
console.error("COOKIE_SECRET is not set");
68-
return c.json({ message: "サーバー側でエラーが発生しています。" }, 500);
69-
}
70-
71-
const browserId = await getSignedCookie(c, cookieSecret, "browserId");
72-
if (!browserId) {
73-
return c.json({ message: "認証されていません。" }, 401);
74-
}
61+
const browserId = c.get("browserId");
7562

7663
try {
7764
const involvedProjects = await prisma.project.findMany({
@@ -118,13 +105,7 @@ const router = new Hono()
118105

119106
// プロジェクト取得
120107
.get("/:projectId", zValidator("param", projectIdParamsSchema), async (c) => {
121-
const cookieSecret = process.env.COOKIE_SECRET;
122-
if (!cookieSecret) {
123-
console.error("COOKIE_SECRET is not set");
124-
return c.json({ message: "サーバー側でエラーが発生しています。" }, 500);
125-
}
126-
127-
const browserId = await getSignedCookie(c, cookieSecret, "browserId");
108+
const browserId = c.get("browserId");
128109

129110
try {
130111
const { projectId } = c.req.valid("param");
@@ -157,8 +138,8 @@ const router = new Hono()
157138
const { browserId: _, ...rest } = g;
158139
return rest;
159140
}),
160-
isHost: browserId ? projectRow.hosts.some((h) => h.browserId === browserId) : false,
161-
meAsGuest: browserId ? (projectRow.guests.find((g) => g.browserId === browserId) ?? null) : null,
141+
isHost: projectRow.hosts.some((h) => h.browserId === browserId),
142+
meAsGuest: projectRow.guests.find((g) => g.browserId === browserId) ?? null,
162143
};
163144
return c.json(data, 200);
164145
} catch (error) {
@@ -169,12 +150,7 @@ const router = new Hono()
169150

170151
// プロジェクト編集
171152
.put("/:projectId", zValidator("param", projectIdParamsSchema), zValidator("json", editReqSchema), async (c) => {
172-
const cookieSecret = process.env.COOKIE_SECRET;
173-
if (!cookieSecret) {
174-
console.error("COOKIE_SECRET is not set");
175-
return c.json({ message: "サーバー設定エラー" }, 500);
176-
}
177-
const browserId = (await getSignedCookie(c, cookieSecret, "browserId")) || undefined;
153+
const browserId = c.get("browserId");
178154
try {
179155
const { projectId } = c.req.valid("param");
180156
const data = c.req.valid("json");
@@ -278,15 +254,7 @@ const router = new Hono()
278254

279255
// プロジェクト削除
280256
.delete("/:projectId", zValidator("param", projectIdParamsSchema), async (c) => {
281-
const cookieSecret = process.env.COOKIE_SECRET;
282-
if (!cookieSecret) {
283-
console.error("COOKIE_SECRET is not set");
284-
return c.json({ message: "サーバー設定エラー" }, 500);
285-
}
286-
const browserId = await getSignedCookie(c, cookieSecret, "browserId");
287-
if (!browserId) {
288-
return c.json({ message: "認証されていません。" }, 401);
289-
}
257+
const browserId = c.get("browserId");
290258
try {
291259
const { projectId } = c.req.valid("param");
292260
// Host 認証
@@ -314,26 +282,20 @@ const router = new Hono()
314282
zValidator("param", projectIdParamsSchema),
315283
zValidator("json", submitReqSchema),
316284
async (c) => {
317-
const cookieSecret = process.env.COOKIE_SECRET;
318-
if (!cookieSecret) {
319-
console.error("COOKIE_SECRET is not set");
320-
return c.json({ message: "サーバー設定エラー" }, 500);
321-
}
322285
const { projectId } = c.req.valid("param");
323-
const browserId = (await getSignedCookie(c, cookieSecret, "browserId")) || undefined;
286+
const browserId = c.get("browserId");
324287

325-
if (browserId) {
326-
const existingGuest = await prisma.guest.findFirst({
327-
where: { projectId, browserId },
328-
});
329-
if (existingGuest) {
330-
return c.json({ message: "すでに登録済みです" }, 403);
331-
}
288+
const existingGuest = await prisma.guest.findFirst({
289+
where: { projectId, browserId },
290+
});
291+
if (existingGuest) {
292+
return c.json({ message: "すでに登録済みです" }, 403);
332293
}
294+
333295
const { name, slots } = c.req.valid("json");
334296

335297
try {
336-
const guest = await prisma.guest.create({
298+
await prisma.guest.create({
337299
data: {
338300
name,
339301
browserId,
@@ -349,7 +311,6 @@ const router = new Hono()
349311
},
350312
include: { slots: true },
351313
});
352-
await setSignedCookie(c, "browserId", guest.browserId, cookieSecret, cookieOptions);
353314
return c.json("日時が登録されました!", 201);
354315
} catch (error) {
355316
console.error("登録エラー:", error);
@@ -364,17 +325,9 @@ const router = new Hono()
364325
zValidator("param", projectIdParamsSchema),
365326
zValidator("json", submitReqSchema),
366327
async (c) => {
367-
const cookieSecret = process.env.COOKIE_SECRET;
368-
if (!cookieSecret) {
369-
console.error("COOKIE_SECRET is not set");
370-
return c.json({ message: "サーバー設定エラー" }, 500);
371-
}
372328
const { projectId } = c.req.valid("param");
373-
const browserId = (await getSignedCookie(c, cookieSecret, "browserId")) || undefined;
329+
const browserId = c.get("browserId");
374330

375-
if (!browserId) {
376-
return c.json({ message: "認証されていません。" }, 401);
377-
}
378331
const { name, slots } = c.req.valid("json");
379332

380333
try {

0 commit comments

Comments
 (0)