feat: initial mIRCcloud.com scaffold

- 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
This commit is contained in:
Jason Preston
2026-08-08 23:50:08 -06:00
commit c59ac7ad0f
52 changed files with 5723 additions and 0 deletions
+157
View File
@@ -0,0 +1,157 @@
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;