import { Hono } from 'hono'; import { eq, or } from 'drizzle-orm'; import bcrypt from 'bcryptjs'; import { db } from '../db'; import { users } from '../db/schema'; import { createToken } from '../lib/jwt'; import { authMiddleware } from '../middleware/auth'; import type { AppEnv } from '../types'; const auth = new Hono(); auth.post('/anonymous', async (c) => { const [user] = await db .insert(users) .values({}) .returning(); const token = await createToken(user.id, null, 'user', null, false); return c.json({ userId: user.id, token }); }); auth.post('/register', authMiddleware, async (c) => { const userId = c.get('userId'); const { email, password, inviteCode } = await c.req.json<{ email: string; password: string; inviteCode: string; }>(); if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { return c.json({ error: 'Valid email required' }, 400); } if (!password || password.length < 8) { return c.json({ error: 'Password must be at least 8 characters' }, 400); } if (process.env.REQUIRE_INVITE !== 'false') { if (!inviteCode) { return c.json({ error: 'Invite code required' }, 400); } const { invitations } = await import('../db/schema'); const { isNull, and, sql } = await import('drizzle-orm'); const [consumed] = await db .update(invitations) .set({ usedBy: userId }) .where( and( eq(invitations.code, inviteCode), isNull(invitations.usedBy), or( isNull(invitations.expiresAt), sql`${invitations.expiresAt} > NOW()`, ), ), ) .returning(); if (!consumed) { return c.json({ error: 'Invalid or used invite code' }, 400); } } const existing = await db .select() .from(users) .where(eq(users.email, email)) .limit(1); if (existing.length > 0) { return c.json({ error: 'Email already in use' }, 409); } const passwordHash = await bcrypt.hash(password, 10); const [updated] = await db .update(users) .set({ email, passwordHash }) .where(eq(users.id, userId)) .returning(); const token = await createToken(updated.id, updated.email, updated.role, updated.username, false); return c.json({ userId: updated.id, token }); }); auth.post('/login', async (c) => { const { login, password } = await c.req.json<{ login: string; password: string }>(); if (!login || !password) { return c.json({ error: 'Login and password required' }, 400); } const [user] = await db .select() .from(users) .where(or(eq(users.email, login), eq(users.username, login))) .limit(1); if (!user || !user.passwordHash) { return c.json({ error: 'Invalid credentials' }, 401); } const valid = await bcrypt.compare(password, user.passwordHash); if (!valid) { return c.json({ error: 'Invalid credentials' }, 401); } const token = await createToken(user.id, user.email, user.role, user.username, user.mustResetPassword); return c.json({ userId: user.id, token }); }); auth.post('/change-password', authMiddleware, async (c) => { const user = c.get('user'); const { currentPassword, newPassword } = await c.req.json<{ currentPassword?: string; newPassword: string; }>(); if (!newPassword || newPassword.length < 8) { return c.json({ error: 'New password must be at least 8 characters' }, 400); } if (!user.mustResetPassword) { if (!currentPassword) { return c.json({ error: 'Current password required' }, 400); } const [dbUser] = await db .select({ passwordHash: users.passwordHash }) .from(users) .where(eq(users.id, user.id)) .limit(1); if (!dbUser?.passwordHash) { return c.json({ error: 'No password set' }, 400); } const valid = await bcrypt.compare(currentPassword, dbUser.passwordHash); if (!valid) { return c.json({ error: 'Current password is incorrect' }, 401); } } const passwordHash = await bcrypt.hash(newPassword, 10); await db .update(users) .set({ passwordHash, mustResetPassword: false }) .where(eq(users.id, user.id)); const token = await createToken(user.id, user.email, user.role, user.username, false); return c.json({ success: true, token }); }); auth.post('/change-username', authMiddleware, async (c) => { const user = c.get('user'); if (user.role !== 'admin') { return c.json({ error: 'Forbidden' }, 403); } const { username } = await c.req.json<{ username: string }>(); if (!username || username.length < 2) { return c.json({ error: 'Username must be at least 2 characters' }, 400); } const existing = await db .select() .from(users) .where(eq(users.username, username)) .limit(1); if (existing.length > 0 && existing[0].id !== user.id) { return c.json({ error: 'Username already taken' }, 409); } await db .update(users) .set({ username }) .where(eq(users.id, user.id)); const token = await createToken(user.id, user.email, user.role, username, user.mustResetPassword); return c.json({ success: true, token }); }); export { auth };