feat: add JWT utility and requireAuth/requireCampaignRole middleware

This commit is contained in:
Aaron Wood 2026-04-11 00:21:53 -04:00
parent 39f8220eb7
commit bd433286ae
2 changed files with 66 additions and 0 deletions

19
server/src/auth/jwt.ts Normal file
View file

@ -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;
}

View file

@ -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<void> => {
const campaignId = req.params.campaignId ?? req.params.id;
const userId = req.user!.userId;
const [rows] = await db.execute<RowDataPacket[]>(
"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();
};
}