feat: implement Keycloak SSO integration with JIT provisioning and Next.js login button
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 2m31s
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 2m31s
This commit is contained in:
@@ -24,3 +24,10 @@ MICHESS_API_PORT=3001
|
||||
# App-URL (für Next.js Metadaten)
|
||||
NEXT_PUBLIC_APP_URL=http://deine-nas-ip:3000
|
||||
NEXT_PUBLIC_API_URL=http://deine-nas-ip:3001
|
||||
|
||||
# Keycloak SSO (OIDC) Integration
|
||||
SSO_AUTHORITY=https://auth.mischlabs.de/realms/mischlabs
|
||||
SSO_CLIENT_ID=michess
|
||||
SSO_CLIENT_SECRET=dein_client_secret_aus_keycloak
|
||||
SSO_REDIRECT_URI=https://michess.mischlabs.de/v1/auth/sso/callback
|
||||
APP_URL=https://michess.mischlabs.de
|
||||
|
||||
127
brain.old
Normal file
127
brain.old
Normal file
@@ -0,0 +1,127 @@
|
||||
# Brain.md — MiChess Project Memory
|
||||
|
||||
> Dieses Dokument ist mein aktives Gedächtnis während der Entwicklung von MiChess.
|
||||
> Ich denke hier, notiere Entscheidungen, Probleme und Fortschritt.
|
||||
|
||||
---
|
||||
|
||||
## Projektziel
|
||||
|
||||
MiChess = Lichess-inspirierte Schachplattform, self-hosted auf einer NAS, in Docker, mit Stockfish-Engine.
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack (Entschieden)
|
||||
|
||||
| Schicht | Technologie | Grund |
|
||||
|---|---|---|
|
||||
| Frontend | Next.js 14 + React + Tailwind + daisyUI | Von chessu geerbt, modern, SSR-fähig |
|
||||
| Backend | Node.js + Express + Socket.io | Von chessu geerbt, real-time-ready |
|
||||
| Datenbank | PostgreSQL | Von chessu geerbt, solide |
|
||||
| Auth | express-session + argon2 | Bereits vorhanden, sicher |
|
||||
| Schach-Logik | chess.js | Bereits vorhanden |
|
||||
| Schachbrett UI | react-chessboard | Bereits vorhanden |
|
||||
| Stockfish | stockfish npm-Paket | Einfache Integration im Backend |
|
||||
| Container | Docker Compose | NAS-Deployment |
|
||||
| Package Manager | pnpm Workspaces | Monorepo-Setup |
|
||||
|
||||
---
|
||||
|
||||
## Basis-Projekt
|
||||
|
||||
- **Geclont von:** `dotnize/chessu` (MIT-Lizenz)
|
||||
- **Warum:** TypeScript, React/Next.js, Express, Socket.io, PostgreSQL, chess.js — perfekter Ausgangspunkt
|
||||
- **Was chessu schon hat:**
|
||||
- User-Accounts (name, email, password, wins/losses/draws)
|
||||
- Session-Auth mit argon2-Passwort-Hashing
|
||||
- Real-time Multiplayer via Socket.io
|
||||
- Spiele mit PGN-Speicherung
|
||||
- Public Games Liste
|
||||
- Archiv (gespielte Partien)
|
||||
- User-Profilseite
|
||||
|
||||
---
|
||||
|
||||
## Was ich bauen muss (Delta zu chessu)
|
||||
|
||||
1. **Stockfish AI-Gegner** — verschiedene Schwierigkeitsgrade (Level 1-8, ELO-basiert)
|
||||
2. **Admin-Panel** — Nutzerverwaltung, Git-Pull-Button, Statistiken
|
||||
3. **Freundessystem** — Freundschaftsanfragen, Freundesliste, Freunde herausfordern
|
||||
4. **Branding** — Alles auf "MiChess" umbenennen
|
||||
5. **Docker NAS-Deployment** — docker-compose für NAS, Auto-Update-Script
|
||||
6. **DB-Schema Erweiterungen** — admin-Flag, friends-Tabelle, friend_requests-Tabelle
|
||||
7. **E-Mail-basierte Registrierung** — (email-Feld schon da, braucht UI-Flow: email → unique username wählen)
|
||||
|
||||
---
|
||||
|
||||
## Architektur-Entscheidungen
|
||||
|
||||
### Stockfish im Backend
|
||||
- Stockfish läuft als Child-Process auf dem Server (NAS)
|
||||
- Kommunikation über UCI-Protokoll
|
||||
- Schwierigkeitsgrade via `Skill Level` (0-20) und `Depth`
|
||||
- Pro aktiver AI-Partie: eigener Stockfish-Prozess (oder Pool)
|
||||
|
||||
### Admin-Rolle
|
||||
- `role` Spalte in `user`-Tabelle (default: 'user', admin: 'admin')
|
||||
- Erster User mit email aus `ADMIN_EMAIL` env var wird automatisch Admin
|
||||
- Admin-Middleware schützt `/v1/admin/*` Routen
|
||||
|
||||
### Friends-System
|
||||
- `friends`-Tabelle: user_id_1, user_id_2 (symmetrisch)
|
||||
- `friend_requests`-Tabelle: from_id, to_id, status (pending/accepted/rejected)
|
||||
- Nutzer suchen via Username
|
||||
|
||||
### Git-Auto-Update (Admin-Feature)
|
||||
- Admin-Route: `POST /v1/admin/update`
|
||||
- Führt `git pull` im Container-Verzeichnis aus
|
||||
- Triggert danach Neustart (via restart-policy des Containers)
|
||||
- Update-Script wird ins Docker-Image eingebaut
|
||||
|
||||
### Docker NAS-Setup
|
||||
- `docker-compose.yml` mit Services: `michess-app` + `postgres`
|
||||
- Watchtower oder einfaches update-script für automatische Updates
|
||||
- Alle Credentials via `.env`-Datei
|
||||
|
||||
---
|
||||
|
||||
## Aktueller Status
|
||||
|
||||
- [x] Basis-Projekt geclont (chessu)
|
||||
- [x] Brain.md erstellt
|
||||
- [x] Plan.md erstellt
|
||||
- [ ] Git remote auf `git.mischlabs.de` gesetzt
|
||||
- [ ] Projekt auf "MiChess" umbenannt
|
||||
- [ ] DB-Schema erweitert (admin, friends, friend_requests)
|
||||
- [ ] Stockfish integriert
|
||||
- [ ] Admin-Panel gebaut
|
||||
- [ ] Friends-System gebaut
|
||||
- [ ] Docker NAS-Deployment finalisiert
|
||||
- [ ] Alles committed & gepusht
|
||||
|
||||
---
|
||||
|
||||
## Bekannte Probleme / Offene Fragen
|
||||
|
||||
- chessu's Dockerfile nutzt `CMD ["start"]` — muss für MiChess angepasst werden (Server + Client separat oder zusammen)
|
||||
- Stockfish binary muss im Docker-Image vorhanden sein (Alpine Linux: `apk add stockfish` oder npm-Paket)
|
||||
- Admin-Git-Pull: funktioniert nur wenn Container read-write Zugriff auf sein eigenes Verzeichnis hat — besser: Host-Script via webhook triggern
|
||||
|
||||
---
|
||||
|
||||
## Lessons Learned
|
||||
|
||||
*(wird im Verlauf befüllt)*
|
||||
|
||||
---
|
||||
|
||||
## Nächste Schritte (immer aktuell)
|
||||
|
||||
1. Git remote setzen → push
|
||||
2. Umbenennen chessu → MiChess
|
||||
3. DB-Schema erweitern
|
||||
4. Stockfish backend einbauen
|
||||
5. Admin-Panel UI + Backend
|
||||
6. Friends-System
|
||||
7. Docker finalisieren
|
||||
8. Issues aus Git bearbeiten
|
||||
@@ -4,6 +4,7 @@ import { SessionContext } from "@/context/session";
|
||||
import { login, register, setGuestSession } from "@/lib/auth";
|
||||
import type { FormEvent } from "react";
|
||||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import { API_URL } from "@/config";
|
||||
|
||||
import Guest from "./Guest";
|
||||
import Login from "./Login";
|
||||
@@ -14,8 +15,20 @@ export default function AuthModal() {
|
||||
const [activeTab, setActiveTab] = useState<"guest" | "login" | "register">("guest");
|
||||
const [serverMessage, setServerMessage] = useState<string | null>(null);
|
||||
const [buttonLoading, setButtonLoading] = useState(false);
|
||||
const [ssoEnabled, setSsoEnabled] = useState(false);
|
||||
const modalToggleRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${API_URL}/v1/auth/sso/config`, { credentials: "include" })
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data && data.enabled) {
|
||||
setSsoEnabled(true);
|
||||
}
|
||||
})
|
||||
.catch((err) => console.error("Error loading SSO config:", err));
|
||||
}, []);
|
||||
|
||||
function closeModal() {
|
||||
if (modalToggleRef.current?.checked) {
|
||||
modalToggleRef.current.checked = false;
|
||||
@@ -151,6 +164,30 @@ export default function AuthModal() {
|
||||
{activeTab === "register" && "Register"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{activeTab !== "guest" && ssoEnabled && (
|
||||
<>
|
||||
<div className="divider opacity-70">oder</div>
|
||||
<a
|
||||
href={`${API_URL}/v1/auth/sso/login`}
|
||||
className="btn btn-accent btn-outline w-full gap-2"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
className="h-5 w-5"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M12 1.5a5.25 5.25 0 0 0-5.25 5.25v3a3 3 0 0 0-3 3v6.75a3 3 0 0 0 3 3h10.5a3 3 0 0 0 3-3v-6.75a3 3 0 0 0-3-3v-3c0-2.9-2.35-5.25-5.25-5.25Zm3.75 8.25v-3a3.75 3.75 0 1 0-7.5 0v3h7.5Z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
Login mit Keycloak
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
</label>
|
||||
</label>
|
||||
|
||||
@@ -15,6 +15,11 @@ services:
|
||||
CORS_ORIGIN: ${CORS_ORIGIN:-https://michess.mischlabs.de}
|
||||
ADMIN_EMAIL: ${ADMIN_EMAIL:-}
|
||||
APP_DIR: /opt/michess
|
||||
SSO_AUTHORITY: ${SSO_AUTHORITY:-}
|
||||
SSO_CLIENT_ID: ${SSO_CLIENT_ID:-}
|
||||
SSO_CLIENT_SECRET: ${SSO_CLIENT_SECRET:-}
|
||||
SSO_REDIRECT_URI: ${SSO_REDIRECT_URI:-}
|
||||
APP_URL: ${APP_URL:-}
|
||||
ports:
|
||||
- "${MICHESS_PORT:-3000}:3000"
|
||||
- "${MICHESS_API_PORT:-3001}:3001"
|
||||
|
||||
44
pnpm-lock.yaml
generated
44
pnpm-lock.yaml
generated
@@ -105,6 +105,9 @@ importers:
|
||||
nanoid:
|
||||
specifier: ^5.0.7
|
||||
version: 5.1.7
|
||||
openid-client:
|
||||
specifier: ^5.6.0
|
||||
version: 5.7.1
|
||||
pg:
|
||||
specifier: ^8.12.0
|
||||
version: 8.20.0
|
||||
@@ -482,6 +485,7 @@ packages:
|
||||
|
||||
'@ungap/structured-clone@1.3.0':
|
||||
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
|
||||
deprecated: Potential CWE-502 - Update to 1.3.1 or higher
|
||||
|
||||
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
|
||||
resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==}
|
||||
@@ -1521,6 +1525,9 @@ packages:
|
||||
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
|
||||
hasBin: true
|
||||
|
||||
jose@4.15.9:
|
||||
resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==}
|
||||
|
||||
js-tokens@4.0.0:
|
||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||
|
||||
@@ -1583,6 +1590,10 @@ packages:
|
||||
lru-cache@10.4.3:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
|
||||
lru-cache@6.0.0:
|
||||
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
map-stream@0.1.0:
|
||||
resolution: {integrity: sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==}
|
||||
|
||||
@@ -1715,6 +1726,10 @@ packages:
|
||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
object-hash@2.2.0:
|
||||
resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
object-hash@3.0.0:
|
||||
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -1747,6 +1762,10 @@ packages:
|
||||
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
oidc-token-hash@5.2.0:
|
||||
resolution: {integrity: sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==}
|
||||
engines: {node: ^10.13.0 || >=12.0.0}
|
||||
|
||||
on-finished@2.4.1:
|
||||
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -1758,6 +1777,9 @@ packages:
|
||||
once@1.4.0:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
|
||||
openid-client@5.7.1:
|
||||
resolution: {integrity: sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==}
|
||||
|
||||
optionator@0.9.4:
|
||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -2549,6 +2571,9 @@ packages:
|
||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
yallist@4.0.0:
|
||||
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
|
||||
|
||||
yargs-parser@21.1.1:
|
||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -4175,6 +4200,8 @@ snapshots:
|
||||
|
||||
jiti@1.21.7: {}
|
||||
|
||||
jose@4.15.9: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
|
||||
js-yaml@4.1.1:
|
||||
@@ -4231,6 +4258,10 @@ snapshots:
|
||||
|
||||
lru-cache@10.4.3: {}
|
||||
|
||||
lru-cache@6.0.0:
|
||||
dependencies:
|
||||
yallist: 4.0.0
|
||||
|
||||
map-stream@0.1.0: {}
|
||||
|
||||
math-intrinsics@1.1.0: {}
|
||||
@@ -4336,6 +4367,8 @@ snapshots:
|
||||
|
||||
object-assign@4.1.1: {}
|
||||
|
||||
object-hash@2.2.0: {}
|
||||
|
||||
object-hash@3.0.0: {}
|
||||
|
||||
object-inspect@1.13.4: {}
|
||||
@@ -4378,6 +4411,8 @@ snapshots:
|
||||
define-properties: 1.2.1
|
||||
es-object-atoms: 1.1.1
|
||||
|
||||
oidc-token-hash@5.2.0: {}
|
||||
|
||||
on-finished@2.4.1:
|
||||
dependencies:
|
||||
ee-first: 1.1.1
|
||||
@@ -4388,6 +4423,13 @@ snapshots:
|
||||
dependencies:
|
||||
wrappy: 1.0.2
|
||||
|
||||
openid-client@5.7.1:
|
||||
dependencies:
|
||||
jose: 4.15.9
|
||||
lru-cache: 6.0.0
|
||||
object-hash: 2.2.0
|
||||
oidc-token-hash: 5.2.0
|
||||
|
||||
optionator@0.9.4:
|
||||
dependencies:
|
||||
deep-is: 0.1.4
|
||||
@@ -5245,6 +5287,8 @@ snapshots:
|
||||
|
||||
y18n@5.0.8: {}
|
||||
|
||||
yallist@4.0.0: {}
|
||||
|
||||
yargs-parser@21.1.1: {}
|
||||
|
||||
yargs@17.7.2:
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
"nanoid": "^5.0.7",
|
||||
"pg": "^8.12.0",
|
||||
"socket.io": "^4.7.5",
|
||||
"xss": "^1.0.15"
|
||||
"xss": "^1.0.15",
|
||||
"openid-client": "^5.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@michess/types": "workspace:*",
|
||||
|
||||
@@ -2,7 +2,10 @@ import type { User } from "@michess/types";
|
||||
import { hash, verify } from "argon2";
|
||||
import type { Request, Response } from "express";
|
||||
import xss from "xss";
|
||||
import crypto from "crypto";
|
||||
import { Issuer } from "openid-client";
|
||||
|
||||
import { db } from "../db/index.js";
|
||||
import { activeGames } from "../db/models/game.model.js";
|
||||
import UserModel from "../db/models/user.model.js";
|
||||
import { io } from "../server.js";
|
||||
@@ -317,3 +320,123 @@ export const updateUser = async (req: Request, res: Response) => {
|
||||
res.status(500).end();
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== KEYCLOAK SSO API ====================
|
||||
|
||||
let oidcClient: any = null;
|
||||
|
||||
async function getOidcClient() {
|
||||
if (oidcClient) return oidcClient;
|
||||
|
||||
const ssoAuthority = process.env.SSO_AUTHORITY || "https://auth.mischlabs.de/realms/mischlabs";
|
||||
const ssoClientId = process.env.SSO_CLIENT_ID || "michess";
|
||||
const ssoClientSecret = process.env.SSO_CLIENT_SECRET;
|
||||
const ssoRedirectUri = process.env.SSO_REDIRECT_URI || "https://michess.mischlabs.de/v1/auth/sso/callback";
|
||||
|
||||
if (!ssoClientSecret) {
|
||||
throw new Error("SSO_CLIENT_SECRET is not configured");
|
||||
}
|
||||
|
||||
const issuer = await Issuer.discover(ssoAuthority);
|
||||
oidcClient = new issuer.Client({
|
||||
client_id: ssoClientId,
|
||||
client_secret: ssoClientSecret,
|
||||
redirect_uris: [ssoRedirectUri],
|
||||
response_types: ["code"]
|
||||
});
|
||||
|
||||
return oidcClient;
|
||||
}
|
||||
|
||||
export const getSsoConfig = async (req: Request, res: Response) => {
|
||||
res.json({
|
||||
enabled: !!(process.env.SSO_CLIENT_SECRET && process.env.SSO_CLIENT_ID && process.env.SSO_AUTHORITY)
|
||||
});
|
||||
};
|
||||
|
||||
export const ssoLogin = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const client = await getOidcClient();
|
||||
const authorizationUrl = client.authorizationUrl({
|
||||
scope: "openid email profile",
|
||||
state: "mischlabs-state"
|
||||
});
|
||||
res.redirect(authorizationUrl);
|
||||
} catch (err: any) {
|
||||
console.error("SSO Login Error:", err);
|
||||
res.status(500).send("SSO Login Initialisierung fehlgeschlagen: " + err.message);
|
||||
}
|
||||
};
|
||||
|
||||
export const ssoCallback = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const client = await getOidcClient();
|
||||
const params = client.callbackParams(req);
|
||||
const ssoRedirectUri = process.env.SSO_REDIRECT_URI || "https://michess.mischlabs.de/v1/auth/sso/callback";
|
||||
const tokenSet = await client.callback(ssoRedirectUri, params, { state: "mischlabs-state" });
|
||||
const userinfo = await client.userinfo(tokenSet.access_token);
|
||||
|
||||
const username = (userinfo.preferred_username || userinfo.name || userinfo.sub) as string;
|
||||
const email = userinfo.email as string;
|
||||
|
||||
if (!username) {
|
||||
return res.status(400).send("Kein Benutzername im OIDC Token gefunden.");
|
||||
}
|
||||
|
||||
// Check if user exists in Michess database
|
||||
const users = await UserModel.findByNameEmail({ name: username, email: email }, false, 1);
|
||||
|
||||
const handleUserLogin = (dbUser: User) => {
|
||||
req.session.user = {
|
||||
id: dbUser.id,
|
||||
name: dbUser.name,
|
||||
email: dbUser.email,
|
||||
wins: dbUser.wins,
|
||||
losses: dbUser.losses,
|
||||
draws: dbUser.draws,
|
||||
elo: dbUser.elo,
|
||||
role: dbUser.role
|
||||
};
|
||||
|
||||
const appUrl = process.env.APP_URL || "https://michess.mischlabs.de";
|
||||
req.session.save(() => {
|
||||
res.redirect(appUrl);
|
||||
});
|
||||
};
|
||||
|
||||
if (users && users.length) {
|
||||
// User exists, log them in
|
||||
handleUserLogin(users[0]);
|
||||
} else {
|
||||
// User does not exist, provision them Just-In-Time (JIT)
|
||||
const randomPassword = crypto.randomBytes(32).toString("hex");
|
||||
const hashedPassword = await hash(randomPassword);
|
||||
|
||||
// Determine role: Option A (automatically make 'MrDiderot' an admin, or the first user in an empty DB)
|
||||
let role = "user";
|
||||
const countRes = await db.query('SELECT COUNT(*) as count FROM "user"');
|
||||
const count = parseInt(countRes.rows[0].count, 10);
|
||||
|
||||
if (count === 0 || username.toLowerCase() === "mrdiderot") {
|
||||
role = "admin";
|
||||
}
|
||||
|
||||
// Create user
|
||||
const newUser = await UserModel.create({ name: username, email: email || "" }, hashedPassword);
|
||||
if (!newUser || !newUser.id) {
|
||||
return res.status(500).send("JIT-Benutzererstellung fehlgeschlagen");
|
||||
}
|
||||
|
||||
// If we assigned admin role, update it in DB
|
||||
if (role === "admin") {
|
||||
await UserModel.adminUpdate(newUser.id as number, { role: "admin" });
|
||||
newUser.role = "admin";
|
||||
}
|
||||
|
||||
handleUserLogin(newUser);
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("SSO Callback Error:", err);
|
||||
res.status(500).send("SSO Authentifizierung fehlgeschlagen: " + err.message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,4 +14,9 @@ router.route("/logout").post(controller.logoutSession);
|
||||
router.route("/register").post(controller.registerUser);
|
||||
router.route("/login").post(controller.loginUser);
|
||||
|
||||
// SSO Keycloak
|
||||
router.route("/sso/config").get(controller.getSsoConfig);
|
||||
router.route("/sso/login").get(controller.ssoLogin);
|
||||
router.route("/sso/callback").get(controller.ssoCallback);
|
||||
|
||||
export default router;
|
||||
|
||||
Reference in New Issue
Block a user