add Keycloak, add better canvas
This commit is contained in:
@@ -11,6 +11,11 @@ interface Config {
|
||||
|
||||
autoExportIntervalSeconds: number;
|
||||
exportPath: string;
|
||||
|
||||
enableKeycloak: boolean;
|
||||
keycloakRealm: string;
|
||||
keycloakAuthUrl: string;
|
||||
keycloakClientId: string;
|
||||
}
|
||||
|
||||
function parseConfigFile(): Config {
|
||||
@@ -54,6 +59,18 @@ function parseConfigFile(): Config {
|
||||
case "EXPORT_PATH":
|
||||
config.exportPath = trimmedValue;
|
||||
break;
|
||||
case "ENABLE_KEYCLOAK":
|
||||
config.enableKeycloak = trimmedValue.toLowerCase() === "true";
|
||||
break;
|
||||
case "KEYCLOAK_REALM":
|
||||
config.keycloakRealm = trimmedValue;
|
||||
break;
|
||||
case "KEYCLOAK_AUTH_URL":
|
||||
config.keycloakAuthUrl = trimmedValue;
|
||||
break;
|
||||
case "KEYCLOAK_CLIENT_ID":
|
||||
config.keycloakClientId = trimmedValue;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -68,6 +85,11 @@ function parseConfigFile(): Config {
|
||||
|
||||
autoExportIntervalSeconds: config.autoExportIntervalSeconds || 60,
|
||||
exportPath: config.exportPath || "./exports/",
|
||||
|
||||
enableKeycloak: config.enableKeycloak || false,
|
||||
keycloakRealm: config.keycloakRealm || "rplace",
|
||||
keycloakAuthUrl: config.keycloakAuthUrl || "http://localhost:8080/auth",
|
||||
keycloakClientId: config.keycloakClientId || "rplace-client",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error reading config file, using defaults:", error);
|
||||
@@ -81,6 +103,11 @@ function parseConfigFile(): Config {
|
||||
|
||||
autoExportIntervalSeconds: 60,
|
||||
exportPath: "./exports/",
|
||||
|
||||
enableKeycloak: false,
|
||||
keycloakRealm: "rplace",
|
||||
keycloakAuthUrl: "http://localhost:8080/auth",
|
||||
keycloakClientId: "rplace-client",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,27 @@
|
||||
import express, { type Request, Response, NextFunction } from "express";
|
||||
import { registerRoutes } from "./routes";
|
||||
import { setupVite, serveStatic, log } from "./vite";
|
||||
import { setupKeycloak } from "./keycloak";
|
||||
import { config } from "./config";
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
|
||||
// Keycloak Setup
|
||||
let keycloak: any = null;
|
||||
if (config.enableKeycloak) {
|
||||
// Set environment variables for Keycloak
|
||||
process.env.KEYCLOAK_REALM = config.keycloakRealm;
|
||||
process.env.KEYCLOAK_AUTH_URL = config.keycloakAuthUrl;
|
||||
process.env.KEYCLOAK_CLIENT_ID = config.keycloakClientId;
|
||||
|
||||
keycloak = setupKeycloak(app);
|
||||
log("Keycloak authentication enabled");
|
||||
} else {
|
||||
log("Keycloak authentication disabled");
|
||||
}
|
||||
|
||||
app.use((req, res, next) => {
|
||||
const start = Date.now();
|
||||
const path = req.path;
|
||||
@@ -60,7 +76,7 @@ app.use((req, res, next) => {
|
||||
// Other ports are firewalled. Default to 5000 if not specified.
|
||||
// this serves both the API and the client.
|
||||
// It is the only port that is not firewalled.
|
||||
const port = parseInt(process.env.PORT || '5000', 10);
|
||||
const port = parseInt(process.env.PORT || '5001', 10);
|
||||
server.listen({
|
||||
port,
|
||||
host: "0.0.0.0",
|
||||
|
||||
56
server/keycloak.ts
Normal file
56
server/keycloak.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
|
||||
import Keycloak from 'keycloak-connect';
|
||||
import session from 'express-session';
|
||||
import { type Express } from 'express';
|
||||
|
||||
interface KeycloakConfig {
|
||||
realm: string;
|
||||
'auth-server-url': string;
|
||||
'ssl-required': string;
|
||||
resource: string;
|
||||
'public-client': boolean;
|
||||
'confidential-port': number;
|
||||
}
|
||||
|
||||
// Keycloak Konfiguration aus Umgebungsvariablen oder Standard
|
||||
const keycloakConfig: KeycloakConfig = {
|
||||
realm: process.env.KEYCLOAK_REALM || 'rplace',
|
||||
'auth-server-url': process.env.KEYCLOAK_AUTH_URL || 'http://localhost:8080/auth',
|
||||
'ssl-required': 'external',
|
||||
resource: process.env.KEYCLOAK_CLIENT_ID || 'rplace-client',
|
||||
'public-client': true,
|
||||
'confidential-port': 0,
|
||||
};
|
||||
|
||||
// Session Store für Keycloak
|
||||
const memoryStore = session.MemoryStore ? new session.MemoryStore() : undefined;
|
||||
|
||||
export function setupKeycloak(app: Express) {
|
||||
// Session Middleware
|
||||
const sessionConfig = {
|
||||
secret: process.env.SESSION_SECRET || 'rplace-secret-key',
|
||||
resave: false,
|
||||
saveUninitialized: true,
|
||||
store: memoryStore,
|
||||
cookie: {
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
httpOnly: true,
|
||||
maxAge: 24 * 60 * 60 * 1000, // 24 Stunden
|
||||
},
|
||||
};
|
||||
|
||||
app.use(session(sessionConfig));
|
||||
|
||||
// Keycloak initialisieren
|
||||
const keycloak = new Keycloak({ store: memoryStore }, keycloakConfig);
|
||||
|
||||
// Keycloak Middleware
|
||||
app.use(keycloak.middleware({
|
||||
logout: '/logout',
|
||||
admin: '/',
|
||||
}));
|
||||
|
||||
return keycloak;
|
||||
}
|
||||
|
||||
export { keycloakConfig };
|
||||
@@ -1,9 +1,42 @@
|
||||
import type { Express } from "express";
|
||||
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);
|
||||
@@ -15,6 +48,38 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
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 {
|
||||
@@ -37,9 +102,14 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
// Config is now read-only from file
|
||||
// Remove the POST endpoint for config updates
|
||||
|
||||
app.post("/api/pixels", async (req, res) => {
|
||||
app.post("/api/pixels", requireAuth, async (req, res) => {
|
||||
try {
|
||||
const pixelData = insertPixelSchema.parse(req.body);
|
||||
const userInfo = getUserFromToken(req);
|
||||
const pixelData = insertPixelSchema.parse({
|
||||
...req.body,
|
||||
userId: userInfo.userId,
|
||||
username: userInfo.username
|
||||
});
|
||||
const config = await storage.getCanvasConfig();
|
||||
|
||||
// Validate coordinates
|
||||
@@ -50,7 +120,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
|
||||
// Check cooldown unless events are enabled
|
||||
if (!config.enableAutomaticEvents) {
|
||||
const cooldown = await storage.getUserCooldown(pixelData.userId);
|
||||
const cooldown = await storage.getUserCooldown(userInfo.userId);
|
||||
if (cooldown && cooldown.cooldownEnds > new Date()) {
|
||||
return res.status(429).json({ message: "Cooldown active" });
|
||||
}
|
||||
@@ -58,7 +128,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
// Set new cooldown
|
||||
const cooldownEnd = new Date(Date.now() + (config.defaultCooldown * 1000));
|
||||
await storage.setUserCooldown({
|
||||
userId: pixelData.userId,
|
||||
userId: userInfo.userId,
|
||||
cooldownEnds: cooldownEnd,
|
||||
});
|
||||
}
|
||||
|
||||
21
server/types/keycloak.d.ts
vendored
Normal file
21
server/types/keycloak.d.ts
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
|
||||
import 'express';
|
||||
|
||||
declare module 'express' {
|
||||
interface Request {
|
||||
kauth?: {
|
||||
grant?: {
|
||||
access_token?: {
|
||||
content?: {
|
||||
sub?: string;
|
||||
preferred_username?: string;
|
||||
name?: string;
|
||||
email?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
login?: (req: Request, res: Response) => void;
|
||||
logout?: (req: Request, res: Response) => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user