{project.name} の日程調整
{isHost && (
-
+
イベント設定
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..948ce5d 100644
--- a/server/src/routes/projects.ts
+++ b/server/src/routes/projects.ts
@@ -1,25 +1,28 @@
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 { customAlphabet } 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();
+/**
+ * ハイフン・アンダースコアを含まない Nano ID 形式。
+ */
+const nanoid = customAlphabet("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", 21);
+
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 +54,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 +63,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 +110,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 +143,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 +155,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 +259,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 +287,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 +316,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 +330,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 {