add Keycloak, add better canvas
This commit is contained in:
parent
126b2f96f8
commit
d5f8de1e4c
@ -51,6 +51,12 @@ EXPORT_PATH=./exports/ # Speicherort für SVG-Exports
|
|||||||
ENABLE_AUTOMATIC_EVENTS=false # Automatische Events deaktiviert
|
ENABLE_AUTOMATIC_EVENTS=false # Automatische Events deaktiviert
|
||||||
EVENT_DURATION_MINUTES=30 # Event-Dauer
|
EVENT_DURATION_MINUTES=30 # Event-Dauer
|
||||||
EVENT_INTERVAL_HOURS=6 # Abstand zwischen Events
|
EVENT_INTERVAL_HOURS=6 # Abstand zwischen Events
|
||||||
|
|
||||||
|
# Keycloak Authentifizierung (optional)
|
||||||
|
ENABLE_KEYCLOAK=false # Keycloak-Authentifizierung
|
||||||
|
KEYCLOAK_REALM=rplace # Keycloak Realm Name
|
||||||
|
KEYCLOAK_AUTH_URL=http://localhost:8080 # Keycloak Server URL
|
||||||
|
KEYCLOAK_CLIENT_ID=rplace-client # Keycloak Client ID Events
|
||||||
```
|
```
|
||||||
|
|
||||||
## Schritt 4: PostgreSQL Datenbank einrichten (optional)
|
## Schritt 4: PostgreSQL Datenbank einrichten (optional)
|
||||||
|
|||||||
96
KEYCLOAK_SETUP.md
Normal file
96
KEYCLOAK_SETUP.md
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
|
||||||
|
# Keycloak Setup für r/place
|
||||||
|
|
||||||
|
## Keycloak Installation
|
||||||
|
|
||||||
|
### Docker (Empfohlen)
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name keycloak \
|
||||||
|
-p 8080:8080 \
|
||||||
|
-e KEYCLOAK_ADMIN=admin \
|
||||||
|
-e KEYCLOAK_ADMIN_PASSWORD=admin \
|
||||||
|
quay.io/keycloak/keycloak:latest \
|
||||||
|
start-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Standalone Installation
|
||||||
|
1. Lade Keycloak von https://www.keycloak.org/downloads herunter
|
||||||
|
2. Entpacke das Archiv
|
||||||
|
3. Starte Keycloak: `bin/kc.sh start-dev`
|
||||||
|
|
||||||
|
## Konfiguration
|
||||||
|
|
||||||
|
### 1. Admin Console öffnen
|
||||||
|
Gehe zu http://localhost:8080/admin und melde dich mit admin/admin an.
|
||||||
|
|
||||||
|
### 2. Realm erstellen
|
||||||
|
1. Klicke auf "Create Realm"
|
||||||
|
2. Name: `rplace`
|
||||||
|
3. Klicke "Create"
|
||||||
|
|
||||||
|
### 3. Client erstellen
|
||||||
|
1. Gehe zu "Clients" → "Create client"
|
||||||
|
2. Client ID: `rplace-client`
|
||||||
|
3. Client type: `OpenID Connect`
|
||||||
|
4. Klicke "Next"
|
||||||
|
5. Client authentication: `OFF` (Public client)
|
||||||
|
6. Standard flow: `ON`
|
||||||
|
7. Direct access grants: `ON`
|
||||||
|
8. Klicke "Save"
|
||||||
|
|
||||||
|
### 4. Client Settings
|
||||||
|
1. Gehe zu deinem Client `rplace-client`
|
||||||
|
2. Settings Tab:
|
||||||
|
- Valid redirect URIs: `http://localhost:5000/*`
|
||||||
|
- Valid post logout redirect URIs: `http://localhost:5000/*`
|
||||||
|
- Web origins: `http://localhost:5000`
|
||||||
|
3. Klicke "Save"
|
||||||
|
|
||||||
|
### 5. Test User erstellen
|
||||||
|
1. Gehe zu "Users" → "Add user"
|
||||||
|
2. Username: `testuser`
|
||||||
|
3. Klicke "Create"
|
||||||
|
4. Gehe zum "Credentials" Tab
|
||||||
|
5. Setze ein Passwort und deaktiviere "Temporary"
|
||||||
|
|
||||||
|
## r/place Konfiguration
|
||||||
|
|
||||||
|
Bearbeite `config.cfg`:
|
||||||
|
```ini
|
||||||
|
ENABLE_KEYCLOAK=true
|
||||||
|
KEYCLOAK_REALM=rplace
|
||||||
|
KEYCLOAK_AUTH_URL=http://localhost:8080
|
||||||
|
KEYCLOAK_CLIENT_ID=rplace-client
|
||||||
|
```
|
||||||
|
|
||||||
|
## Erweiterte Konfiguration
|
||||||
|
|
||||||
|
### HTTPS (Produktion)
|
||||||
|
Für Produktionsumgebungen:
|
||||||
|
```ini
|
||||||
|
KEYCLOAK_AUTH_URL=https://dein-keycloak-server.de
|
||||||
|
```
|
||||||
|
|
||||||
|
### Benutzer-Attribute
|
||||||
|
Du kannst zusätzliche Benutzerattribute in Keycloak konfigurieren:
|
||||||
|
1. Gehe zu "Client scopes"
|
||||||
|
2. Bearbeite "profile" scope
|
||||||
|
3. Füge Mappers für zusätzliche Attribute hinzu
|
||||||
|
|
||||||
|
### Sicherheit
|
||||||
|
- Ändere Admin-Passwort
|
||||||
|
- Konfiguriere SSL/TLS
|
||||||
|
- Setze starke Passwort-Richtlinien
|
||||||
|
- Aktiviere Brute-Force-Schutz
|
||||||
|
|
||||||
|
## Fehlerbehebung
|
||||||
|
|
||||||
|
### CORS-Probleme
|
||||||
|
Stelle sicher, dass die Web origins korrekt konfiguriert sind.
|
||||||
|
|
||||||
|
### Token-Probleme
|
||||||
|
Überprüfe die Client-Konfiguration und Redirect-URIs.
|
||||||
|
|
||||||
|
### Verbindungsprobleme
|
||||||
|
Stelle sicher, dass Keycloak erreichbar ist und die URLs korrekt sind.
|
||||||
58
client/src/components/auth-banner.tsx
Normal file
58
client/src/components/auth-banner.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { getAuthStatus, type AuthStatus } from "@/lib/config";
|
||||||
|
|
||||||
|
export function AuthBanner() {
|
||||||
|
const [authStatus, setAuthStatus] = useState<AuthStatus | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getAuthStatus().then(setAuthStatus);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!authStatus?.keycloakEnabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authStatus.authenticated) {
|
||||||
|
return (
|
||||||
|
<div className="bg-green-50 border-b border-green-200 p-3">
|
||||||
|
<div className="flex items-center justify-between max-w-7xl mx-auto">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-green-700 font-medium">
|
||||||
|
Angemeldet als {authStatus.user?.username}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => window.location.href = "/logout"}
|
||||||
|
className="border-green-300 text-green-700 hover:bg-green-100"
|
||||||
|
>
|
||||||
|
Abmelden
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-blue-50 border-b border-blue-200 p-3">
|
||||||
|
<div className="flex items-center justify-between max-w-7xl mx-auto">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-blue-700">
|
||||||
|
Melde dich an, um Pixel zu platzieren
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={() => window.location.href = "/login"}
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 text-white"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Anmelden
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -131,31 +131,49 @@
|
|||||||
100% { background-position: 20px 20px; }
|
100% { background-position: 20px 20px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Smooth scrolling für den gesamten Container */
|
/* Canvas container optimiert für glattes Zoomen */
|
||||||
.scroll-smooth {
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Canvas zoom transitions */
|
|
||||||
.canvas-zoom {
|
|
||||||
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Geschmeidiges Mausrad-Scrolling */
|
|
||||||
.canvas-container {
|
.canvas-container {
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: auto; /* Für präzises Zoom-Verhalten */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Optimierte Pixel-Hover-Effekte */
|
.canvas-container:not(:hover) {
|
||||||
.pixel {
|
scroll-behavior: smooth; /* Smooth nur wenn nicht gehovered */
|
||||||
transition: transform 0.15s ease-out, box-shadow 0.15s ease-out, opacity 0.1s ease-out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pixel:hover {
|
/* Zoom Controls */
|
||||||
transform: scale(1.1);
|
.canvas-container * {
|
||||||
z-index: 10;
|
image-rendering: pixelated;
|
||||||
position: relative;
|
image-rendering: -moz-crisp-edges;
|
||||||
box-shadow: 0 0 8px rgba(255, 255, 255, 0.3);
|
image-rendering: crisp-edges;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optimierte Performance für große Canvas */
|
||||||
|
.canvas-container canvas {
|
||||||
|
will-change: transform;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smooth transitions für UI Elemente */
|
||||||
|
.zoom-controls {
|
||||||
|
transition: opacity 0.2s ease, transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pixel-perfekte Rendering */
|
||||||
|
canvas {
|
||||||
|
image-rendering: pixelated;
|
||||||
|
image-rendering: -moz-crisp-edges;
|
||||||
|
image-rendering: crisp-edges;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Verbesserte Hover-Effekte */
|
||||||
|
.canvas-container:hover {
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Info Panel Styling */
|
||||||
|
.info-panel {
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Pixel-Vorschau */
|
/* Pixel-Vorschau */
|
||||||
|
|||||||
@ -29,7 +29,7 @@ export const COLORS = [
|
|||||||
"#ffb470", // Beige
|
"#ffb470", // Beige
|
||||||
"#000000", // Black
|
"#000000", // Black
|
||||||
"#515252", // Dark Gray
|
"#515252", // Dark Gray
|
||||||
"#898d90", // Gray
|
"#898989", // Gray
|
||||||
"#d4d7d9", // Light Gray
|
"#d4d7d9", // Light Gray
|
||||||
"#ffffff", // White
|
"#ffffff", // White
|
||||||
] as const;
|
] as const;
|
||||||
@ -48,3 +48,24 @@ export function generateUserId(): string {
|
|||||||
export function getUsername(): string {
|
export function getUsername(): string {
|
||||||
return generateUserId();
|
return generateUserId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const API_BASE = "/api";
|
||||||
|
|
||||||
|
export interface AuthStatus {
|
||||||
|
authenticated: boolean;
|
||||||
|
keycloakEnabled: boolean;
|
||||||
|
user?: {
|
||||||
|
userId: string;
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAuthStatus(): Promise<AuthStatus> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE}/auth/status`);
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to get auth status:", error);
|
||||||
|
return { authenticated: false, keycloakEnabled: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,6 +11,7 @@ import { useToast } from "@/hooks/use-toast";
|
|||||||
import { DEFAULT_SELECTED_COLOR, generateUserId, getUsername } from "@/lib/config";
|
import { DEFAULT_SELECTED_COLOR, generateUserId, getUsername } from "@/lib/config";
|
||||||
import { Pixel, CanvasConfig, InsertPixel, WSMessage } from "@shared/schema";
|
import { Pixel, CanvasConfig, InsertPixel, WSMessage } from "@shared/schema";
|
||||||
import { apiRequest } from "@/lib/queryClient";
|
import { apiRequest } from "@/lib/queryClient";
|
||||||
|
import { AuthBanner } from "@/components/auth-banner";
|
||||||
|
|
||||||
export default function CanvasPage() {
|
export default function CanvasPage() {
|
||||||
const [selectedColor, setSelectedColor] = useState(DEFAULT_SELECTED_COLOR);
|
const [selectedColor, setSelectedColor] = useState(DEFAULT_SELECTED_COLOR);
|
||||||
@ -146,6 +147,7 @@ export default function CanvasPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen flex flex-col bg-canvas-bg text-white">
|
<div className="h-screen flex flex-col bg-canvas-bg text-white">
|
||||||
|
<AuthBanner />
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className="bg-panel-bg border-b border-gray-700 px-4 py-3 flex items-center justify-between">
|
<header className="bg-panel-bg border-b border-gray-700 px-4 py-3 flex items-center justify-between">
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
|
|||||||
16
config.cfg
16
config.cfg
@ -2,11 +2,11 @@
|
|||||||
# Ändere diese Werte um die Canvas-Einstellungen anzupassen
|
# Ändere diese Werte um die Canvas-Einstellungen anzupassen
|
||||||
|
|
||||||
# Canvas Dimensionen
|
# Canvas Dimensionen
|
||||||
CANVAS_WIDTH=100
|
CANVAS_WIDTH=500
|
||||||
CANVAS_HEIGHT=100
|
CANVAS_HEIGHT=200
|
||||||
|
|
||||||
# Cooldown Einstellungen (in Sekunden)
|
# Cooldown Einstellungen (in Sekunden)
|
||||||
DEFAULT_COOLDOWN=5
|
DEFAULT_COOLDOWN=10
|
||||||
|
|
||||||
# Automatische Events (true/false)
|
# Automatische Events (true/false)
|
||||||
# Wenn aktiviert, gibt es keine Cooldowns
|
# Wenn aktiviert, gibt es keine Cooldowns
|
||||||
@ -14,10 +14,16 @@ ENABLE_AUTOMATIC_EVENTS=false
|
|||||||
|
|
||||||
# Event Einstellungen
|
# Event Einstellungen
|
||||||
EVENT_DURATION_MINUTES=30
|
EVENT_DURATION_MINUTES=30
|
||||||
EVENT_INTERVAL_HOURS=6
|
EVENT_INTERVAL_HOURS=1
|
||||||
|
|
||||||
|
|
||||||
# Grid-Funktionalität wurde entfernt
|
|
||||||
|
|
||||||
# Export Einstellungen
|
# Export Einstellungen
|
||||||
AUTO_EXPORT_INTERVAL_SECONDS=60
|
AUTO_EXPORT_INTERVAL_SECONDS=60
|
||||||
EXPORT_PATH=./exports/
|
EXPORT_PATH=./exports/
|
||||||
|
|
||||||
|
# Keycloak Einstellungen
|
||||||
|
ENABLE_KEYCLOAK=false
|
||||||
|
KEYCLOAK_REALM=rplace
|
||||||
|
KEYCLOAK_AUTH_URL=http://localhost:8080/auth
|
||||||
|
KEYCLOAK_CLIENT_ID=rplace-client
|
||||||
861
package-lock.json
generated
861
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -51,9 +51,10 @@
|
|||||||
"drizzle-zod": "^0.7.0",
|
"drizzle-zod": "^0.7.0",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"express-session": "^1.18.1",
|
"express-session": "^1.18.2",
|
||||||
"framer-motion": "^11.13.1",
|
"framer-motion": "^11.13.1",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
|
"keycloak-connect": "^26.1.1",
|
||||||
"lucide-react": "^0.453.0",
|
"lucide-react": "^0.453.0",
|
||||||
"memorystore": "^1.6.7",
|
"memorystore": "^1.6.7",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
|
|||||||
@ -11,6 +11,11 @@ interface Config {
|
|||||||
|
|
||||||
autoExportIntervalSeconds: number;
|
autoExportIntervalSeconds: number;
|
||||||
exportPath: string;
|
exportPath: string;
|
||||||
|
|
||||||
|
enableKeycloak: boolean;
|
||||||
|
keycloakRealm: string;
|
||||||
|
keycloakAuthUrl: string;
|
||||||
|
keycloakClientId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseConfigFile(): Config {
|
function parseConfigFile(): Config {
|
||||||
@ -54,6 +59,18 @@ function parseConfigFile(): Config {
|
|||||||
case "EXPORT_PATH":
|
case "EXPORT_PATH":
|
||||||
config.exportPath = trimmedValue;
|
config.exportPath = trimmedValue;
|
||||||
break;
|
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,
|
autoExportIntervalSeconds: config.autoExportIntervalSeconds || 60,
|
||||||
exportPath: config.exportPath || "./exports/",
|
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) {
|
} catch (error) {
|
||||||
console.error("Error reading config file, using defaults:", error);
|
console.error("Error reading config file, using defaults:", error);
|
||||||
@ -81,6 +103,11 @@ function parseConfigFile(): Config {
|
|||||||
|
|
||||||
autoExportIntervalSeconds: 60,
|
autoExportIntervalSeconds: 60,
|
||||||
exportPath: "./exports/",
|
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 express, { type Request, Response, NextFunction } from "express";
|
||||||
import { registerRoutes } from "./routes";
|
import { registerRoutes } from "./routes";
|
||||||
import { setupVite, serveStatic, log } from "./vite";
|
import { setupVite, serveStatic, log } from "./vite";
|
||||||
|
import { setupKeycloak } from "./keycloak";
|
||||||
|
import { config } from "./config";
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.urlencoded({ extended: false }));
|
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) => {
|
app.use((req, res, next) => {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
const path = req.path;
|
const path = req.path;
|
||||||
@ -60,7 +76,7 @@ app.use((req, res, next) => {
|
|||||||
// Other ports are firewalled. Default to 5000 if not specified.
|
// Other ports are firewalled. Default to 5000 if not specified.
|
||||||
// this serves both the API and the client.
|
// this serves both the API and the client.
|
||||||
// It is the only port that is not firewalled.
|
// 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({
|
server.listen({
|
||||||
port,
|
port,
|
||||||
host: "0.0.0.0",
|
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 { createServer, type Server } from "http";
|
||||||
import { WebSocketServer, WebSocket } from "ws";
|
import { WebSocketServer, WebSocket } from "ws";
|
||||||
import { storage } from "./storage";
|
import { storage } from "./storage";
|
||||||
import { insertPixelSchema, insertUserCooldownSchema, type WSMessage } from "@shared/schema";
|
import { insertPixelSchema, insertUserCooldownSchema, type WSMessage } from "@shared/schema";
|
||||||
import { CanvasExporter } from "./export";
|
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> {
|
export async function registerRoutes(app: Express): Promise<Server> {
|
||||||
const httpServer = createServer(app);
|
const httpServer = createServer(app);
|
||||||
@ -15,6 +48,38 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
const exporter = new CanvasExporter(storage);
|
const exporter = new CanvasExporter(storage);
|
||||||
exporter.startAutoExport();
|
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
|
// API Routes
|
||||||
app.get("/api/pixels", async (req, res) => {
|
app.get("/api/pixels", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@ -37,9 +102,14 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
// Config is now read-only from file
|
// Config is now read-only from file
|
||||||
// Remove the POST endpoint for config updates
|
// Remove the POST endpoint for config updates
|
||||||
|
|
||||||
app.post("/api/pixels", async (req, res) => {
|
app.post("/api/pixels", requireAuth, async (req, res) => {
|
||||||
try {
|
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();
|
const config = await storage.getCanvasConfig();
|
||||||
|
|
||||||
// Validate coordinates
|
// Validate coordinates
|
||||||
@ -50,7 +120,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
|
|
||||||
// Check cooldown unless events are enabled
|
// Check cooldown unless events are enabled
|
||||||
if (!config.enableAutomaticEvents) {
|
if (!config.enableAutomaticEvents) {
|
||||||
const cooldown = await storage.getUserCooldown(pixelData.userId);
|
const cooldown = await storage.getUserCooldown(userInfo.userId);
|
||||||
if (cooldown && cooldown.cooldownEnds > new Date()) {
|
if (cooldown && cooldown.cooldownEnds > new Date()) {
|
||||||
return res.status(429).json({ message: "Cooldown active" });
|
return res.status(429).json({ message: "Cooldown active" });
|
||||||
}
|
}
|
||||||
@ -58,7 +128,7 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
// Set new cooldown
|
// Set new cooldown
|
||||||
const cooldownEnd = new Date(Date.now() + (config.defaultCooldown * 1000));
|
const cooldownEnd = new Date(Date.now() + (config.defaultCooldown * 1000));
|
||||||
await storage.setUserCooldown({
|
await storage.setUserCooldown({
|
||||||
userId: pixelData.userId,
|
userId: userInfo.userId,
|
||||||
cooldownEnds: cooldownEnd,
|
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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user