135 lines
3.0 KiB
TypeScript
135 lines
3.0 KiB
TypeScript
import { Router } from 'express';
|
|
import { and, eq } from 'drizzle-orm';
|
|
import { z } from 'zod';
|
|
|
|
import { db } from '../db/client';
|
|
import { users } from '../db/schema';
|
|
import { requireAuth } from '../middleware/auth';
|
|
import { signAccessToken } from '../utils/jwt';
|
|
import { hashPassword, verifyPassword } from '../utils/password';
|
|
|
|
const router = Router();
|
|
|
|
const registerSchema = z.object({
|
|
email: z.email().trim().toLowerCase(),
|
|
password: z.string().min(8),
|
|
name: z.string().trim().min(1).max(255),
|
|
});
|
|
|
|
const loginSchema = z.object({
|
|
email: z.email().trim().toLowerCase(),
|
|
password: z.string().min(1),
|
|
});
|
|
|
|
router.post('/register', async (req, res) => {
|
|
const parsed = registerSchema.safeParse(req.body);
|
|
|
|
if (!parsed.success) {
|
|
res.status(400).json({ message: 'Invalid request body', errors: parsed.error.flatten() });
|
|
return;
|
|
}
|
|
|
|
const { email, password, name } = parsed.data;
|
|
|
|
const existingUser = await db.query.users.findFirst({
|
|
where: eq(users.email, email),
|
|
});
|
|
|
|
if (existingUser) {
|
|
res.status(409).json({ message: 'Email already in use' });
|
|
return;
|
|
}
|
|
|
|
const passwordHash = await hashPassword(password);
|
|
|
|
const [newUser] = await db
|
|
.insert(users)
|
|
.values({
|
|
email,
|
|
name,
|
|
passwordHash,
|
|
})
|
|
.returning({
|
|
id: users.id,
|
|
email: users.email,
|
|
name: users.name,
|
|
createdAt: users.createdAt,
|
|
});
|
|
|
|
if (!newUser) {
|
|
res.status(500).json({ message: 'Failed to create user' });
|
|
return;
|
|
}
|
|
|
|
const token = signAccessToken({ userId: newUser.id, email: newUser.email });
|
|
|
|
res.status(201).json({ token, user: newUser });
|
|
});
|
|
|
|
router.post('/login', async (req, res) => {
|
|
const parsed = loginSchema.safeParse(req.body);
|
|
|
|
if (!parsed.success) {
|
|
res.status(400).json({ message: 'Invalid request body', errors: parsed.error.flatten() });
|
|
return;
|
|
}
|
|
|
|
const { email, password } = parsed.data;
|
|
|
|
const user = await db.query.users.findFirst({
|
|
where: eq(users.email, email),
|
|
});
|
|
|
|
if (!user) {
|
|
res.status(401).json({ message: 'Invalid email or password' });
|
|
return;
|
|
}
|
|
|
|
const isPasswordValid = await verifyPassword(password, user.passwordHash);
|
|
|
|
if (!isPasswordValid) {
|
|
res.status(401).json({ message: 'Invalid email or password' });
|
|
return;
|
|
}
|
|
|
|
const token = signAccessToken({ userId: user.id, email: user.email });
|
|
|
|
res.json({
|
|
token,
|
|
user: {
|
|
id: user.id,
|
|
email: user.email,
|
|
name: user.name,
|
|
createdAt: user.createdAt,
|
|
},
|
|
});
|
|
});
|
|
|
|
router.get('/me', requireAuth, async (req, res) => {
|
|
const authenticatedUser = req.user;
|
|
|
|
if (!authenticatedUser) {
|
|
res.status(401).json({ message: 'Unauthorized' });
|
|
return;
|
|
}
|
|
|
|
const user = await db.query.users.findFirst({
|
|
where: and(eq(users.id, authenticatedUser.userId), eq(users.email, authenticatedUser.email)),
|
|
columns: {
|
|
id: true,
|
|
email: true,
|
|
name: true,
|
|
createdAt: true,
|
|
},
|
|
});
|
|
|
|
if (!user) {
|
|
res.status(404).json({ message: 'User not found' });
|
|
return;
|
|
}
|
|
|
|
res.json({ user });
|
|
});
|
|
|
|
export default router;
|