diff --git a/packages/backend/src/middleware/auth.ts b/packages/backend/src/middleware/auth.ts new file mode 100644 index 0000000..11e80a6 --- /dev/null +++ b/packages/backend/src/middleware/auth.ts @@ -0,0 +1,56 @@ +import type { Request, Response, NextFunction } from 'express'; +import { eq } from 'drizzle-orm'; +import type { Db } from '../db/client.js'; +import { users } from '../db/schema.js'; +import { validateAuthSession } from '../services/auth/sessions.js'; +import { ApiError } from '../lib/errors.js'; +import { SID_COOKIE } from '../lib/cookies.js'; +import type { Role, User } from '@flashcard/shared'; + +declare module 'express-serve-static-core' { + interface Request { + user?: User; + sessionId?: string; + } +} + +function rowToUser(r: typeof users.$inferSelect): User { + return { + id: r.id, + email: r.email, + displayName: r.displayName, + role: r.role, + isActive: r.isActive, + emailVerifiedAt: r.emailVerifiedAt ?? null, + pendingEmail: r.pendingEmail ?? null, + createdAt: r.createdAt, + updatedAt: r.updatedAt, + }; +} + +export function currentUserOrNull(db: Db) { + return async (req: Request, _res: Response, next: NextFunction) => { + const sid = req.cookies?.[SID_COOKIE]; + if (!sid) return next(); + const session = await validateAuthSession(db, sid); + if (!session) return next(); + const row = db.select().from(users).where(eq(users.id, session.userId)).get(); + if (!row || !row.isActive) return next(); + req.user = rowToUser(row); + req.sessionId = sid; + next(); + }; +} + +export function requireAuth(req: Request, _res: Response, next: NextFunction): void { + if (!req.user) return next(new ApiError(401, 'UNAUTHENTICATED', 'Authentication required')); + next(); +} + +export function requireRole(role: Role) { + return (req: Request, _res: Response, next: NextFunction): void => { + if (!req.user) return next(new ApiError(401, 'UNAUTHENTICATED', 'Authentication required')); + if (req.user.role !== role) return next(new ApiError(403, 'FORBIDDEN', 'Insufficient role')); + next(); + }; +}