import { Router, Request, Response, NextFunction } from "express"; import bcrypt from "bcryptjs"; import jwt from "jsonwebtoken"; import { z } from "zod"; import { prisma } from "../lib/prisma.js"; import { AppError } from "../middleware/errorHandler.js"; import { requireAuth } from "../middleware/auth.js"; import { AuthenticatedRequest } from "../types/index.js"; const router = Router(); const ACCESS_TOKEN_TTL = "15m"; const REFRESH_TOKEN_TTL = "7d"; const REFRESH_TOKEN_TTL_MS = 7 * 24 * 60 * 60 * 1000; function signAccessToken(payload: object): string { const secret = process.env.JWT_SECRET; if (!secret) throw new Error("JWT_SECRET not configured"); return jwt.sign(payload, secret, { expiresIn: ACCESS_TOKEN_TTL }); } function signRefreshToken(payload: object): string { const secret = process.env.JWT_SECRET; if (!secret) throw new Error("JWT_SECRET not configured"); return jwt.sign(payload, secret, { expiresIn: REFRESH_TOKEN_TTL }); } // POST /api/v1/auth/login const LoginSchema = z.object({ email: z.string().email(), password: z.string().min(1), }); router.post( "/login", async (req: Request, res: Response, next: NextFunction) => { try { const body = LoginSchema.parse(req.body); const user = await prisma.user.findUnique({ where: { email: body.email }, include: { role: true, vendor: true }, }); if (!user || !(await bcrypt.compare(body.password, user.passwordHash))) { throw new AppError(401, "INVALID_CREDENTIALS", "Invalid email or password"); } const tokenPayload = { userId: user.id, vendorId: user.vendorId, roleId: user.roleId, roleName: user.role.name, }; const accessToken = signAccessToken(tokenPayload); const refreshToken = signRefreshToken({ userId: user.id }); await prisma.refreshToken.create({ data: { token: refreshToken, userId: user.id, expiresAt: new Date(Date.now() + REFRESH_TOKEN_TTL_MS), }, }); res.json({ accessToken, refreshToken, user: { id: user.id, email: user.email, name: user.name, role: user.role.name, vendorId: user.vendorId, vendorName: user.vendor.name, }, }); } catch (err) { next(err); } } ); // POST /api/v1/auth/refresh const RefreshSchema = z.object({ refreshToken: z.string().min(1), }); router.post( "/refresh", async (req: Request, res: Response, next: NextFunction) => { try { const { refreshToken } = RefreshSchema.parse(req.body); const secret = process.env.JWT_SECRET; if (!secret) throw new AppError(500, "CONFIG_ERROR", "JWT secret not configured"); let decoded: { userId: string }; try { decoded = jwt.verify(refreshToken, secret) as { userId: string }; } catch { throw new AppError(401, "INVALID_TOKEN", "Invalid or expired refresh token"); } const stored = await prisma.refreshToken.findUnique({ where: { token: refreshToken }, include: { user: { include: { role: true } } }, }); if (!stored || stored.userId !== decoded.userId || stored.expiresAt < new Date()) { throw new AppError(401, "INVALID_TOKEN", "Refresh token not found or expired"); } // Rotate refresh token await prisma.refreshToken.delete({ where: { id: stored.id } }); const user = stored.user; const tokenPayload = { userId: user.id, vendorId: user.vendorId, roleId: user.roleId, roleName: user.role.name, }; const newAccessToken = signAccessToken(tokenPayload); const newRefreshToken = signRefreshToken({ userId: user.id }); await prisma.refreshToken.create({ data: { token: newRefreshToken, userId: user.id, expiresAt: new Date(Date.now() + REFRESH_TOKEN_TTL_MS), }, }); res.json({ accessToken: newAccessToken, refreshToken: newRefreshToken }); } catch (err) { next(err); } } ); // POST /api/v1/auth/logout router.post( "/logout", requireAuth as unknown as (req: Request, res: Response, next: NextFunction) => void, async (req: Request, res: Response, next: NextFunction) => { try { const authReq = req as AuthenticatedRequest; const { refreshToken } = z.object({ refreshToken: z.string().optional() }).parse(req.body); if (refreshToken) { await prisma.refreshToken.deleteMany({ where: { token: refreshToken, userId: authReq.auth.userId }, }); } else { // Logout all devices await prisma.refreshToken.deleteMany({ where: { userId: authReq.auth.userId }, }); } res.json({ message: "Logged out successfully" }); } catch (err) { next(err); } } ); // GET /api/v1/auth/me router.get( "/me", requireAuth as unknown as (req: Request, res: Response, next: NextFunction) => void, async (req: Request, res: Response, next: NextFunction) => { try { const authReq = req as AuthenticatedRequest; const user = await prisma.user.findUnique({ where: { id: authReq.auth.userId }, include: { role: true, vendor: true }, }); if (!user) throw new AppError(404, "NOT_FOUND", "User not found"); res.json({ id: user.id, email: user.email, name: user.name, role: user.role.name, vendorId: user.vendorId, vendorName: user.vendor.name, }); } catch (err) { next(err); } } ); export default router;