place_maxlan/server/routes.ts

240 lines
6.7 KiB
TypeScript

import type { Express, Request, Response, NextFunction } from "express";
import { createServer, type Server } from "http";
import { WebSocketServer, WebSocket } from "ws";
import { storage } from "./storage";
import { insertPixelSchema, insertUserCooldownSchema, type WSMessage } from "@shared/schema";
import { CanvasExporter } from "./export";
import { config } from "./config";
// Authentication middleware
function requireAuth(req: Request, res: Response, next: NextFunction) {
if (!config.enableKeycloak) {
return next();
}
// Check if user is authenticated via Keycloak
if (req.kauth && req.kauth.grant) {
return next();
}
return res.status(401).json({ message: "Authentication required" });
}
// Get user info from Keycloak token
function getUserFromToken(req: Request): { userId: string; username: string } {
if (!config.enableKeycloak || !req.kauth?.grant?.access_token) {
return {
userId: "User",
username: "Anonymous"
};
}
const token = req.kauth.grant.access_token;
const content = token.content;
return {
userId: content.sub || content.preferred_username || "User",
username: content.preferred_username || content.name || "User"
};
}
export async function registerRoutes(app: Express): Promise<Server> {
const httpServer = createServer(app);
const wss = new WebSocketServer({ server: httpServer, path: '/ws' });
let connectedUsers = new Set<WebSocket>();
// Initialize canvas exporter
const exporter = new CanvasExporter(storage);
exporter.startAutoExport();
// Authentication Routes
app.get("/api/auth/status", (req, res) => {
if (!config.enableKeycloak) {
return res.json({ authenticated: false, keycloakEnabled: false });
}
const isAuthenticated = req.kauth && req.kauth.grant;
const user = isAuthenticated ? getUserFromToken(req) : null;
res.json({
authenticated: isAuthenticated,
keycloakEnabled: true,
user: user
});
});
// Login redirect
app.get("/login", (req, res) => {
if (config.enableKeycloak && req.kauth) {
return req.kauth.login(req, res);
}
res.redirect("/");
});
// Logout
app.get("/logout", (req, res) => {
if (config.enableKeycloak && req.kauth) {
return req.kauth.logout(req, res);
}
res.redirect("/");
});
// API Routes
app.get("/api/pixels", async (req, res) => {
try {
const pixels = await storage.getAllPixels();
res.json(pixels);
} catch (error) {
res.status(500).json({ message: "Failed to fetch pixels" });
}
});
app.get("/api/config", async (req, res) => {
try {
const config = await storage.getCanvasConfig();
res.json(config);
} catch (error) {
res.status(500).json({ message: "Failed to fetch config" });
}
});
// Config is now read-only from file
// Remove the POST endpoint for config updates
app.post("/api/pixels", requireAuth, async (req, res) => {
try {
const userInfo = getUserFromToken(req);
const pixelData = insertPixelSchema.parse({
...req.body,
userId: userInfo.userId,
username: userInfo.username
});
const config = await storage.getCanvasConfig();
// Validate coordinates
if (pixelData.x < 0 || pixelData.x >= config.canvasWidth ||
pixelData.y < 0 || pixelData.y >= config.canvasHeight) {
return res.status(400).json({ message: "Pixel coordinates out of bounds" });
}
// Check cooldown unless events are enabled
if (!config.enableAutomaticEvents) {
const cooldown = await storage.getUserCooldown(userInfo.userId);
if (cooldown && cooldown.cooldownEnds > new Date()) {
return res.status(429).json({ message: "Cooldown active" });
}
// Set new cooldown
const cooldownEnd = new Date(Date.now() + (config.defaultCooldown * 1000));
await storage.setUserCooldown({
userId: userInfo.userId,
cooldownEnds: cooldownEnd,
});
}
const pixel = await storage.placePixel(pixelData);
// Broadcast pixel placement to all connected clients
broadcast({
type: "pixel_placed",
data: {
x: pixel.x,
y: pixel.y,
color: pixel.color,
userId: pixel.userId,
username: pixel.username,
timestamp: pixel.createdAt.toISOString(),
},
});
res.json(pixel);
} catch (error) {
res.status(400).json({ message: "Invalid pixel data" });
}
});
app.get("/api/recent", async (req, res) => {
try {
const limit = parseInt(req.query.limit as string) || 10;
const recent = await storage.getRecentPlacements(limit);
res.json(recent);
} catch (error) {
res.status(500).json({ message: "Failed to fetch recent placements" });
}
});
app.get("/api/cooldown/:userId", async (req, res) => {
try {
const { userId } = req.params;
const cooldown = await storage.getUserCooldown(userId);
const config = await storage.getCanvasConfig();
if (!cooldown || config.enableAutomaticEvents) {
return res.json({ remainingSeconds: 0 });
}
const remaining = Math.max(0, Math.ceil((cooldown.cooldownEnds.getTime() - Date.now()) / 1000));
res.json({ remainingSeconds: remaining });
} catch (error) {
res.status(500).json({ message: "Failed to fetch cooldown" });
}
});
// WebSocket handling
function broadcast(message: WSMessage) {
const messageStr = JSON.stringify(message);
connectedUsers.forEach(ws => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(messageStr);
}
});
}
function broadcastUserCount() {
broadcast({
type: "user_count",
data: { count: connectedUsers.size },
});
}
wss.on('connection', (ws, req) => {
console.log(`New WebSocket connection from ${req.socket.remoteAddress}`);
connectedUsers.add(ws);
broadcastUserCount();
// Send current user count immediately
ws.send(JSON.stringify({
type: "user_count",
data: { count: connectedUsers.size },
}));
ws.on('close', (code, reason) => {
console.log(`WebSocket disconnected: ${code} ${reason}`);
connectedUsers.delete(ws);
broadcastUserCount();
});
ws.on('error', (error) => {
console.error('WebSocket error:', error);
connectedUsers.delete(ws);
broadcastUserCount();
});
ws.on('pong', () => {
// Keep connection alive
});
});
// Keep connections alive with ping/pong
const pingInterval = setInterval(() => {
connectedUsers.forEach(ws => {
if (ws.readyState === WebSocket.OPEN) {
ws.ping();
}
});
}, 30000);
return httpServer;
}