- Express + TypeScript backend with IRC connection manager - React + Vite frontend with mIRC-inspired theme - Drizzle ORM + PostgreSQL schema (users, connections, channels, messages) - Redis pub/sub for real-time WebSocket delivery across replicas - IRC color code parser (16 + 99 extended palette) - Kubernetes manifests for homecloud deployment - Multi-stage Dockerfile with non-root user - docker-compose for local development
158 lines
3.7 KiB
TypeScript
158 lines
3.7 KiB
TypeScript
import { Router } from 'express';
|
|
import { z } from 'zod';
|
|
import { db } from '../db/index.js';
|
|
import { users } from '../db/schema.js';
|
|
import { eq } from 'drizzle-orm';
|
|
import {
|
|
hashPassword,
|
|
verifyPassword,
|
|
generateToken,
|
|
authenticateHttp,
|
|
AuthenticatedRequest,
|
|
} from '../auth/index.js';
|
|
|
|
const router = Router();
|
|
|
|
const registerSchema = z.object({
|
|
username: z
|
|
.string()
|
|
.min(2)
|
|
.max(64)
|
|
.regex(/^[a-zA-Z0-9_-]+$/, 'Username must be alphanumeric with dashes/underscores'),
|
|
email: z.string().email().max(255),
|
|
password: z.string().min(8).max(128),
|
|
});
|
|
|
|
const loginSchema = z.object({
|
|
email: z.string().email(),
|
|
password: z.string(),
|
|
});
|
|
|
|
router.post('/register', async (req, res) => {
|
|
try {
|
|
const body = registerSchema.parse(req.body);
|
|
|
|
const existingUser = await db.query.users.findFirst({
|
|
where: eq(users.email, body.email),
|
|
});
|
|
|
|
if (existingUser) {
|
|
res.status(409).json({ error: 'Email already registered' });
|
|
return;
|
|
}
|
|
|
|
const existingUsername = await db.query.users.findFirst({
|
|
where: eq(users.username, body.username),
|
|
});
|
|
|
|
if (existingUsername) {
|
|
res.status(409).json({ error: 'Username already taken' });
|
|
return;
|
|
}
|
|
|
|
const passwordHash = await hashPassword(body.password);
|
|
|
|
const [user] = await db
|
|
.insert(users)
|
|
.values({
|
|
username: body.username,
|
|
email: body.email,
|
|
passwordHash,
|
|
})
|
|
.returning({
|
|
id: users.id,
|
|
username: users.username,
|
|
email: users.email,
|
|
createdAt: users.createdAt,
|
|
});
|
|
|
|
const token = generateToken({ userId: user!.id, username: user!.username });
|
|
|
|
res.status(201).json({
|
|
user: {
|
|
id: user!.id,
|
|
username: user!.username,
|
|
email: user!.email,
|
|
createdAt: user!.createdAt,
|
|
},
|
|
token,
|
|
});
|
|
} catch (err) {
|
|
if (err instanceof z.ZodError) {
|
|
res.status(400).json({ error: 'Validation failed', details: err.errors });
|
|
return;
|
|
}
|
|
console.error('Registration error:', err);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
router.post('/login', async (req, res) => {
|
|
try {
|
|
const body = loginSchema.parse(req.body);
|
|
|
|
const user = await db.query.users.findFirst({
|
|
where: eq(users.email, body.email),
|
|
});
|
|
|
|
if (!user) {
|
|
res.status(401).json({ error: 'Invalid email or password' });
|
|
return;
|
|
}
|
|
|
|
const valid = await verifyPassword(body.password, user.passwordHash);
|
|
if (!valid) {
|
|
res.status(401).json({ error: 'Invalid email or password' });
|
|
return;
|
|
}
|
|
|
|
const token = generateToken({ userId: user.id, username: user.username });
|
|
|
|
res.json({
|
|
user: {
|
|
id: user.id,
|
|
username: user.username,
|
|
email: user.email,
|
|
createdAt: user.createdAt,
|
|
},
|
|
token,
|
|
});
|
|
} catch (err) {
|
|
if (err instanceof z.ZodError) {
|
|
res.status(400).json({ error: 'Validation failed', details: err.errors });
|
|
return;
|
|
}
|
|
console.error('Login error:', err);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
router.get('/me', authenticateHttp, async (req, res) => {
|
|
try {
|
|
const authReq = req as AuthenticatedRequest;
|
|
const userId = authReq.user!.userId;
|
|
|
|
const user = await db.query.users.findFirst({
|
|
where: eq(users.id, userId),
|
|
columns: {
|
|
id: true,
|
|
username: true,
|
|
email: true,
|
|
createdAt: true,
|
|
},
|
|
});
|
|
|
|
if (!user) {
|
|
res.status(404).json({ error: 'User not found' });
|
|
return;
|
|
}
|
|
|
|
res.json({ user });
|
|
} catch (err) {
|
|
console.error('Get user error:', err);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
export default router;
|