== 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?


