Refactor 4 with bugfixes
This commit is contained in:
22
server/src/routes/health.ts
Normal file
22
server/src/routes/health.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Router } from 'express';
|
||||
import { getAllPublicServers } from '../cqrs';
|
||||
import { connectedUsers } from '../websocket/state';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/health', async (_req, res) => {
|
||||
const servers = await getAllPublicServers();
|
||||
|
||||
res.json({
|
||||
status: 'ok',
|
||||
timestamp: Date.now(),
|
||||
serverCount: servers.length,
|
||||
connectedUsers: connectedUsers.size
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/time', (_req, res) => {
|
||||
res.json({ now: Date.now() });
|
||||
});
|
||||
|
||||
export default router;
|
||||
14
server/src/routes/index.ts
Normal file
14
server/src/routes/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Express } from 'express';
|
||||
import healthRouter from './health';
|
||||
import proxyRouter from './proxy';
|
||||
import usersRouter from './users';
|
||||
import serversRouter from './servers';
|
||||
import joinRequestsRouter from './join-requests';
|
||||
|
||||
export function registerRoutes(app: Express): void {
|
||||
app.use('/api', healthRouter);
|
||||
app.use('/api', proxyRouter);
|
||||
app.use('/api/users', usersRouter);
|
||||
app.use('/api/servers', serversRouter);
|
||||
app.use('/api/requests', joinRequestsRouter);
|
||||
}
|
||||
33
server/src/routes/join-requests.ts
Normal file
33
server/src/routes/join-requests.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Router } from 'express';
|
||||
import { JoinRequestPayload } from '../cqrs/types';
|
||||
import {
|
||||
getJoinRequestById,
|
||||
getServerById,
|
||||
updateJoinRequestStatus
|
||||
} from '../cqrs';
|
||||
import { notifyUser } from '../websocket/broadcast';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.put('/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { ownerId, status } = req.body;
|
||||
const request = await getJoinRequestById(id);
|
||||
|
||||
if (!request)
|
||||
return res.status(404).json({ error: 'Request not found' });
|
||||
|
||||
const server = await getServerById(request.serverId);
|
||||
|
||||
if (!server || server.ownerId !== ownerId)
|
||||
return res.status(403).json({ error: 'Not authorized' });
|
||||
|
||||
await updateJoinRequestStatus(id, status as JoinRequestPayload['status']);
|
||||
|
||||
const updated: JoinRequestPayload = { ...request, status };
|
||||
|
||||
notifyUser(request.userId, { type: 'request_update', request: updated });
|
||||
res.json(updated);
|
||||
});
|
||||
|
||||
export default router;
|
||||
49
server/src/routes/proxy.ts
Normal file
49
server/src/routes/proxy.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Router } from 'express';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/image-proxy', async (req, res) => {
|
||||
try {
|
||||
const url = String(req.query.url || '');
|
||||
|
||||
if (!/^https?:\/\//i.test(url)) {
|
||||
return res.status(400).json({ error: 'Invalid URL' });
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 8000);
|
||||
const response = await fetch(url, { redirect: 'follow', signal: controller.signal });
|
||||
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (!response.ok) {
|
||||
return res.status(response.status).end();
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
|
||||
if (!contentType.toLowerCase().startsWith('image/')) {
|
||||
return res.status(415).json({ error: 'Unsupported content type' });
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const MAX_BYTES = 8 * 1024 * 1024;
|
||||
|
||||
if (arrayBuffer.byteLength > MAX_BYTES) {
|
||||
return res.status(413).json({ error: 'Image too large' });
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', contentType);
|
||||
res.setHeader('Cache-Control', 'public, max-age=3600');
|
||||
res.send(Buffer.from(arrayBuffer));
|
||||
} catch (err) {
|
||||
if ((err as { name?: string })?.name === 'AbortError') {
|
||||
return res.status(504).json({ error: 'Timeout fetching image' });
|
||||
}
|
||||
|
||||
console.error('Image proxy error:', err);
|
||||
res.status(502).json({ error: 'Failed to fetch image' });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
159
server/src/routes/servers.ts
Normal file
159
server/src/routes/servers.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import { Router } from 'express';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { ServerPayload, JoinRequestPayload } from '../cqrs/types';
|
||||
import {
|
||||
getAllPublicServers,
|
||||
getServerById,
|
||||
upsertServer,
|
||||
deleteServer,
|
||||
createJoinRequest,
|
||||
getPendingRequestsForServer
|
||||
} from '../cqrs';
|
||||
import { notifyServerOwner } from '../websocket/broadcast';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/', async (req, res) => {
|
||||
const { q, tags, limit = 20, offset = 0 } = req.query;
|
||||
|
||||
let results = await getAllPublicServers();
|
||||
|
||||
if (q) {
|
||||
const search = String(q).toLowerCase();
|
||||
|
||||
results = results.filter(server =>
|
||||
server.name.toLowerCase().includes(search) ||
|
||||
server.description?.toLowerCase().includes(search)
|
||||
);
|
||||
}
|
||||
|
||||
if (tags) {
|
||||
const tagList = String(tags).split(',');
|
||||
|
||||
results = results.filter(server => tagList.some(tag => server.tags.includes(tag)));
|
||||
}
|
||||
|
||||
const total = results.length;
|
||||
|
||||
results = results.slice(Number(offset), Number(offset) + Number(limit));
|
||||
|
||||
res.json({ servers: results, total, limit: Number(limit), offset: Number(offset) });
|
||||
});
|
||||
|
||||
router.post('/', async (req, res) => {
|
||||
const { id: clientId, name, description, ownerId, ownerPublicKey, isPrivate, maxUsers, tags } = req.body;
|
||||
|
||||
if (!name || !ownerId || !ownerPublicKey)
|
||||
return res.status(400).json({ error: 'Missing required fields' });
|
||||
|
||||
const server: ServerPayload = {
|
||||
id: clientId || uuidv4(),
|
||||
name,
|
||||
description,
|
||||
ownerId,
|
||||
ownerPublicKey,
|
||||
isPrivate: isPrivate ?? false,
|
||||
maxUsers: maxUsers ?? 0,
|
||||
currentUsers: 0,
|
||||
tags: tags ?? [],
|
||||
createdAt: Date.now(),
|
||||
lastSeen: Date.now()
|
||||
};
|
||||
|
||||
await upsertServer(server);
|
||||
res.status(201).json(server);
|
||||
});
|
||||
|
||||
router.put('/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { ownerId, ...updates } = req.body;
|
||||
const existing = await getServerById(id);
|
||||
|
||||
if (!existing)
|
||||
return res.status(404).json({ error: 'Server not found' });
|
||||
|
||||
if (existing.ownerId !== ownerId)
|
||||
return res.status(403).json({ error: 'Not authorized' });
|
||||
|
||||
const server: ServerPayload = { ...existing, ...updates, lastSeen: Date.now() };
|
||||
|
||||
await upsertServer(server);
|
||||
res.json(server);
|
||||
});
|
||||
|
||||
router.post('/:id/heartbeat', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { currentUsers } = req.body;
|
||||
const existing = await getServerById(id);
|
||||
|
||||
if (!existing)
|
||||
return res.status(404).json({ error: 'Server not found' });
|
||||
|
||||
const server: ServerPayload = {
|
||||
...existing,
|
||||
lastSeen: Date.now(),
|
||||
currentUsers: typeof currentUsers === 'number' ? currentUsers : existing.currentUsers
|
||||
};
|
||||
|
||||
await upsertServer(server);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
router.delete('/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { ownerId } = req.body;
|
||||
const existing = await getServerById(id);
|
||||
|
||||
if (!existing)
|
||||
return res.status(404).json({ error: 'Server not found' });
|
||||
|
||||
if (existing.ownerId !== ownerId)
|
||||
return res.status(403).json({ error: 'Not authorized' });
|
||||
|
||||
await deleteServer(id);
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
router.post('/:id/join', async (req, res) => {
|
||||
const { id: serverId } = req.params;
|
||||
const { userId, userPublicKey, displayName } = req.body;
|
||||
const server = await getServerById(serverId);
|
||||
|
||||
if (!server)
|
||||
return res.status(404).json({ error: 'Server not found' });
|
||||
|
||||
const request: JoinRequestPayload = {
|
||||
id: uuidv4(),
|
||||
serverId,
|
||||
userId,
|
||||
userPublicKey,
|
||||
displayName,
|
||||
status: server.isPrivate ? 'pending' : 'approved',
|
||||
createdAt: Date.now()
|
||||
};
|
||||
|
||||
await createJoinRequest(request);
|
||||
|
||||
if (server.isPrivate)
|
||||
notifyServerOwner(server.ownerId, { type: 'join_request', request });
|
||||
|
||||
res.status(201).json(request);
|
||||
});
|
||||
|
||||
router.get('/:id/requests', async (req, res) => {
|
||||
const { id: serverId } = req.params;
|
||||
const { ownerId } = req.query;
|
||||
const server = await getServerById(serverId);
|
||||
|
||||
if (!server)
|
||||
return res.status(404).json({ error: 'Server not found' });
|
||||
|
||||
if (server.ownerId !== ownerId)
|
||||
return res.status(403).json({ error: 'Not authorized' });
|
||||
|
||||
const requests = await getPendingRequestsForServer(serverId);
|
||||
|
||||
res.json({ requests });
|
||||
});
|
||||
|
||||
export default router;
|
||||
46
server/src/routes/users.ts
Normal file
46
server/src/routes/users.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import crypto from 'crypto';
|
||||
import { Router } from 'express';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { getUserByUsername, registerUser } from '../cqrs';
|
||||
|
||||
const router = Router();
|
||||
|
||||
function hashPassword(pw: string): string {
|
||||
return crypto.createHash('sha256').update(pw)
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
router.post('/register', async (req, res) => {
|
||||
const { username, password, displayName } = req.body;
|
||||
|
||||
if (!username || !password)
|
||||
return res.status(400).json({ error: 'Missing username/password' });
|
||||
|
||||
const existing = await getUserByUsername(username);
|
||||
|
||||
if (existing)
|
||||
return res.status(409).json({ error: 'Username taken' });
|
||||
|
||||
const user = {
|
||||
id: uuidv4(),
|
||||
username,
|
||||
passwordHash: hashPassword(password),
|
||||
displayName: displayName || username,
|
||||
createdAt: Date.now()
|
||||
};
|
||||
|
||||
await registerUser(user);
|
||||
res.status(201).json({ id: user.id, username: user.username, displayName: user.displayName });
|
||||
});
|
||||
|
||||
router.post('/login', async (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
const user = await getUserByUsername(username);
|
||||
|
||||
if (!user || user.passwordHash !== hashPassword(password))
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
|
||||
res.json({ id: user.id, username: user.username, displayName: user.displayName });
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user