import { Router, type Router as RouterType } from 'express'; import { z } from 'zod'; import { prisma } from '@agent-fox/shared'; import { hashPassword, verifyPassword } from '../lib/password.js'; import { generateTokenPair, verifyRefreshToken } from '../lib/jwt.js'; import { requireAuth } from '../middleware/auth.js'; import { generateApiKey } from '../lib/api-key.js'; import { encryptApiKey, decryptApiKey } from '../lib/crypto.js'; const router: RouterType = Router(); const registerSchema = z.object({ email: z.string().email(), password: z.string().min(8), name: z.string().min(1).max(100), }); const loginSchema = z.object({ email: z.string().email(), password: z.string(), }); router.post('/register', async (req, res) => { const parsed = registerSchema.safeParse(req.body); if (!parsed.success) { res.status(400).json({ success: false, error: { code: 'VALIDATION', message: parsed.error.issues[0].message } }); return; } const { email, password, name } = parsed.data; const existing = await prisma.user.findUnique({ where: { email } }); if (existing) { res.status(409).json({ success: false, error: { code: 'CONFLICT', message: 'Email already registered' } }); return; } const passwordHash = await hashPassword(password); const user = await prisma.user.create({ data: { email, passwordHash, name }, }); const tokens = generateTokenPair({ userId: user.id, email: user.email }); res.status(201).json({ success: true, data: { user: { id: user.id, email: user.email, name: user.name }, ...tokens } }); }); router.post('/login', async (req, res) => { const parsed = loginSchema.safeParse(req.body); if (!parsed.success) { res.status(400).json({ success: false, error: { code: 'VALIDATION', message: parsed.error.issues[0].message } }); return; } const { email, password } = parsed.data; const user = await prisma.user.findUnique({ where: { email } }); if (!user || !user.passwordHash) { res.status(401).json({ success: false, error: { code: 'UNAUTHORIZED', message: 'Invalid credentials' } }); return; } const valid = await verifyPassword(password, user.passwordHash); if (!valid) { res.status(401).json({ success: false, error: { code: 'UNAUTHORIZED', message: 'Invalid credentials' } }); return; } const tokens = generateTokenPair({ userId: user.id, email: user.email }); res.json({ success: true, data: { user: { id: user.id, email: user.email, name: user.name }, ...tokens } }); }); router.post('/refresh', async (req, res) => { const { refreshToken } = req.body; if (!refreshToken) { res.status(400).json({ success: false, error: { code: 'VALIDATION', message: 'Refresh token required' } }); return; } try { const payload = verifyRefreshToken(refreshToken); const user = await prisma.user.findUnique({ where: { id: payload.userId } }); if (!user) { res.status(401).json({ success: false, error: { code: 'UNAUTHORIZED', message: 'User not found' } }); return; } const tokens = generateTokenPair({ userId: user.id, email: user.email }); res.json({ success: true, data: tokens }); } catch { res.status(401).json({ success: false, error: { code: 'UNAUTHORIZED', message: 'Invalid refresh token' } }); } }); const setPasswordSchema = z.object({ password: z.string().min(8), }); router.post('/set-password', requireAuth, async (req, res) => { const parsed = setPasswordSchema.safeParse(req.body); if (!parsed.success) { res.status(400).json({ success: false, error: { code: 'VALIDATION', message: parsed.error.issues[0].message } }); return; } const user = await prisma.user.findUnique({ where: { id: req.user!.userId } }); if (!user) { res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: 'User not found' } }); return; } if (user.passwordHash) { res.status(400).json({ success: false, error: { code: 'ALREADY_HAS_PASSWORD', message: 'Password already set. Use change-password instead.' } }); return; } const passwordHash = await hashPassword(parsed.data.password); await prisma.user.update({ where: { id: user.id }, data: { passwordHash } }); res.json({ success: true, data: { message: 'Password set successfully' } }); }); const changePasswordSchema = z.object({ currentPassword: z.string(), newPassword: z.string().min(8), }); router.post('/change-password', requireAuth, async (req, res) => { const parsed = changePasswordSchema.safeParse(req.body); if (!parsed.success) { res.status(400).json({ success: false, error: { code: 'VALIDATION', message: parsed.error.issues[0].message } }); return; } const { currentPassword, newPassword } = parsed.data; const user = await prisma.user.findUnique({ where: { id: req.user!.userId } }); if (!user || !user.passwordHash) { res.status(400).json({ success: false, error: { code: 'NO_PASSWORD', message: 'No password set for this account' } }); return; } const valid = await verifyPassword(currentPassword, user.passwordHash); if (!valid) { res.status(401).json({ success: false, error: { code: 'UNAUTHORIZED', message: 'Current password is incorrect' } }); return; } const newHash = await hashPassword(newPassword); await prisma.user.update({ where: { id: user.id }, data: { passwordHash: newHash } }); res.json({ success: true, data: { message: 'Password changed' } }); }); const profileSchema = z.object({ name: z.string().min(1).max(100), }); router.put('/profile', requireAuth, async (req, res) => { const parsed = profileSchema.safeParse(req.body); if (!parsed.success) { res.status(400).json({ success: false, error: { code: 'VALIDATION', message: parsed.error.issues[0].message } }); return; } const user = await prisma.user.update({ where: { id: req.user!.userId }, data: { name: parsed.data.name }, select: { id: true, email: true, name: true }, }); res.json({ success: true, data: user }); }); router.get('/me', requireAuth, async (req, res) => { const user = await prisma.user.findUnique({ where: { id: req.user!.userId }, select: { id: true, email: true, name: true, avatarUrl: true, passwordHash: true }, }); if (!user) { res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: 'User not found' } }); return; } const { passwordHash, ...rest } = user; res.json({ success: true, data: { ...rest, hasPassword: !!passwordHash } }); }); // --- API Key Management --- router.get('/api-key/status', requireAuth, async (req, res) => { const user = await prisma.user.findUnique({ where: { id: req.user!.userId }, select: { apiKeyPrefix: true, apiKeyHash: true }, }); res.json({ success: true, data: { hasKey: !!user?.apiKeyHash, prefix: user?.apiKeyPrefix || null }, }); }); router.post('/api-key/generate', requireAuth, async (req, res) => { const user = await prisma.user.findUnique({ where: { id: req.user!.userId }, select: { apiKeyHash: true }, }); if (user?.apiKeyHash) { res.status(400).json({ success: false, error: { code: 'ALREADY_EXISTS', message: 'API key already exists. Use rotate to replace it.' } }); return; } const { raw, hash } = generateApiKey(); const encrypted = encryptApiKey(raw); const prefix = raw.slice(0, 12); await prisma.user.update({ where: { id: req.user!.userId }, data: { apiKeyHash: hash, apiKeyEncrypted: encrypted, apiKeyPrefix: prefix }, }); res.json({ success: true, data: { apiKey: raw } }); }); router.post('/api-key/rotate', requireAuth, async (req, res) => { const { raw, hash } = generateApiKey(); const encrypted = encryptApiKey(raw); const prefix = raw.slice(0, 12); await prisma.user.update({ where: { id: req.user!.userId }, data: { apiKeyHash: hash, apiKeyEncrypted: encrypted, apiKeyPrefix: prefix }, }); res.json({ success: true, data: { apiKey: raw } }); }); const revealSchema = z.object({ password: z.string() }); router.post('/api-key/reveal', requireAuth, async (req, res) => { const parsed = revealSchema.safeParse(req.body); if (!parsed.success) { res.status(400).json({ success: false, error: { code: 'VALIDATION', message: 'Password is required' } }); return; } const user = await prisma.user.findUnique({ where: { id: req.user!.userId }, select: { passwordHash: true, apiKeyEncrypted: true }, }); if (!user?.passwordHash) { res.status(400).json({ success: false, error: { code: 'NO_PASSWORD', message: 'No password set' } }); return; } const valid = await verifyPassword(parsed.data.password, user.passwordHash); if (!valid) { res.status(401).json({ success: false, error: { code: 'UNAUTHORIZED', message: 'Incorrect password' } }); return; } if (!user.apiKeyEncrypted) { res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: 'No API key generated' } }); return; } const apiKey = decryptApiKey(user.apiKeyEncrypted); res.json({ success: true, data: { apiKey } }); }); export default router;