import { Router, Request, Response, NextFunction } from 'express'; import bcrypt from 'bcryptjs'; import jwt from 'jsonwebtoken'; import { z } from 'zod'; import prisma from '../lib/prisma'; import { authenticate } from '../middleware/auth'; import { AppError } from '../lib/errors'; const router = Router(); const JWT_SECRET = process.env.JWT_SECRET || 'change-me-in-production'; const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET || 'change-me-too'; const ACCESS_TOKEN_EXPIRY = '15m'; const REFRESH_TOKEN_EXPIRY = '7d'; // Store for invalidated refresh tokens (in production, use Redis) const invalidatedTokens = new Set(); const loginSchema = z.object({ email: z.string().email('Invalid email format'), password: z.string().min(1, 'Password is required'), }); const refreshSchema = z.object({ refreshToken: z.string().min(1, 'Refresh token is required'), }); function generateTokens(user: { id: string; email: string; role: string; tenantId: string | null; firstName: string; lastName: string }) { const payload = { id: user.id, email: user.email, role: user.role, tenantId: user.tenantId, firstName: user.firstName, lastName: user.lastName, }; const accessToken = jwt.sign(payload, JWT_SECRET, { expiresIn: ACCESS_TOKEN_EXPIRY }); const refreshToken = jwt.sign({ id: user.id, type: 'refresh' }, JWT_REFRESH_SECRET, { expiresIn: REFRESH_TOKEN_EXPIRY }); return { accessToken, refreshToken }; } // POST /api/auth/login router.post('/login', async (req: Request, res: Response, next: NextFunction) => { try { const parsed = loginSchema.safeParse(req.body); if (!parsed.success) { res.status(400).json({ error: 'Validation error', details: parsed.error.flatten().fieldErrors, }); return; } const { email, password } = parsed.data; const user = await prisma.user.findUnique({ where: { email }, include: { tenant: true }, }); if (!user || !user.active) { res.status(401).json({ error: 'Invalid credentials' }); return; } const validPassword = await bcrypt.compare(password, user.passwordHash); if (!validPassword) { res.status(401).json({ error: 'Invalid credentials' }); return; } const tokens = generateTokens(user); res.json({ ...tokens, user: { id: user.id, email: user.email, firstName: user.firstName, lastName: user.lastName, role: user.role, tenantId: user.tenantId, tenant: user.tenant ? { id: user.tenant.id, name: user.tenant.name, slug: user.tenant.slug } : null, }, }); } catch (err) { next(err); } }); // POST /api/auth/refresh router.post('/refresh', async (req: Request, res: Response, next: NextFunction) => { try { const parsed = refreshSchema.safeParse(req.body); if (!parsed.success) { res.status(400).json({ error: 'Validation error', details: parsed.error.flatten().fieldErrors, }); return; } const { refreshToken } = parsed.data; if (invalidatedTokens.has(refreshToken)) { res.status(401).json({ error: 'Token has been invalidated' }); return; } let decoded: any; try { decoded = jwt.verify(refreshToken, JWT_REFRESH_SECRET); } catch { res.status(401).json({ error: 'Invalid refresh token' }); return; } if (decoded.type !== 'refresh') { res.status(401).json({ error: 'Invalid token type' }); return; } const user = await prisma.user.findUnique({ where: { id: decoded.id }, }); if (!user || !user.active) { res.status(401).json({ error: 'User not found or inactive' }); return; } // Invalidate old refresh token invalidatedTokens.add(refreshToken); const tokens = generateTokens(user); res.json(tokens); } catch (err) { next(err); } }); // POST /api/auth/logout router.post('/logout', authenticate, async (req: Request, res: Response, next: NextFunction) => { try { const refreshToken = req.body.refreshToken; if (refreshToken) { invalidatedTokens.add(refreshToken); } res.json({ message: 'Logged out successfully' }); } catch (err) { next(err); } }); // GET /api/auth/me router.get('/me', authenticate, async (req: Request, res: Response, next: NextFunction) => { try { const user = await prisma.user.findUnique({ where: { id: req.user!.id }, include: { tenant: true }, }); if (!user) { res.status(404).json({ error: 'User not found' }); return; } res.json({ id: user.id, email: user.email, firstName: user.firstName, lastName: user.lastName, role: user.role, tenantId: user.tenantId, tenant: user.tenant ? { id: user.tenant.id, name: user.tenant.name, slug: user.tenant.slug, logo: user.tenant.logo } : null, createdAt: user.createdAt, updatedAt: user.updatedAt, }); } catch (err) { next(err); } }); export default router;