diff --git a/server/src/auth/jwt.ts b/server/src/auth/jwt.ts new file mode 100644 index 0000000..148df8f --- /dev/null +++ b/server/src/auth/jwt.ts @@ -0,0 +1,19 @@ +import jwt from "jsonwebtoken"; + +const SECRET = process.env.JWT_SECRET ?? "dev_jwt_secret_change_me"; +export const COOKIE_NAME = "darkwatch_token"; +const EXPIRY = "7d"; + +export interface JWTPayload { + userId: number; + email: string; + username: string; +} + +export function signToken(payload: JWTPayload): string { + return jwt.sign(payload, SECRET, { expiresIn: EXPIRY }); +} + +export function verifyToken(token: string): JWTPayload { + return jwt.verify(token, SECRET) as JWTPayload; +} diff --git a/server/src/auth/middleware.ts b/server/src/auth/middleware.ts new file mode 100644 index 0000000..067d7af --- /dev/null +++ b/server/src/auth/middleware.ts @@ -0,0 +1,47 @@ +import type { Request, Response, NextFunction } from "express"; +import type { RowDataPacket } from "mysql2"; +import db from "../db.js"; +import { verifyToken, COOKIE_NAME } from "./jwt.js"; +import type { JWTPayload } from "./jwt.js"; + +declare global { + namespace Express { + interface Request { + user?: JWTPayload; + } + } +} + +export function requireAuth(req: Request, res: Response, next: NextFunction): void { + const token = req.cookies?.[COOKIE_NAME]; + if (!token) { + res.status(401).json({ error: "Unauthorized" }); + return; + } + try { + req.user = verifyToken(token); + next(); + } catch { + res.status(401).json({ error: "Unauthorized" }); + } +} + +export function requireCampaignRole(role: "dm" | "player") { + return async (req: Request, res: Response, next: NextFunction): Promise => { + const campaignId = req.params.campaignId ?? req.params.id; + const userId = req.user!.userId; + const [rows] = await db.execute( + "SELECT role FROM campaign_members WHERE campaign_id = ? AND user_id = ?", + [campaignId, userId] + ); + if (rows.length === 0) { + res.status(403).json({ error: "Not a campaign member" }); + return; + } + if (role === "dm" && rows[0].role !== "dm") { + res.status(403).json({ error: "DM access required" }); + return; + } + next(); + }; +}