diff --git a/.remember/tmp/save-session.pid b/.remember/tmp/save-session.pid new file mode 100644 index 0000000..a812a53 --- /dev/null +++ b/.remember/tmp/save-session.pid @@ -0,0 +1 @@ +1252644 diff --git a/SECURITY-AUDIT.md b/SECURITY-AUDIT.md new file mode 100644 index 0000000..330ccf2 --- /dev/null +++ b/SECURITY-AUDIT.md @@ -0,0 +1,360 @@ +# Security Audit Report: Stundenerfassung + +**Datum:** 2026-03-31 +**Geprüft von:** Claude Code (automatisierte Analyse) +**Scope:** Alle API-Routes, Middleware, Services, EJS-Templates, Checkin-Server + +--- + +## Zusammenfassung + +| Priorität | Anzahl | Beschreibung | +|-----------|--------|--------------| +| P0 - Vor Go-Live fixen | 4 | Session-Secret, Default-Credentials, Checkin ohne Auth, kein CSRF | +| P1 - Bald fixen | 4 | XSS (mehrere Stellen), Session-Cookie-Flags, kein Rate-Limiting | +| P2 - Sollte gefixt werden | 3 | Error-Message-Leaks, fehlende Security-Headers, JS-String-Injection | +| P3 - Nice to have | 1 | LDAP-Passwort unverschlüsselt in DB | + +--- + +## 1. SQL Injection + +### Ergebnis: KEINE Schwachstellen gefunden + +Alle SQLite-Queries verwenden konsequent parametrisierte Queries (`?` Platzhalter) in allen Route-Dateien: + +```js +db.get('SELECT * FROM users WHERE username = ? COLLATE NOCASE', [username], ...) +db.run('INSERT INTO users (...) VALUES (?, ?, ?, ...)', [values...], ...) +``` + +Die MSSQL-Projektsuche (`services/mssql-infra-service.js:53`) verwendet ebenfalls korrekt parametrisierte Eingaben: + +```js +request.input('search', sql.NVarChar, `%${searchTerm.toUpperCase()}%`); +``` + +Die dynamischen `IN (${placeholders})`-Konstruktionen in `routes/admin-routes.js:475` und `routes/verwaltung-routes.js:1108` sind sicher, da die Platzhalter aus `userIds.map(() => '?')` generiert werden. + +--- + +## 2. XSS (Cross-Site Scripting) + +### 2.1 CRITICAL: onclick-Handler-Injection in admin.ejs + +**Datei:** `views/admin.ejs` (ca. Zeile 213) + +```html + +``` + +**Problem:** Ein Username wie `'); alert(document.cookie);//` bricht aus dem String aus und führt JavaScript im Admin-Kontext aus. + +**Fix:** `onclick`-Handler durch Event-Listener ersetzen und Daten über `data-*`-Attribute übergeben: + +```html + +``` + +```js +document.querySelectorAll('.btn-delete').forEach(btn => { + btn.addEventListener('click', () => { + deleteUser(btn.dataset.id, btn.dataset.username); + }); +}); +``` + +--- + +### 2.2 HIGH: innerHTML mit unescapten Daten (4 Stellen) + +#### a) `views/project-search.ejs` (ca. Zeile 107-114) + +```js +tr.innerHTML = ` + ${auftrag} + ${proj} + ${such} + ${knd} + ${bez} +`; +``` + +**Problem:** MSSQL-Suchergebnisse werden direkt in innerHTML eingefügt. Falls die externe Datenbank manipulierte Daten enthält, wird HTML/JS ausgeführt. + +**Fix:** `textContent` statt `innerHTML` verwenden: + +```js +const tr = document.createElement('tr'); +[auftrag, proj, such, knd, bez].forEach(val => { + const td = document.createElement('td'); + td.textContent = val; + tr.appendChild(td); +}); +``` + +#### b) `views/verwaltung.ejs` (ca. Zeile 871-876) + +```js +li.innerHTML = `Korrektur am ${dateText} ${hoursDisplay} – ${reason}`; +``` + +**Problem:** Das `reason`-Feld aus der Tabelle `overtime_corrections` wird ohne Escaping eingefügt. Jeder Verwaltungs-User kann beim Speichern einer Korrektur HTML/JS injizieren. + +**Fix:** `reason` mit einer Escape-Funktion behandeln oder `textContent` verwenden. + +#### c) `views/overtime-breakdown.ejs` (ca. Zeile 516-520) + +Identisches Problem wie b) - gleiches `reason`-Feld, gleiche innerHTML-Nutzung. + +#### d) `views/project-search.ejs` - data-Attribut + +```js +data-auftrag="${auftrag}" +``` + +**Problem:** Unescapte Daten in HTML-Attributen können aus dem Attribut ausbrechen. + +--- + +### 2.3 MEDIUM: JavaScript-String-Literal-Injection + +**Datei:** `views/verwaltung.ejs` (ca. Zeile 337) + +```js +const currentUser = '<%= user.firstname %> <%= user.lastname %>'; +``` + +**Problem:** EJS `<%=` escaped HTML-Entities (`<`, `>`, `&`, `"`), aber NICHT einfache Anführungszeichen (`'`) oder Backslashes in JS-Kontext. Ein Name mit `'` bricht den String. + +**Fix:** `JSON.stringify()` verwenden: + +```js +const currentUser = <%- JSON.stringify((user.firstname || '') + ' ' + (user.lastname || '')) %>; +``` + +--- + +## 3. Authentication & Session Security + +### 3.1 CRITICAL (P0): Hardcoded Session Secret + +**Datei:** `server.js:33` + +```js +secret: 'stundenerfassung-geheim-2024' +``` + +**Problem:** Jeder, der den Quellcode kennt, kann Session-Cookies fälschen und sich als beliebiger User ausgeben. + +**Fix:** Umgebungsvariable verwenden: + +```js +secret: process.env.SESSION_SECRET || require('crypto').randomBytes(64).toString('hex') +``` + +Und in der Produktionsumgebung `SESSION_SECRET` als Environment-Variable setzen. + +--- + +### 3.2 CRITICAL (P0): Default-Credentials + +**Datei:** `server.js:91-92` (und `database.js` bei der Initialisierung) + +``` +Admin: admin / admin123 +Verwaltung: verwaltung / verwaltung123 +``` + +**Problem:** Falls diese Accounts noch mit den Standard-Passwörtern existieren, kann jeder sich als Admin anmelden. + +**Fix:** +- Standard-Passwörter nach erster Anmeldung erzwingen zu ändern +- Oder: Standard-Accounts bei Produktion entfernen / Passwort aus Env-Variable lesen +- Mindestens: Konsolenausgabe der Credentials in Produktion deaktivieren + +--- + +### 3.3 CRITICAL (P0): Kein CSRF-Schutz + +**Betrifft:** Alle POST/PUT/DELETE-Routen + +**Problem:** Keine CSRF-Tokens auf Formularen oder API-Aufrufen. Ein Angreifer kann eine bösartige Webseite erstellen, die im Namen eines eingeloggten Admins Aktionen ausführt (User erstellen, Timesheets löschen, Einstellungen ändern). + +**Fix:** CSRF-Middleware installieren: + +```bash +npm install csurf +``` + +```js +const csrf = require('csurf'); +app.use(csrf()); +``` + +Oder alternativ `SameSite=Strict` auf dem Session-Cookie setzen (einfacher, aber weniger robust). + +--- + +### 3.4 HIGH (P1): Session-Cookie ohne Sicherheits-Flags + +**Datei:** `server.js:33-37` + +```js +cookie: { maxAge: 24 * 60 * 60 * 1000 } +``` + +**Problem:** Fehlende Flags: +- `httpOnly: true` - Verhindert JS-Zugriff auf Cookie (wichtig gegen XSS) +- `secure: true` - Cookie nur über HTTPS senden +- `sameSite: 'strict'` - Schutz gegen CSRF + +**Fix:** + +```js +cookie: { + maxAge: 24 * 60 * 60 * 1000, + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict' +} +``` + +--- + +### 3.5 HIGH (P1): Kein Rate-Limiting auf Login + +**Datei:** `routes/auth-routes.js` - POST `/login` + +**Problem:** Unbegrenzte Login-Versuche ermöglichen Brute-Force-Angriffe auf Passwörter. + +**Fix:** + +```bash +npm install express-rate-limit +``` + +```js +const rateLimit = require('express-rate-limit'); +const loginLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 Minuten + max: 10, // Max 10 Versuche + message: { error: 'Zu viele Anmeldeversuche. Bitte in 15 Minuten erneut versuchen.' } +}); +app.post('/login', loginLimiter, (req, res) => { ... }); +``` + +--- + +## 4. Checkin-Server (Port 3334) + +### CRITICAL (P0): Komplett unauthentifizierte Endpoints + +**Datei:** `checkin-server.js:44,110` + +``` +GET /api/checkin/:userId +GET /api/checkout/:userId +``` + +**Problem:** JEDER, der eine User-ID kennt (oder errät), kann beliebige Mitarbeiter ein- und auschecken. User-IDs sind sequentielle Integers ab 1 - Enumeration ist trivial. + +**Fix-Optionen:** +1. **Token-basiert:** QR-Codes enthalten einen signierten Token statt nur die User-ID +2. **HMAC-Signatur:** `/api/checkin/:userId?token=HMAC(userId, secret)` - Server validiert Token +3. **IP-Beschränkung:** Checkin-Server nur aus dem internen Netzwerk erreichbar machen (Firewall) +4. **Mindestens:** UUID statt sequentieller IDs für den Checkin-Endpunkt verwenden + +--- + +## 5. Information Disclosure + +### 5.1 HIGH (P2): Fehlermeldungen leaken interne Details + +**Datei:** `routes/admin-routes.js:334` + +```js +error: 'Verbindung zur MSSQL-Datenbank fehlgeschlagen: ' + err.message +``` + +**Datei:** `routes/timesheet-routes.js:248` + +```js +error: 'Fehler beim Speichern: ' + err.message +``` + +**Problem:** MSSQL-Fehlermeldungen können Servernamen, IP-Adressen und Datenbankstruktur leaken. SQLite-Fehler können Tabellen-/Spaltennamen verraten. + +**Fix:** Generische Fehlermeldungen an den Client senden, Details nur serverseitig loggen: + +```js +console.error('MSSQL Fehler:', err); +return res.status(500).json({ error: 'Datenbankverbindung fehlgeschlagen' }); +``` + +--- + +### 5.2 MEDIUM (P3): LDAP-Bind-Passwort unverschlüsselt in DB + +**Tabelle:** `ldap_config.bind_password` + +**Problem:** Das LDAP-Bind-Passwort wird im Klartext in der SQLite-Datenbank gespeichert. Der GET-Endpoint (`routes/admin-ldap-routes.js:18`) entfernt es zwar aus der Antwort, aber es liegt im Klartext in der DB-Datei. + +**Fix:** Passwort verschlüsselt speichern (z.B. mit `crypto.createCipheriv`) und bei Bedarf entschlüsseln. + +--- + +## 6. Fehlende Security-Headers + +### P2: Keine globalen Sicherheits-Header + +**Datei:** `server.js` + +**Fehlende Header:** +- `X-Content-Type-Options: nosniff` +- `X-Frame-Options: DENY` +- `Content-Security-Policy` +- `Strict-Transport-Security` (HSTS) +- `Referrer-Policy: strict-origin-when-cross-origin` + +**Fix:** `helmet` Middleware installieren: + +```bash +npm install helmet +``` + +```js +const helmet = require('helmet'); +app.use(helmet()); +``` + +Das setzt automatisch alle empfohlenen Security-Headers. + +--- + +## 7. Positiv aufgefallen + +- Alle SQL-Queries parametrisiert (kein SQL-Injection-Risiko) +- PDF-Pfad-Traversal korrekt verhindert (`resolveWithinBaseDir()`, Regex-Validierung) +- Passwörter mit bcrypt gehasht +- Rollen-System korrekt implementiert (requireAuth, requireAdmin, requireVerwaltung) +- EJS `<%=` (escaped) wird konsequent statt `<%-` (unescaped) verwendet +- LDAP-Passwort wird im GET-Endpoint korrekt entfernt +- `X-Content-Type-Options: nosniff` wird auf PDF-Responses gesetzt + +--- + +## Checkliste zum Abarbeiten + +- [ ] **P0** Session-Secret aus Env-Variable lesen (`server.js`) +- [ ] **P0** Default-Credentials entfernen oder Passwort-Änderung erzwingen +- [ ] **P0** Checkin-Server absichern (Token/HMAC/IP-Restriction) +- [ ] **P0** CSRF-Schutz hinzufügen (csurf oder SameSite-Cookie) +- [ ] **P1** XSS in admin.ejs fixen (onclick -> addEventListener) +- [ ] **P1** innerHTML durch textContent/DOM-Methoden ersetzen (4 Stellen) +- [ ] **P1** Session-Cookie-Flags setzen (httpOnly, secure, sameSite) +- [ ] **P1** Rate-Limiting auf Login-Route +- [ ] **P2** Fehlermeldungen generisch machen (keine err.message an Client) +- [ ] **P2** Security-Headers via helmet setzen +- [ ] **P2** JS-String-Literal-Injection fixen (JSON.stringify) +- [ ] **P3** LDAP-Passwort verschlüsselt speichern diff --git a/checkin-server.js b/checkin-server.js index e9c387a..8ba690e 100644 --- a/checkin-server.js +++ b/checkin-server.js @@ -30,7 +30,14 @@ function sendResponse(req, res, success, data) { if (!success && data.status) { res.status(data.status); } - res.render('checkin-result', { success, title, message }); + res.render('checkin-result', { + success, + title, + message, + confirmUrl: data.confirmUrl || null, + confirmLabel: data.confirmLabel || null, + confirmMethod: data.confirmMethod || 'GET' + }); } else { if (success) { res.json({ success: true, ...data }); @@ -106,8 +113,13 @@ checkinApp.get('/api/checkin/:userId', (req, res) => { }); }); -// API: Check-out (Gehen) -checkinApp.get('/api/checkout/:userId', (req, res) => { +// API: Check-out (Gehen) - GET und POST (POST für force-Bestätigung) +checkinApp.post('/api/checkout/:userId', express.urlencoded({ extended: false }), (req, res) => { + handleCheckout(req, res, true); +}); +checkinApp.get('/api/checkout/:userId', (req, res) => handleCheckout(req, res, false)); + +function handleCheckout(req, res, force) { const userId = parseInt(req.params.userId); const currentDate = getCurrentDate(); const currentTime = getCurrentTime(); @@ -131,7 +143,28 @@ checkinApp.get('/api/checkout/:userId', (req, res) => { status: 400 }); } - + + // Wenn bereits eine End-Zeit existiert und nicht erzwungen wird → Bestätigung anfordern + if (entry.end_time && !force) { + if (wantsHtml(req)) { + return res.render('checkin-result', { + success: true, + title: 'Bereits ausgecheckt', + message: `Du wurdest heute bereits um ${entry.end_time} ausgecheckt. Möchtest du die End-Zeit auf ${currentTime} überschreiben?`, + confirmUrl: `/api/checkout/${userId}`, + confirmMethod: 'POST', + confirmLabel: 'Ja, End-Zeit überschreiben' + }); + } + return res.status(409).json({ + success: false, + error: 'Bereits ausgecheckt', + existing_end_time: entry.end_time, + confirm_url: `/api/checkout/${userId}`, + confirm_method: 'POST' + }); + } + // Berechne total_hours basierend auf start_time, end_time und break_minutes const breakMinutes = entry.break_minutes || 0; const totalHours = updateTotalHours(entry.start_time, currentTime, breakMinutes); @@ -154,7 +187,7 @@ checkinApp.get('/api/checkout/:userId', (req, res) => { }); }); }); -}); +} // Check-in-Server starten (auf Port 3334) checkinApp.listen(CHECKIN_PORT, () => { diff --git a/doc/API-ROUTES.md b/doc/API-ROUTES.md new file mode 100644 index 0000000..0db22b3 --- /dev/null +++ b/doc/API-ROUTES.md @@ -0,0 +1,110 @@ +# API Routen + +Übersicht aller HTTP-Routen der Anwendung. Die Registrierung erfolgt in [server.js](../server.js). + +Auth-Kürzel: **A** = `requireAuth`, **V** = `requireVerwaltung`, **AD** = `requireAdmin`, — = öffentlich. + +## Root & Auth ([routes/auth-routes.js](../routes/auth-routes.js)) + +| Methode | Pfad | Auth | Quelle | +|---|---|---|---| +| GET | `/` | — | [server.js:67](../server.js#L67) | +| GET | `/login` | — | [auth-routes.js:94](../routes/auth-routes.js#L94) | +| POST | `/login` | — | [auth-routes.js:99](../routes/auth-routes.js#L99) | +| GET | `/logout` | — | [auth-routes.js:178](../routes/auth-routes.js#L178) | + +## Dashboard ([routes/dashboard-routes.js](../routes/dashboard-routes.js)) + +| Methode | Pfad | Auth | Quelle | +|---|---|---|---| +| GET | `/dashboard` | A | [dashboard-routes.js:39](../routes/dashboard-routes.js#L39) | +| GET | `/overtime-breakdown` | A | [dashboard-routes.js:65](../routes/dashboard-routes.js#L65) | +| GET | `/api/checkin-root-url` | — | [dashboard-routes.js:11](../routes/dashboard-routes.js#L11) | +| GET | `/api/dashboard/qr-pdf/internal` | A | [dashboard-routes.js:23](../routes/dashboard-routes.js#L23) | +| GET | `/api/dashboard/qr-pdf/external` | A | [dashboard-routes.js:31](../routes/dashboard-routes.js#L31) | + +## User ([routes/user-routes.js](../routes/user-routes.js)) + +| Methode | Pfad | Auth | Quelle | +|---|---|---|---| +| GET | `/api/user/last-week` | A | [user-routes.js:11](../routes/user-routes.js#L11) | +| POST | `/api/user/last-week` | A | [user-routes.js:24](../routes/user-routes.js#L24) | +| GET | `/api/user/weekend-percentages` | A | [user-routes.js:43](../routes/user-routes.js#L43) | +| GET | `/api/user/data` | A | [user-routes.js:57](../routes/user-routes.js#L57) | +| GET | `/api/user/client-ip` | A | [user-routes.js:74](../routes/user-routes.js#L74) | +| GET | `/api/user/ping-ip` | A | [user-routes.js:90](../routes/user-routes.js#L90) | +| POST | `/api/user/ping-ip` | A | [user-routes.js:103](../routes/user-routes.js#L103) | +| GET | `/api/user/project-search-enabled` | A | [user-routes.js:134](../routes/user-routes.js#L134) | +| POST | `/api/user/project-search-enabled` | A | [user-routes.js:152](../routes/user-routes.js#L152) | +| POST | `/api/user/switch-role` | A | [user-routes.js:178](../routes/user-routes.js#L178) | +| GET | `/api/user/planned-vacation` | A | [user-routes.js:203](../routes/user-routes.js#L203) | +| GET | `/api/user/stats` | A | [user-routes.js:274](../routes/user-routes.js#L274) | +| GET | `/api/user/overtime-breakdown` | A | [user-routes.js:571](../routes/user-routes.js#L571) | + +## Timesheet ([routes/timesheet-routes.js](../routes/timesheet-routes.js)) + +| Methode | Pfad | Auth | Quelle | +|---|---|---|---| +| POST | `/api/timesheet/save` | A | [timesheet-routes.js:42](../routes/timesheet-routes.js#L42) | +| GET | `/api/timesheet/holidays` | A | [timesheet-routes.js:289](../routes/timesheet-routes.js#L289) | +| GET | `/api/timesheet/week/:weekStart` | A | [timesheet-routes.js:300](../routes/timesheet-routes.js#L300) | +| POST | `/api/timesheet/submit` | A | [timesheet-routes.js:338](../routes/timesheet-routes.js#L338) | +| GET | `/api/timesheet/latest-submitted/:weekStart` | A | [timesheet-routes.js:531](../routes/timesheet-routes.js#L531) | +| GET | `/api/timesheet/download-info/:id` | V | [timesheet-routes.js:565](../routes/timesheet-routes.js#L565) | +| GET | `/api/timesheet/pdf/:id` | A | [timesheet-routes.js:594](../routes/timesheet-routes.js#L594) | + +## Projektsuche ([routes/project-search-routes.js](../routes/project-search-routes.js)) + +| Methode | Pfad | Auth | Quelle | +|---|---|---|---| +| GET | `/projects/search` | A | [project-search-routes.js:6](../routes/project-search-routes.js#L6) | +| GET | `/api/projects/search` | A | [project-search-routes.js:21](../routes/project-search-routes.js#L21) | + +## Verwaltung ([routes/verwaltung-routes.js](../routes/verwaltung-routes.js)) + +| Methode | Pfad | Auth | Quelle | +|---|---|---|---| +| GET | `/verwaltung` | V | [verwaltung-routes.js:26](../routes/verwaltung-routes.js#L26) | +| GET | `/verwaltung/projektauswertung` | V | [verwaltung-routes.js:276](../routes/verwaltung-routes.js#L276) | +| GET | `/api/verwaltung/employee/:id/weeks` | V | [verwaltung-routes.js:154](../routes/verwaltung-routes.js#L154) | +| PUT | `/api/verwaltung/user/:id/overtime-offset` | V | [verwaltung-routes.js:502](../routes/verwaltung-routes.js#L502) | +| PUT | `/api/verwaltung/user/:id/vacation-offset` | V | [verwaltung-routes.js:571](../routes/verwaltung-routes.js#L571) | +| GET | `/api/verwaltung/user/:id/overtime-corrections` | V | [verwaltung-routes.js:591](../routes/verwaltung-routes.js#L591) | +| GET | `/api/verwaltung/user/:id/sick-days` | V | [verwaltung-routes.js:611](../routes/verwaltung-routes.js#L611) | +| GET | `/api/verwaltung/user/:id/stats` | V | [verwaltung-routes.js:663](../routes/verwaltung-routes.js#L663) | +| GET | `/api/verwaltung/employees/current-overtime` | V | [verwaltung-routes.js:634](../routes/verwaltung-routes.js#L634) | +| PUT | `/api/verwaltung/timesheet/:id/comment` | V | [verwaltung-routes.js:1003](../routes/verwaltung-routes.js#L1003) | +| GET | `/api/verwaltung/bulk-download/:year/:week` | V | [verwaltung-routes.js:1019](../routes/verwaltung-routes.js#L1019) | + +## Admin ([routes/admin-routes.js](../routes/admin-routes.js)) + +| Methode | Pfad | Auth | Quelle | +|---|---|---|---| +| GET | `/admin` | AD | [admin-routes.js:26](../routes/admin-routes.js#L26) | +| POST | `/admin/users` | AD | [admin-routes.js:71](../routes/admin-routes.js#L71) | +| PUT | `/admin/users/:id` | AD | [admin-routes.js:127](../routes/admin-routes.js#L127) | +| DELETE | `/admin/users/:id` | AD | [admin-routes.js:110](../routes/admin-routes.js#L110) | +| GET | `/admin/options` | AD | [admin-routes.js:188](../routes/admin-routes.js#L188) | +| POST | `/admin/options` | AD | [admin-routes.js:208](../routes/admin-routes.js#L208) | +| GET | `/admin/mssql-config` | AD | [admin-routes.js:260](../routes/admin-routes.js#L260) | +| POST | `/admin/mssql-config` | AD | [admin-routes.js:279](../routes/admin-routes.js#L279) | +| POST | `/admin/mssql-test-connection` | AD | [admin-routes.js:328](../routes/admin-routes.js#L328) | +| GET | `/admin/api/timesheet-duplicates` | AD | [admin-routes.js:342](../routes/admin-routes.js#L342) | +| DELETE | `/admin/api/timesheet-entry/:id` | AD | [admin-routes.js:410](../routes/admin-routes.js#L410) | +| GET | `/admin/api/pdfs/years` | AD | [admin-routes.js:436](../routes/admin-routes.js#L436) | +| GET | `/admin/api/pdfs/users` | AD | [admin-routes.js:452](../routes/admin-routes.js#L452) | +| GET | `/admin/api/pdfs/files` | AD | [admin-routes.js:504](../routes/admin-routes.js#L504) | +| GET | `/admin/api/pdfs/file` | AD | [admin-routes.js:539](../routes/admin-routes.js#L539) | + +## Admin LDAP ([routes/admin-ldap-routes.js](../routes/admin-ldap-routes.js)) + +| Methode | Pfad | Auth | Quelle | +|---|---|---|---| +| GET | `/admin/ldap/config` | AD | [admin-ldap-routes.js:10](../routes/admin-ldap-routes.js#L10) | +| POST | `/admin/ldap/config` | AD | [admin-ldap-routes.js:26](../routes/admin-ldap-routes.js#L26) | +| POST | `/admin/ldap/sync` | AD | [admin-ldap-routes.js:134](../routes/admin-ldap-routes.js#L134) | +| GET | `/admin/ldap/sync/log` | AD | [admin-ldap-routes.js:152](../routes/admin-ldap-routes.js#L152) | + +## Hinweis: Check-in-Server + +Zusätzlich läuft ein separater Server auf Port **3334** ([checkin-server.js](../checkin-server.js)), eingebunden in [server.js:104](../server.js#L104). Hauptserver läuft auf Port **3333**. diff --git a/views/checkin-result.ejs b/views/checkin-result.ejs index e5f2360..40952f5 100644 --- a/views/checkin-result.ejs +++ b/views/checkin-result.ejs @@ -6,15 +6,40 @@ <%= title %> - Stundenerfassung <%- include('header') %> @@ -63,6 +125,16 @@

<%= title %>

<%= message %>

+ <% if (typeof confirmUrl !== 'undefined' && confirmUrl) { %> + <% var _method = (typeof confirmMethod !== 'undefined' && confirmMethod) ? confirmMethod : 'GET'; %> + <% if (_method.toUpperCase() === 'POST') { %> +
+ +
+ <% } else { %> + <%= confirmLabel || 'Bestätigen' %> + <% } %> + <% } %> <%- include('footer') %>