Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion server/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -11,14 +12,19 @@ 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({
origin: allowedOrigins,
credentials: true,
}),
)
.use("*", browserIdMiddleware)
.get("/", (c) => {
return c.json({ message: "Hello! イツヒマ?" });
})
Expand Down
72 changes: 72 additions & 0 deletions server/src/middleware/browserId.ts
Original file line number Diff line number Diff line change
@@ -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();
};
91 changes: 22 additions & 69 deletions server/src/routes/projects.ts
Original file line number Diff line number Diff line change
@@ -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");

Expand Down Expand Up @@ -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);
Expand All @@ -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({
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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) {
Expand All @@ -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");
Expand Down Expand Up @@ -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 認証
Expand Down Expand Up @@ -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,
Expand All @@ -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);
Expand All @@ -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 {
Expand Down