814 lines
25 KiB
Plaintext
Executable File
814 lines
25 KiB
Plaintext
Executable File
== PortalUI mit User-Verwaltung ==
|
||
|
||
Mein Server hat u.A. zwei Docker Container: Einmal das Portal, und einmal einen Authentifikations-Dienst.
|
||
|
||
|
||
AppServerPortalUI:
|
||
image: nginx:alpine
|
||
container_name: appServer_PortalUI
|
||
volumes:
|
||
- /home/chk/Documents/appServerPortalUI/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||
- /home/chk/Documents/appServerPortalUI/public:/usr/share/nginx/html:ro
|
||
# Let's Encrypt mounts
|
||
- /home/chk/Documents/appServerPortalUI/letsencrypt/conf:/etc/letsencrypt:ro
|
||
- /home/chk/Documents/appServerPortalUI/letsencrypt/www:/var/www/certbot:ro
|
||
# PortForwarding-Script-etc
|
||
- /home/chk/Documents/appServerPortalUI/forwarding.conf:/etc/nginx/forwarding.conf:ro
|
||
- /home/chk/Documents/appServerPortalUI/connect-proxies.sh:/docker-entrypoint.d/40-connect-proxies.sh:ro
|
||
ports:
|
||
- "80:80"
|
||
- "443:443"
|
||
networks:
|
||
- appRobotNet
|
||
restart: unless-stopped
|
||
command: ["nginx", "-g", "daemon off;"]
|
||
|
||
AppServerAuth:
|
||
image: node:24-alpine
|
||
container_name: appServer_Auth
|
||
volumes:
|
||
- /home/chk/Documents/appServerPortalUI/auth:/usr/src/app
|
||
working_dir: /usr/src/app
|
||
command: sh -c "npm install && node auth.js"
|
||
ports:
|
||
- "10300:3000" # optional, für Tests
|
||
networks:
|
||
- default
|
||
- appRobotNet
|
||
restart: unless-stopped
|
||
|
||
|
||
== /home/chk/Documents/appServerPortalUI/nginx.conf ==
|
||
|
||
# /etc/nginx/conf.d/default.conf
|
||
# IMMER aktive HTTP-Konfiguration (Port 80)
|
||
server {
|
||
listen 80 default_server;
|
||
listen [::]:80 default_server;
|
||
|
||
# ACME HTTP-01 Challenge (Certbot)
|
||
location ^~ /.well-known/acme-challenge/ {
|
||
root /var/www/certbot;
|
||
default_type "text/plain; charset=utf-8";
|
||
}
|
||
|
||
# Deine Default-Page
|
||
root /usr/share/nginx/html;
|
||
index index.html;
|
||
|
||
location = / { try_files /index.html =404; }
|
||
location / { try_files $uri $uri/ =404; }
|
||
}
|
||
|
||
== nginxPages/10-server-schooltech.conf ==
|
||
Soll die "Nutzlast" die Pages, die der User verwenden soll, weiterleiten.
|
||
|
||
hier gibt es eine ganze reihe weiterer pages, die bereitstehen.
|
||
server {
|
||
listen 443 ssl http2;
|
||
server_name server.schooltech.ch;
|
||
|
||
ssl_certificate /etc/letsencrypt/live/server.schooltech.ch/fullchain.pem;
|
||
ssl_certificate_key /etc/letsencrypt/live/server.schooltech.ch/privkey.pem;
|
||
ssl_protocols TLSv1.2 TLSv1.3;
|
||
ssl_prefer_server_ciphers on;
|
||
|
||
root /usr/share/nginx/html;
|
||
index index.html;
|
||
|
||
# UI / SPA
|
||
location / {
|
||
try_files $uri $uri/ /index.html;
|
||
}
|
||
|
||
# API forwarding (auth)
|
||
location /api/ {
|
||
proxy_pass http://appserverauth:3000/api/;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
}
|
||
|
||
# Internal auth endpoint for auth_request
|
||
location = /nginxauth {
|
||
internal;
|
||
proxy_pass http://appserverauth:3000/internal/auth;
|
||
proxy_set_header Cookie $http_cookie;
|
||
proxy_set_header X-Original-URI $request_uri;
|
||
proxy_set_header X-Original-Host $host;
|
||
proxy_set_header X-Forwarded-Host $host;
|
||
}
|
||
|
||
# Security headers
|
||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||
}
|
||
|
||
== nginxPages/50-subdomains-userA.conf ==
|
||
Für jeden User gibt es eine reihe von Pages, die eben hier weitergeleitet werden sollen.
|
||
|
||
server {
|
||
listen 443 ssl http2 default_server;
|
||
server_name _;
|
||
|
||
ssl_certificate /etc/letsencrypt/live/server.schooltech.ch/fullchain.pem;
|
||
ssl_certificate_key /etc/letsencrypt/live/server.schooltech.ch/privkey.pem;
|
||
ssl_protocols TLSv1.2 TLSv1.3;
|
||
|
||
return 444;
|
||
}
|
||
|
||
# ------------------------------------------------------------
|
||
# portainer.server.schooltech.ch
|
||
# ------------------------------------------------------------
|
||
server {
|
||
listen 443 ssl http2;
|
||
server_name portainer.server.schooltech.ch;
|
||
|
||
ssl_certificate /etc/letsencrypt/live/server.schooltech.ch/fullchain.pem;
|
||
ssl_certificate_key /etc/letsencrypt/live/server.schooltech.ch/privkey.pem;
|
||
ssl_protocols TLSv1.2 TLSv1.3;
|
||
ssl_prefer_server_ciphers on;
|
||
|
||
# Auth nur auf UI
|
||
location / {
|
||
auth_request /nginxauth;
|
||
|
||
proxy_pass http://portainer:9000;
|
||
proxy_http_version 1.1;
|
||
|
||
proxy_set_header Upgrade $http_upgrade;
|
||
proxy_set_header Connection "upgrade";
|
||
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Forwarded-Proto https;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
|
||
# iFrame-freundlich
|
||
proxy_hide_header X-Frame-Options;
|
||
add_header X-Frame-Options "ALLOWALL" always;
|
||
proxy_hide_header Content-Security-Policy;
|
||
add_header Content-Security-Policy "frame-ancestors *" always;
|
||
}
|
||
|
||
location = /nginxauth {
|
||
internal;
|
||
proxy_pass http://appserverauth:3000/internal/auth;
|
||
proxy_set_header Cookie $http_cookie;
|
||
proxy_set_header X-Original-URI $request_uri;
|
||
proxy_set_header X-Original-Host $host;
|
||
proxy_set_header X-Forwarded-Host $host;
|
||
}
|
||
}
|
||
|
||
# ------------------------------------------------------------
|
||
# abc.server.schooltech.ch
|
||
# ------------------------------------------------------------
|
||
server {
|
||
listen 443 ssl http2;
|
||
server_name abc.server.schooltech.ch;
|
||
|
||
ssl_certificate /etc/letsencrypt/live/server.schooltech.ch/fullchain.pem;
|
||
ssl_certificate_key /etc/letsencrypt/live/server.schooltech.ch/privkey.pem;
|
||
|
||
root /usr/share/nginx/abc;
|
||
index index.html;
|
||
|
||
location / {
|
||
try_files $uri $uri/ /index.html;
|
||
}
|
||
}
|
||
|
||
# ------------------------------------------------------------
|
||
# xyz.server.schooltech.ch
|
||
# ------------------------------------------------------------
|
||
server {
|
||
listen 443 ssl http2;
|
||
server_name xyz.server.schooltech.ch;
|
||
|
||
ssl_certificate /etc/letsencrypt/live/server.schooltech.ch/fullchain.pem;
|
||
ssl_certificate_key /etc/letsencrypt/live/server.schooltech.ch/privkey.pem;
|
||
|
||
root /usr/share/nginx/xyz;
|
||
index index.html;
|
||
|
||
location / {
|
||
try_files $uri $uri/ /index.html;
|
||
}
|
||
}
|
||
|
||
|
||
|
||
diese sind alle unter xxx.server.schooltech.ch erreichbar.
|
||
Die ganze nginxPages/50-subdomains-userA.conf werden mit einem Script erstellt:
|
||
|
||
== connect-proxies.sh ==========
|
||
|
||
#!/bin/sh
|
||
# /docker-entrypoint.d/40-connect-proxies.sh
|
||
# Generiert pro Zeile in /etc/nginx/forwarding.conf:
|
||
# - HTTPS vHost (Proxy oder local static) NUR wenn Zertifikate existieren
|
||
# - optional 80->443 Redirect-Server (mit ACME-Ausnahme) bei http_behavior=redirect
|
||
# Läuft automatisch beim Containerstart (nginx:alpine EntryPoint).
|
||
|
||
set -eu
|
||
|
||
CONF_DIR="/etc/nginx/conf.d"
|
||
LIVE_DIR="/etc/letsencrypt/live"
|
||
FWD_FILE="/etc/nginx/forwarding.conf"
|
||
|
||
HTTPS_SUFFIX="-https.generated.conf"
|
||
HTTP_REDIRECT_SUFFIX="-http-redirect.generated.conf"
|
||
GLOBALS_FILE="$CONF_DIR/_globals.generated.conf"
|
||
|
||
echo "[connect-proxies] start …"
|
||
|
||
# 0) Forwarding-Datei vorhanden?
|
||
if [ ! -f "$FWD_FILE" ]; then
|
||
echo "[connect-proxies] WARN: $FWD_FILE fehlt – keine Proxies zu generieren."
|
||
exit 0
|
||
fi
|
||
|
||
# 1) Globale HTTP-Kontext-Map + Resolver (idempotent)
|
||
# >>> CHANGE: Resolver NICHT hardcoden. Dynamisch aus /etc/resolv.conf ableiten, Fallback 127.0.0.11
|
||
RESOLVERS="$(awk '/^nameserver/{print $2}' /etc/resolv.conf | xargs || true)"
|
||
if [ -n "${RESOLVERS:-}" ]; then
|
||
RESOLVER_LINE="resolver $RESOLVERS ipv6=off valid=30s;"
|
||
else
|
||
RESOLVER_LINE="resolver 127.0.0.11 ipv6=off valid=30s;"
|
||
fi
|
||
|
||
cat > "$GLOBALS_FILE" <<NGINX
|
||
# Automatisch generiert – nicht editieren
|
||
map \$http_upgrade \$connection_upgrade {
|
||
default upgrade;
|
||
'' close;
|
||
}
|
||
$RESOLVER_LINE
|
||
NGINX
|
||
# <<< END CHANGE
|
||
|
||
# 2) Alte generierte Confs entfernen
|
||
rm -f "$CONF_DIR/"*"$HTTPS_SUFFIX" 2>/dev/null || true
|
||
rm -f "$CONF_DIR/"*"$HTTP_REDIRECT_SUFFIX" 2>/dev/null || true
|
||
|
||
# 3) Zeilen verarbeiten
|
||
LINE_NO=0
|
||
while IFS= read -r RAW || [ -n "$RAW" ]; do
|
||
LINE_NO=$((LINE_NO+1))
|
||
# trim + CR entfernen
|
||
LINE="$(printf '%s' "$RAW" | tr -d '\r' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')"
|
||
[ -z "$LINE" ] && continue
|
||
case "$LINE" in \#*) continue;; esac
|
||
|
||
# Spalten splitten (mindestens 2 erforderlich)
|
||
set -- $LINE
|
||
SERVER_NAME="${1:-}"
|
||
UPSTREAM_URL="${2:-}"
|
||
HTTP_BEHAVIOR="${3:-redirect}"
|
||
WEBSOCKETS="${4:-false}"
|
||
VERIFY_TLS="${5:-false}"
|
||
CERT_DOMAIN="${6:-$SERVER_NAME}"
|
||
LISTEN_PORT="${7:-443}"
|
||
|
||
if [ -z "$SERVER_NAME" ] || [ -z "$UPSTREAM_URL" ]; then
|
||
echo "[connect-proxies] WARN(Line $LINE_NO): unvollständig -> $LINE"
|
||
continue
|
||
fi
|
||
|
||
FULLCHAIN="$LIVE_DIR/$CERT_DOMAIN/fullchain.pem"
|
||
PRIVKEY="$LIVE_DIR/$CERT_DOMAIN/privkey.pem"
|
||
|
||
HTTPS_OUT="$CONF_DIR/${SERVER_NAME}-p${LISTEN_PORT}${HTTPS_SUFFIX}"
|
||
HTTP_REDIRECT_OUT="$CONF_DIR/${SERVER_NAME}${HTTP_REDIRECT_SUFFIX}"
|
||
|
||
# >>> NEW: Upstream normalisieren (Trailing Slash entfernen) + DNS-Check vorbereiten
|
||
SANITIZED_UPSTREAM="${UPSTREAM_URL%/}"
|
||
DNS_OK="true"
|
||
SCHEME=""; HOST=""; PORT=""
|
||
|
||
if [ "$SANITIZED_UPSTREAM" != "local" ]; then
|
||
case "$SANITIZED_UPSTREAM" in
|
||
http://*)
|
||
URI="${SANITIZED_UPSTREAM#http://}"; SCHEME="http"; DEFAULT_PORT="80"
|
||
;;
|
||
https://*)
|
||
URI="${SANITIZED_UPSTREAM#https://}"; SCHEME="https"; DEFAULT_PORT="443"
|
||
;;
|
||
*)
|
||
echo "[connect-proxies] WARN(Line $LINE_NO): $SERVER_NAME upstream_url ungültig: '$UPSTREAM_URL' – überspringe."
|
||
continue
|
||
;;
|
||
esac
|
||
HOSTPORT="${URI%%/*}"
|
||
HOST="${HOSTPORT%%:*}"
|
||
PORT="${HOSTPORT#*:}"; [ "$PORT" = "$HOSTPORT" ] && PORT="$DEFAULT_PORT"
|
||
|
||
if command -v getent >/dev/null 2>&1; then
|
||
if ! getent hosts "$HOST" >/dev/null 2>&1; then
|
||
DNS_OK="false"
|
||
echo "[connect-proxies] [-] $SERVER_NAME: DNS nicht auflösbar ($HOST) – erzeuge 503-Placeholder statt Proxy."
|
||
fi
|
||
else
|
||
DNS_OK="unknown"
|
||
fi
|
||
fi
|
||
# <<< END NEW
|
||
|
||
if [ -f "$FULLCHAIN" ] && [ -f "$PRIVKEY" ]; then
|
||
echo "[connect-proxies] [+] $SERVER_NAME: Zertifikat OK (cert_domain=$CERT_DOMAIN). Erzeuge 443 …"
|
||
|
||
# Fall A: local (statisch, kein proxy_pass)
|
||
if [ "$SANITIZED_UPSTREAM" = "local" ]; then
|
||
cat > "$HTTPS_OUT" <<NGINX
|
||
# Auto-generated - 443 static site
|
||
server {
|
||
listen ${LISTEN_PORT} ssl http2;
|
||
listen [::]:${LISTEN_PORT} ssl http2;
|
||
server_name $SERVER_NAME;
|
||
|
||
ssl_certificate $FULLCHAIN;
|
||
ssl_certificate_key $PRIVKEY;
|
||
|
||
ssl_protocols TLSv1.2 TLSv1.3;
|
||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||
|
||
root /usr/share/nginx/html;
|
||
index index.html;
|
||
|
||
location / {
|
||
try_files \$uri \$uri/ /index.html;
|
||
}
|
||
}
|
||
NGINX
|
||
|
||
else
|
||
# >>> NEW: Zwei Pfade – DNS_OK=false => Placeholder; sonst Proxy mit Laufzeit-Resolver
|
||
if [ "$DNS_OK" = "false" ]; then
|
||
# 443 Placeholder – keine Proxy-Verbindung, saubere 503
|
||
cat > "$HTTPS_OUT" <<NGINX
|
||
# Auto-generated - 443 placeholder (DNS failed)
|
||
server {
|
||
listen ${LISTEN_PORT} ssl http2;
|
||
listen [::]:${LISTEN_PORT} ssl http2;
|
||
server_name $SERVER_NAME;
|
||
|
||
ssl_certificate $FULLCHAIN;
|
||
ssl_certificate_key $PRIVKEY;
|
||
|
||
ssl_protocols TLSv1.2 TLSv1.3;
|
||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||
|
||
location / {
|
||
default_type text/html;
|
||
return 503 "<!doctype html><html><head><meta charset='utf-8'><title>Service temporarily unavailable</title></head><body style='font-family:sans-serif;margin:3rem'><h1>\$server_name nicht erreichbar</h1><p>DNS-Auflösung fehlgeschlagen. Bitte später erneut versuchen.</p></body></html>";
|
||
}
|
||
}
|
||
NGINX
|
||
else
|
||
# 443 Proxy – DNS ok/unknown: Laufzeit-Auflösung + freundlicher 503 bei Downstreams
|
||
cat > "$HTTPS_OUT" <<NGINX
|
||
# Auto-generated - 443 reverse proxy
|
||
server {
|
||
listen ${LISTEN_PORT} ssl http2;
|
||
listen [::]:${LISTEN_PORT} ssl http2;
|
||
server_name $SERVER_NAME;
|
||
|
||
ssl_certificate $FULLCHAIN;
|
||
ssl_certificate_key $PRIVKEY;
|
||
|
||
ssl_protocols TLSv1.2 TLSv1.3;
|
||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||
|
||
# Fehler sauber abfangen und 503 liefern (statt rohe 502/504)
|
||
proxy_intercept_errors on;
|
||
error_page 502 503 504 = @service_down;
|
||
|
||
location / {
|
||
# >>> CHANGE: variable proxy_pass -> DNS zur Laufzeit (verhindert nginx -t Crash)
|
||
set \$target $SANITIZED_UPSTREAM;
|
||
proxy_pass \$target;
|
||
|
||
proxy_set_header Host \$host;
|
||
proxy_set_header X-Real-IP \$remote_addr;
|
||
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto \$scheme;
|
||
|
||
proxy_http_version 1.1;
|
||
NGINX
|
||
|
||
if [ "$WEBSOCKETS" = "true" ]; then
|
||
cat >> "$HTTPS_OUT" <<'NGINX'
|
||
proxy_set_header Upgrade $http_upgrade;
|
||
proxy_set_header Connection $connection_upgrade;
|
||
NGINX
|
||
else
|
||
cat >> "$HTTPS_OUT" <<'NGINX'
|
||
proxy_set_header Upgrade "";
|
||
proxy_set_header Connection close;
|
||
NGINX
|
||
fi
|
||
|
||
case "$SANITIZED_UPSTREAM" in
|
||
https://*)
|
||
if [ "$VERIFY_TLS" = "true" ]; then
|
||
cat >> "$HTTPS_OUT" <<'NGINX'
|
||
proxy_ssl_verify on;
|
||
proxy_ssl_server_name on;
|
||
NGINX
|
||
else
|
||
cat >> "$HTTPS_OUT" <<'NGINX'
|
||
proxy_ssl_verify off;
|
||
proxy_ssl_server_name on;
|
||
NGINX
|
||
fi
|
||
;;
|
||
esac
|
||
|
||
cat >> "$HTTPS_OUT" <<'NGINX'
|
||
client_max_body_size 50m;
|
||
proxy_connect_timeout 60s;
|
||
proxy_send_timeout 60s;
|
||
proxy_read_timeout 60s;
|
||
}
|
||
|
||
location @service_down {
|
||
default_type text/html;
|
||
return 503 "<!doctype html><html><head><meta charset='utf-8'><title>Service temporarily unavailable</title></head><body style='font-family:sans-serif;margin:3rem'><h1>$server_name nicht erreichbar</h1><p>Der Dienst ist momentan nicht verfügbar. Bitte später erneut versuchen.</p></body></html>";
|
||
}
|
||
|
||
}
|
||
NGINX
|
||
fi
|
||
# <<< END NEW
|
||
fi
|
||
|
||
# 80->443 Redirect-Server nur, wenn gewünscht
|
||
if [ "$HTTP_BEHAVIOR" = "redirect" ]; then
|
||
cat > "$HTTP_REDIRECT_OUT" <<NGINX
|
||
# Auto-generated – 80->443 redirect for $SERVER_NAME
|
||
server {
|
||
listen 80;
|
||
listen [::]:80;
|
||
server_name $SERVER_NAME;
|
||
|
||
# ACME-Ausnahme
|
||
location ^~ /.well-known/acme-challenge/ {
|
||
root /var/www/certbot;
|
||
default_type "text/plain; charset=utf-8";
|
||
}
|
||
|
||
location / {
|
||
return 301 https://\$host\$request_uri;
|
||
}
|
||
}
|
||
NGINX
|
||
else
|
||
# Sicherstellen, dass kein alter Redirect liegen bleibt
|
||
rm -f "$HTTP_REDIRECT_OUT" 2>/dev/null || true
|
||
fi
|
||
|
||
else
|
||
echo "[connect-proxies] [-] $SERVER_NAME: keine Zertifikate (cert_domain=$CERT_DOMAIN). Entferne evtl. alte Confs."
|
||
rm -f "$HTTPS_OUT" "$HTTP_REDIRECT_OUT" 2>/dev/null || true
|
||
fi
|
||
|
||
done < "$FWD_FILE"
|
||
|
||
echo "[connect-proxies] nginx -t …"
|
||
nginx -t
|
||
echo "[connect-proxies] done."
|
||
|
||
=====
|
||
wobei die daten dann im forwarding.conf stehen:
|
||
|
||
|
||
# 444 → 8121 (WS/WSS)
|
||
# fluidncRedWs.server.schooltech.ch http://appServer_TunnelHead:8121 redirect true false fluidncRedWs.server.schooltech.ch 444
|
||
server.schooltech.ch local static false false
|
||
tcPortainer.server.schooltech.ch http://thinkcentre.local:9000 redirect true false
|
||
tcGuac.server.schooltech.ch http://thinkcentre.local:9000 redirect true false
|
||
|
||
#inf InformatiWeb ist per Tunnel angeschlossen. Soll auf 97xx Ports gehen
|
||
infPortainer.server.schooltech.ch http://appServer_TunnelHead:9903 redirect true false
|
||
infGuac.server.schooltech.ch http://appServer_TunnelHead:9980 redirect true false
|
||
|
||
#RP5 ist "Lokal" der Server
|
||
rp5Guac.server.schooltech.ch http://appServer_guacamole:8080 redirect true false
|
||
rp5Portainer.server.schooltech.ch http://portainer:9000 redirect true false
|
||
|
||
|
||
=== Das PortalUI stellt neben den Weiterleitungen auch noch eine Navigations-Page
|
||
zur Verfügung: index.html mit app.js
|
||
|
||
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<title>Service Portal</title>
|
||
<link rel="stylesheet" href="style.css" />
|
||
</head>
|
||
<body>
|
||
|
||
<header id="header">
|
||
<div class="logo">schooltech</div>
|
||
<nav id="services"></nav>
|
||
<div class="user">
|
||
<button id="login-btn">Login</button>
|
||
</div>
|
||
</header>
|
||
|
||
<main>
|
||
<h1>server.schooltech.ch - Service Portal</h1>
|
||
|
||
<!-- iFrame für Services -->
|
||
<iframe id="service-frame" style="width:100%;height:80vh;display:none;"></iframe>
|
||
|
||
<!-- Login Modal -->
|
||
<div id="login-modal" style="display:none;">
|
||
<label>User: <input type="text" id="username"/></label>
|
||
<label>Pass: <input type="password" id="password"/></label>
|
||
<button id="login-submit">Login</button>
|
||
<div id="login-msg"></div>
|
||
</div>
|
||
</main>
|
||
|
||
|
||
<script src="app.js"></script>
|
||
</body>
|
||
</html>
|
||
|
||
|
||
// ==== FRONTEND app.js ====
|
||
|
||
// Service-Liste
|
||
const services = [
|
||
{ id: "abc", name: "Control GamePad", url: "https://abc.server.schooltech.ch/" },
|
||
{ id: "xyz", name: "Guacamole", url: "https://xyz.server.schooltech.ch/" },
|
||
{ id: "sim", name: "Simulation", url: "https://simulation.server.schooltech.ch/" },
|
||
{ id: "portainer", name: "Portainer", url: "https://portainer.server.schooltech.ch/" }
|
||
];
|
||
|
||
// DOM-Elemente
|
||
const iframe = document.getElementById("service-frame");
|
||
const loginModal = document.getElementById("login-modal");
|
||
const loginBtn = document.getElementById("login-btn");
|
||
const loginSubmit = document.getElementById("login-submit");
|
||
const loginMsg = document.getElementById("login-msg");
|
||
const nav = document.getElementById("services");
|
||
|
||
const usernameInput = document.getElementById("username");
|
||
const passwordInput = document.getElementById("password");
|
||
|
||
let loggedIn = false;
|
||
|
||
// ===========================
|
||
// Login anzeigen
|
||
// ===========================
|
||
function switchToLogin() {
|
||
loginBtn.textContent = "Login";
|
||
loginBtn.onclick = () => {
|
||
loginModal.style.display = "block";
|
||
};
|
||
}
|
||
|
||
// ===========================
|
||
// Logout anzeigen
|
||
// ===========================
|
||
function switchToLogout() {
|
||
loginBtn.textContent = "Logout";
|
||
loginBtn.onclick = async () => {
|
||
try {
|
||
await fetch("/api/logout", { method: "POST" });
|
||
} catch (e) {
|
||
console.warn("Logout request failed:", e);
|
||
}
|
||
performLocalLogout();
|
||
};
|
||
}
|
||
|
||
// ===========================
|
||
// Lokales Logout
|
||
// ===========================
|
||
function performLocalLogout() {
|
||
loggedIn = false;
|
||
iframe.src = "";
|
||
iframe.style.display = "none";
|
||
nav.innerHTML = "";
|
||
loginModal.style.display = "block";
|
||
switchToLogin();
|
||
}
|
||
|
||
// ===========================
|
||
// Login-Logik
|
||
// ===========================
|
||
async function doLogin() {
|
||
const user = usernameInput.value;
|
||
const pass = passwordInput.value;
|
||
|
||
try {
|
||
const res = await fetch("/api/login", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ user, pass })
|
||
});
|
||
|
||
if (res.ok) {
|
||
loggedIn = true;
|
||
loginModal.style.display = "none";
|
||
loginMsg.textContent = "";
|
||
setupServiceButtons();
|
||
switchToLogout();
|
||
} else {
|
||
loginMsg.textContent = "Login fehlgeschlagen";
|
||
}
|
||
} catch (e) {
|
||
loginMsg.textContent = "Fehler: " + e.message;
|
||
}
|
||
}
|
||
|
||
loginSubmit.onclick = doLogin;
|
||
|
||
// Enter-Taste Login
|
||
[usernameInput, passwordInput].forEach(input => {
|
||
input.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
doLogin();
|
||
}
|
||
});
|
||
});
|
||
|
||
// ===========================
|
||
// Buttons erzeugen
|
||
// ===========================
|
||
function setupServiceButtons() {
|
||
nav.innerHTML = "";
|
||
services.forEach(svc => {
|
||
const btn = document.createElement("button");
|
||
btn.textContent = svc.name;
|
||
btn.onclick = async () => {
|
||
try {
|
||
await fetch('/api/event', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
event: 'open',
|
||
service: svc.id,
|
||
url: svc.url
|
||
})
|
||
});
|
||
} catch(e) {
|
||
console.error('Event log failed', e);
|
||
}
|
||
openService(svc);
|
||
};
|
||
nav.appendChild(btn);
|
||
});
|
||
}
|
||
|
||
// ===========================
|
||
// Service öffnen
|
||
// ===========================
|
||
function openService(svc) {
|
||
iframe.src = svc.url;
|
||
iframe.style.display = "block";
|
||
window.scrollTo(0,0);
|
||
}
|
||
|
||
// ===========================
|
||
// Session Status beim Laden prüfen
|
||
// ===========================
|
||
(async function checkStatus() {
|
||
try {
|
||
const r = await fetch('/api/status');
|
||
if (r.ok) {
|
||
loggedIn = true;
|
||
setupServiceButtons();
|
||
switchToLogout();
|
||
} else {
|
||
switchToLogin();
|
||
}
|
||
} catch (e) {
|
||
switchToLogin();
|
||
}
|
||
})();
|
||
|
||
|
||
app.js muss sicherlich noch ausgebaut werden. Hier sind nur ein Bruchteil der relevanten
|
||
Pages verlinkt. Aber das kann später erfolgen.
|
||
|
||
== auth/auth.js
|
||
import express from "express";
|
||
import cookieParser from "cookie-parser";
|
||
import bcrypt from "bcrypt";
|
||
import fs from "fs";
|
||
import crypto from "crypto";
|
||
|
||
const USERS = JSON.parse(fs.readFileSync("./users.json"));
|
||
const SESSIONS = {}; // in-memory session store
|
||
|
||
const app = express();
|
||
app.use(express.json());
|
||
app.use(cookieParser());
|
||
|
||
app.post("/api/login", async (req,res)=>{
|
||
const { user, pass } = req.body;
|
||
|
||
console.log(`Auth-Service login attempt for ${user}`);
|
||
|
||
const hash = USERS[user];
|
||
if(!hash) return res.status(401).send({ ok:false });
|
||
|
||
const valid = await bcrypt.compare(pass, hash);
|
||
if(!valid) return res.status(401).send({ ok:false });
|
||
|
||
// create secure random session
|
||
const sessionID = crypto.randomBytes(32).toString("hex");
|
||
SESSIONS[sessionID] = {
|
||
user,
|
||
created: Date.now()
|
||
};
|
||
|
||
res.cookie("SESSIONID", sessionID, {
|
||
httpOnly: true,
|
||
secure: true,
|
||
domain: ".server.schooltech.ch",
|
||
sameSite: "None",
|
||
path: "/"
|
||
});
|
||
|
||
res.status(200).send({ ok:true });
|
||
});
|
||
|
||
// Logout endpoint
|
||
app.post("/api/logout", (req, res) => {
|
||
const sid = req.cookies.SESSIONID;
|
||
if (sid && SESSIONS[sid]) {
|
||
delete SESSIONS[sid];
|
||
}
|
||
// Cookie löschen
|
||
res.clearCookie("SESSIONID", {
|
||
httpOnly: true,
|
||
secure: true,
|
||
domain: ".server.schooltech.ch",
|
||
sameSite: "None",
|
||
path: "/"
|
||
});
|
||
return res.status(200).send({ ok: true });
|
||
});
|
||
|
||
// Event logging endpoint for frontend button presses
|
||
app.post('/api/event', (req,res)=>{
|
||
const svc = req.body.service || req.body.action || 'unknown';
|
||
const user = req.cookies.SESSIONID || 'anonymous';
|
||
|
||
console.log(`Event: user=${user} service=${svc} payload=${JSON.stringify(req.body)}`);
|
||
res.status(200).send({ ok:true });
|
||
});
|
||
|
||
// Optional für Nginx auth_request
|
||
app.get("/internal/auth", (req,res)=>{
|
||
const sid = req.cookies.SESSIONID;
|
||
if (sid && SESSIONS[sid]) {
|
||
return res.sendStatus(200);
|
||
}
|
||
return res.sendStatus(401);
|
||
});
|
||
|
||
// Status endpoint (unter /api so dass Nginx /api/ auf appserverauth proxyt)
|
||
app.get("/api/status", (req, res) => {
|
||
const sid = req.cookies.SESSIONID;
|
||
if (sid && SESSIONS[sid]) {
|
||
return res.status(200).send({ ok: true, user: SESSIONS[sid].user });
|
||
}
|
||
return res.status(401).send({ ok: false });
|
||
});
|
||
|
||
app.listen(3000, ()=>console.log("Auth-Service läuft auf 3000"));
|
||
|
||
|
||
|
||
==============
|
||
==============
|
||
|
||
Das läuft alles. Das ist leicht erweiterbar. Und soll eigentlich nicht geändert werden.
|
||
Aber ich brauche Authentifikation. User sollen sich einmal einloggen, wenn sei auf
|
||
die portalUI Index.html kommen. Das ist ja unter server.schooltech.ch erreichbar.
|
||
|
||
Danach soll für alle weiteren xyzABC.server.schooltech.ch Pages die Identifikation
|
||
akzeptiert werden.
|
||
|
||
Soweit ich das sehe, kann ich eine cookie basierte Identifikation auf server.schooltech.ch
|
||
laufen lassen, und dann später genau diese cookies in den einzelnen Pages überprüfen.
|
||
|
||
Fragen:
|
||
|
||
1) ist der aufbau für dich einigermassen klar? Stell Fragen falls etwas unklar ist
|
||
|
||
2) Wie kann ich die Page server.schooltech.ch Passwort-Schützen? Was muss ich einstellen,
|
||
dass per default nur die "Login" Option kommt, und sonst keine Links angezeigt werden?
|
||
|
||
|