import { Hono } from 'hono'; import { randomBytes } from 'crypto'; import { prisma } from '../db'; import { hashPassword, verifyPassword } from '../utils/password'; import { createSession, destroySession, requireAuth } from '../middleware/session'; import { sendPasswordResetEmail, isEmailConfigured } from '../utils/email'; const auth = new Hono(); /** * POST /api/register * Create a new user account */ auth.post('/register', async (c) => { const body = await c.req.json(); const { username, email, password } = body; // Validation if (!username || !email || !password) { return c.json({ error: 'Username, email, and password are required' }, 400); } if (username.length < 3 || username.length > 30) { return c.json({ error: 'Username must be 3-30 characters' }, 400); } if (password.length < 8) { return c.json({ error: 'Password must be at least 8 characters' }, 400); } // Check if username or email already exists const existingUser = await prisma.user.findFirst({ where: { OR: [{ username }, { email }], }, }); if (existingUser) { if (existingUser.username === username) { return c.json({ error: 'Username already taken' }, 400); } return c.json({ error: 'Email already registered' }, 400); } // Create user const passwordHash = await hashPassword(password); const user = await prisma.user.create({ data: { username, email, passwordHash, }, }); // Create session await createSession(c, user.id); return c.json({ username: user.username, email: user.email, role: user.role, }); }); /** * POST /api/login * Authenticate and create session */ auth.post('/login', async (c) => { const body = await c.req.json(); const { username, password } = body; if (!username || !password) { return c.json({ error: 'Username and password are required' }, 400); } // Find user by username or email const user = await prisma.user.findFirst({ where: { OR: [{ username }, { email: username }], }, }); if (!user) { return c.json({ error: 'Invalid username or password' }, 401); } // Verify password const isValid = await verifyPassword(password, user.passwordHash); if (!isValid) { return c.json({ error: 'Invalid username or password' }, 401); } // Create session await createSession(c, user.id); return c.json({ username: user.username, email: user.email, role: user.role, }); }); /** * POST /api/logout * Destroy session */ auth.post('/logout', async (c) => { await destroySession(c); return c.json({ success: true }); }); /** * GET /api/auth/status * Check if user is authenticated */ auth.get('/auth/status', (c) => { const user = c.get('user'); return c.json({ authenticated: !!user, }); }); /** * GET /api/me * Get current user info */ auth.get('/me', requireAuth, (c) => { const user = c.get('user'); return c.json({ username: user!.username, email: user!.email, role: user!.role, }); }); /** * POST /api/forgot-password * Request a password reset */ auth.post('/forgot-password', async (c) => { const body = await c.req.json(); const { email } = body; if (!email) { return c.json({ error: 'Email is required' }, 400); } // Find user by email const user = await prisma.user.findUnique({ where: { email }, }); // Always return success to prevent email enumeration if (!user) { return c.json({ message: 'If an account with that email exists, a reset link has been sent.' }); } // Generate secure token const resetToken = randomBytes(32).toString('hex'); const resetTokenExpiry = new Date(Date.now() + 60 * 60 * 1000); // 1 hour from now // Store token in database await prisma.user.update({ where: { id: user.id }, data: { resetToken, resetTokenExpiry, }, }); // Send email with reset link const baseUrl = process.env.APP_URL || 'https://leagues.tools'; if (isEmailConfigured()) { const emailResult = await sendPasswordResetEmail(email, resetToken, baseUrl); if (!emailResult.success) { console.error(`Failed to send password reset email to ${email}:`, emailResult.error); } } else { // Log token in development when email is not configured console.log(`Password reset requested for ${email}. Token: ${resetToken}`); console.log(`Reset URL: ${baseUrl}/reset-password?token=${resetToken}`); } return c.json({ message: 'If an account with that email exists, a reset link has been sent.' }); }); /** * POST /api/reset-password * Reset password using token */ auth.post('/reset-password', async (c) => { const body = await c.req.json(); const { token, password } = body; if (!token || !password) { return c.json({ error: 'Token and password are required' }, 400); } if (password.length < 8) { return c.json({ error: 'Password must be at least 8 characters' }, 400); } // Find user with this token const user = await prisma.user.findFirst({ where: { resetToken: token, resetTokenExpiry: { gt: new Date(), // Token must not be expired }, }, }); if (!user) { return c.json({ error: 'Invalid or expired reset token' }, 400); } // Update password and clear token const passwordHash = await hashPassword(password); await prisma.user.update({ where: { id: user.id }, data: { passwordHash, resetToken: null, resetTokenExpiry: null, }, }); return c.json({ message: 'Password has been reset successfully. You can now log in.' }); }); export default auth;