- 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
214 lines
6.0 KiB
TypeScript
214 lines
6.0 KiB
TypeScript
import { Router } from 'express';
|
|
import { z } from 'zod';
|
|
import { db } from '../db/index.js';
|
|
import { connections } from '../db/schema.js';
|
|
import { eq, and } from 'drizzle-orm';
|
|
import { authenticateHttp, AuthenticatedRequest } from '../auth/index.js';
|
|
import { connectionManager } from '../irc/connection-manager.js';
|
|
|
|
const router = Router();
|
|
|
|
router.use(authenticateHttp);
|
|
|
|
const createConnectionSchema = z.object({
|
|
name: z.string().min(1).max(128),
|
|
hostname: z.string().min(1).max(255),
|
|
port: z.number().int().min(1).max(65535).default(6667),
|
|
tls: z.boolean().default(false),
|
|
nick: z.string().min(1).max(64),
|
|
username: z.string().max(64).nullable().optional(),
|
|
realname: z.string().max(255).nullable().optional(),
|
|
password: z.string().nullable().optional(),
|
|
saslUsername: z.string().max(128).nullable().optional(),
|
|
saslPassword: z.string().nullable().optional(),
|
|
autoConnect: z.boolean().default(false),
|
|
});
|
|
|
|
const updateConnectionSchema = createConnectionSchema.partial();
|
|
|
|
router.get('/', async (req, res) => {
|
|
try {
|
|
const authReq = req as AuthenticatedRequest;
|
|
const userId = authReq.user!.userId;
|
|
|
|
const userConnections = await db.query.connections.findMany({
|
|
where: eq(connections.userId, userId),
|
|
with: { channels: true },
|
|
});
|
|
|
|
res.json({ connections: userConnections });
|
|
} catch (err) {
|
|
console.error('List connections error:', err);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
router.post('/', async (req, res) => {
|
|
try {
|
|
const authReq = req as AuthenticatedRequest;
|
|
const userId = authReq.user!.userId;
|
|
const body = createConnectionSchema.parse(req.body);
|
|
|
|
const [conn] = await db
|
|
.insert(connections)
|
|
.values({
|
|
userId,
|
|
name: body.name,
|
|
hostname: body.hostname,
|
|
port: body.port,
|
|
tls: body.tls,
|
|
nick: body.nick,
|
|
username: body.username || null,
|
|
realname: body.realname || null,
|
|
password: body.password || null,
|
|
saslUsername: body.saslUsername || null,
|
|
saslPassword: body.saslPassword || null,
|
|
autoConnect: body.autoConnect,
|
|
})
|
|
.returning();
|
|
|
|
res.status(201).json({ connection: conn });
|
|
} catch (err) {
|
|
if (err instanceof z.ZodError) {
|
|
res.status(400).json({ error: 'Validation failed', details: err.errors });
|
|
return;
|
|
}
|
|
console.error('Create connection error:', err);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
router.put('/:id', async (req, res) => {
|
|
try {
|
|
const authReq = req as AuthenticatedRequest;
|
|
const userId = authReq.user!.userId;
|
|
const connectionId = parseInt(req.params.id!, 10);
|
|
const body = updateConnectionSchema.parse(req.body);
|
|
|
|
const existing = await db.query.connections.findFirst({
|
|
where: and(
|
|
eq(connections.id, connectionId),
|
|
eq(connections.userId, userId)
|
|
),
|
|
});
|
|
|
|
if (!existing) {
|
|
res.status(404).json({ error: 'Connection not found' });
|
|
return;
|
|
}
|
|
|
|
const [updated] = await db
|
|
.update(connections)
|
|
.set({
|
|
...body,
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(eq(connections.id, connectionId))
|
|
.returning();
|
|
|
|
res.json({ connection: updated });
|
|
} catch (err) {
|
|
if (err instanceof z.ZodError) {
|
|
res.status(400).json({ error: 'Validation failed', details: err.errors });
|
|
return;
|
|
}
|
|
console.error('Update connection error:', err);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
router.delete('/:id', async (req, res) => {
|
|
try {
|
|
const authReq = req as AuthenticatedRequest;
|
|
const userId = authReq.user!.userId;
|
|
const connectionId = parseInt(req.params.id!, 10);
|
|
|
|
const existing = await db.query.connections.findFirst({
|
|
where: and(
|
|
eq(connections.id, connectionId),
|
|
eq(connections.userId, userId)
|
|
),
|
|
});
|
|
|
|
if (!existing) {
|
|
res.status(404).json({ error: 'Connection not found' });
|
|
return;
|
|
}
|
|
|
|
await connectionManager.disconnect(userId, connectionId);
|
|
await db.delete(connections).where(eq(connections.id, connectionId));
|
|
|
|
res.status(204).send();
|
|
} catch (err) {
|
|
console.error('Delete connection error:', err);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
router.post('/:id/connect', async (req, res) => {
|
|
try {
|
|
const authReq = req as AuthenticatedRequest;
|
|
const userId = authReq.user!.userId;
|
|
const connectionId = parseInt(req.params.id!, 10);
|
|
|
|
const conn = await db.query.connections.findFirst({
|
|
where: and(
|
|
eq(connections.id, connectionId),
|
|
eq(connections.userId, userId)
|
|
),
|
|
});
|
|
|
|
if (!conn) {
|
|
res.status(404).json({ error: 'Connection not found' });
|
|
return;
|
|
}
|
|
|
|
await connectionManager.connect(userId, {
|
|
id: conn.id,
|
|
hostname: conn.hostname,
|
|
port: conn.port,
|
|
tls: conn.tls,
|
|
nick: conn.nick,
|
|
username: conn.username,
|
|
realname: conn.realname,
|
|
password: conn.password,
|
|
saslUsername: conn.saslUsername,
|
|
saslPassword: conn.saslPassword,
|
|
});
|
|
|
|
res.json({ status: 'connecting', connectionId });
|
|
} catch (err) {
|
|
console.error('Connect error:', err);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
router.post('/:id/disconnect', async (req, res) => {
|
|
try {
|
|
const authReq = req as AuthenticatedRequest;
|
|
const userId = authReq.user!.userId;
|
|
const connectionId = parseInt(req.params.id!, 10);
|
|
|
|
const conn = await db.query.connections.findFirst({
|
|
where: and(
|
|
eq(connections.id, connectionId),
|
|
eq(connections.userId, userId)
|
|
),
|
|
});
|
|
|
|
if (!conn) {
|
|
res.status(404).json({ error: 'Connection not found' });
|
|
return;
|
|
}
|
|
|
|
await connectionManager.disconnect(userId, connectionId);
|
|
|
|
res.json({ status: 'disconnected', connectionId });
|
|
} catch (err) {
|
|
console.error('Disconnect error:', err);
|
|
res.status(500).json({ error: 'Internal server error' });
|
|
}
|
|
});
|
|
|
|
export default router;
|