Checkout Server bestätigung, Darkmode Checkout server
This commit is contained in:
1
.remember/tmp/save-session.pid
Normal file
1
.remember/tmp/save-session.pid
Normal file
@@ -0,0 +1 @@
|
||||
1252644
|
||||
360
SECURITY-AUDIT.md
Normal file
360
SECURITY-AUDIT.md
Normal file
@@ -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
|
||||
<button onclick="deleteUser(<%= u.id %>, '<%= u.username %>')">Löschen</button>
|
||||
```
|
||||
|
||||
**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
|
||||
<button class="btn-delete" data-id="<%= u.id %>" data-username="<%= u.username %>">Löschen</button>
|
||||
```
|
||||
|
||||
```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 = `
|
||||
<td>${auftrag}</td>
|
||||
<td>${proj}</td>
|
||||
<td>${such}</td>
|
||||
<td>${knd}</td>
|
||||
<td>${bez}</td>
|
||||
`;
|
||||
```
|
||||
|
||||
**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} <span class="${hoursClass}">${hoursDisplay}</span> – ${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
|
||||
@@ -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, () => {
|
||||
|
||||
110
doc/API-ROUTES.md
Normal file
110
doc/API-ROUTES.md
Normal file
@@ -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**.
|
||||
@@ -6,15 +6,40 @@
|
||||
<title><%= title %> - Stundenerfassung</title>
|
||||
<%- include('header') %>
|
||||
<style>
|
||||
:root {
|
||||
--ci-bg-from: #2c3e50;
|
||||
--ci-bg-to: #3498db;
|
||||
--ci-card-bg: #ffffff;
|
||||
--ci-card-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
||||
--ci-title: #2c3e50;
|
||||
--ci-text: #555;
|
||||
--ci-footer: rgba(255, 255, 255, 0.95);
|
||||
--ci-toggle-bg: #ffffff;
|
||||
--ci-toggle-text: #2c3e50;
|
||||
--ci-toggle-border: rgba(0,0,0,0.1);
|
||||
}
|
||||
[data-theme="dark"] {
|
||||
--ci-bg-from: #0f172a;
|
||||
--ci-bg-to: #1e293b;
|
||||
--ci-card-bg: #1e293b;
|
||||
--ci-card-shadow: 0 10px 40px rgba(0,0,0,0.5);
|
||||
--ci-title: #f1f5f9;
|
||||
--ci-text: #cbd5e1;
|
||||
--ci-footer: rgba(241, 245, 249, 0.85);
|
||||
--ci-toggle-bg: #1e293b;
|
||||
--ci-toggle-text: #f1f5f9;
|
||||
--ci-toggle-border: rgba(255,255,255,0.15);
|
||||
}
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
|
||||
background: linear-gradient(135deg, var(--ci-bg-from) 0%, var(--ci-bg-to) 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
color: #333;
|
||||
color: var(--ci-text);
|
||||
transition: background 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
.checkin-main {
|
||||
flex: 1;
|
||||
@@ -23,13 +48,14 @@
|
||||
justify-content: center;
|
||||
}
|
||||
.card {
|
||||
background: #fff;
|
||||
background: var(--ci-card-bg);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
||||
box-shadow: var(--ci-card-shadow);
|
||||
padding: 2.5rem;
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
.icon {
|
||||
font-size: 4rem;
|
||||
@@ -39,10 +65,10 @@
|
||||
.card h1 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
color: #2c3e50;
|
||||
color: var(--ci-title);
|
||||
}
|
||||
.card .message {
|
||||
color: #555;
|
||||
color: var(--ci-text);
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
@@ -50,10 +76,46 @@
|
||||
.card.success h1 { color: #27ae60; }
|
||||
.card.error .icon { color: #e74c3c; }
|
||||
.card.error h1 { color: #e74c3c; }
|
||||
.confirm-btn {
|
||||
display: inline-block;
|
||||
margin-top: 1.5rem;
|
||||
padding: 0.85rem 1.5rem;
|
||||
background: #e67e22;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.confirm-btn:hover { background: #d35400; }
|
||||
.app-footer {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
color: var(--ci-footer);
|
||||
}
|
||||
.theme-toggle-btn {
|
||||
border: 1px solid var(--ci-toggle-border);
|
||||
background: var(--ci-toggle-bg);
|
||||
color: var(--ci-toggle-text);
|
||||
padding: 8px 14px;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
.theme-toggle-btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 16px rgba(0,0,0,0.2);
|
||||
}
|
||||
.theme-toggle-floating {
|
||||
position: fixed;
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
z-index: 10000;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -63,6 +125,16 @@
|
||||
<div class="icon" aria-hidden="true"><%= success ? '✓' : '!' %></div>
|
||||
<h1><%= title %></h1>
|
||||
<p class="message"><%= message %></p>
|
||||
<% if (typeof confirmUrl !== 'undefined' && confirmUrl) { %>
|
||||
<% var _method = (typeof confirmMethod !== 'undefined' && confirmMethod) ? confirmMethod : 'GET'; %>
|
||||
<% if (_method.toUpperCase() === 'POST') { %>
|
||||
<form method="POST" action="<%= confirmUrl %>" style="margin:0;">
|
||||
<button type="submit" class="confirm-btn"><%= confirmLabel || 'Bestätigen' %></button>
|
||||
</form>
|
||||
<% } else { %>
|
||||
<a class="confirm-btn" href="<%= confirmUrl %>"><%= confirmLabel || 'Bestätigen' %></a>
|
||||
<% } %>
|
||||
<% } %>
|
||||
</div>
|
||||
</main>
|
||||
<%- include('footer') %>
|
||||
|
||||
Reference in New Issue
Block a user