116 lines
3.3 KiB
TypeScript
116 lines
3.3 KiB
TypeScript
import { Router, Request, Response } from 'express';
|
|
import bcrypt from 'bcryptjs';
|
|
import jwt from 'jsonwebtoken';
|
|
import { z } from 'zod';
|
|
import prisma from '../lib/prisma.js';
|
|
import { authenticate } from '../middleware/auth.js';
|
|
|
|
const router = Router();
|
|
|
|
const loginSchema = z.object({
|
|
email: z.string().email(),
|
|
password: z.string().min(1),
|
|
});
|
|
|
|
const registerSchema = z.object({
|
|
username: z.string().min(2).max(32),
|
|
email: z.string().email(),
|
|
password: z.string().min(6),
|
|
});
|
|
|
|
function signToken(userId: string, role: string, isAdmin: boolean): string {
|
|
return jwt.sign({ userId, role, isAdmin }, process.env.JWT_SECRET!, { expiresIn: '7d' });
|
|
}
|
|
|
|
function safeUser(user: { id: string; username: string; email: string; role: string; isAdmin: boolean; isBanned: boolean; avatarUrl: string | null; createdAt: Date }) {
|
|
return {
|
|
id: user.id,
|
|
username: user.username,
|
|
email: user.email,
|
|
role: user.role,
|
|
isAdmin: user.isAdmin,
|
|
isBanned: user.isBanned,
|
|
avatarUrl: user.avatarUrl,
|
|
createdAt: user.createdAt.toISOString(),
|
|
};
|
|
}
|
|
|
|
// POST /api/auth/login
|
|
router.post('/login', async (req: Request, res: Response): Promise<void> => {
|
|
const parsed = loginSchema.safeParse(req.body);
|
|
if (!parsed.success) {
|
|
res.status(400).json({ error: parsed.error.flatten() });
|
|
return;
|
|
}
|
|
|
|
const { email, password } = parsed.data;
|
|
|
|
const user = await prisma.user.findUnique({ where: { email: email.toLowerCase() } });
|
|
if (!user) {
|
|
res.status(401).json({ error: 'No account found with that email address.' });
|
|
return;
|
|
}
|
|
|
|
const valid = await bcrypt.compare(password, user.password);
|
|
if (!valid) {
|
|
res.status(401).json({ error: 'Incorrect password.' });
|
|
return;
|
|
}
|
|
|
|
if (user.isBanned) {
|
|
res.status(403).json({ error: 'This account has been suspended.' });
|
|
return;
|
|
}
|
|
|
|
const token = signToken(user.id, user.role, user.isAdmin);
|
|
res.json({ token, user: safeUser(user) });
|
|
});
|
|
|
|
// POST /api/auth/register (public site only — creates role=user)
|
|
router.post('/register', async (req: Request, res: Response): Promise<void> => {
|
|
const parsed = registerSchema.safeParse(req.body);
|
|
if (!parsed.success) {
|
|
res.status(400).json({ error: parsed.error.flatten() });
|
|
return;
|
|
}
|
|
|
|
const { username, email, password } = parsed.data;
|
|
|
|
const emailTaken = await prisma.user.findUnique({ where: { email: email.toLowerCase() } });
|
|
if (emailTaken) {
|
|
res.status(409).json({ error: 'An account with this email already exists.' });
|
|
return;
|
|
}
|
|
|
|
const usernameTaken = await prisma.user.findUnique({ where: { username } });
|
|
if (usernameTaken) {
|
|
res.status(409).json({ error: 'This username is already taken.' });
|
|
return;
|
|
}
|
|
|
|
const hashed = await bcrypt.hash(password, 10);
|
|
const user = await prisma.user.create({
|
|
data: {
|
|
username,
|
|
email: email.toLowerCase(),
|
|
password: hashed,
|
|
role: 'user',
|
|
},
|
|
});
|
|
|
|
const token = signToken(user.id, user.role, user.isAdmin);
|
|
res.status(201).json({ token, user: safeUser(user) });
|
|
});
|
|
|
|
// GET /api/auth/me
|
|
router.get('/me', authenticate, async (req: Request, res: Response): Promise<void> => {
|
|
const user = await prisma.user.findUnique({ where: { id: req.user!.userId } });
|
|
if (!user) {
|
|
res.status(404).json({ error: 'User not found' });
|
|
return;
|
|
}
|
|
res.json(safeUser(user));
|
|
});
|
|
|
|
export default router;
|