Add Anonymous User Identification — Browser-ID System
525
Anonymous User Identification %E2%80%94 Browser-ID System.-.md
Normal file
525
Anonymous User Identification %E2%80%94 Browser-ID System.-.md
Normal file
@@ -0,0 +1,525 @@
|
||||
# Anonymous User Identification — Browser-ID System
|
||||
|
||||
## Übersicht
|
||||
|
||||
Das Bürgerbeteiligungsportal identifiziert Nutzer anonym über eine **Browser-ID** (UUID), die als Cookie gespeichert wird. Dieses System ermöglicht niedrigschwellige Bürgerbeteiligung ohne Registrierung oder Passwort, während gleichzeitig Beiträge und Abstimmungen zuverlässig einem Browser zugeordnet werden können.
|
||||
|
||||
---
|
||||
|
||||
## Architektur-Entscheidung
|
||||
|
||||
### Problem
|
||||
|
||||
- Bürger sollen ohne Registrierung Beiträge erstellen und abstimmen können.
|
||||
- Nur der eingegebene Name reicht nicht zur Identifikation — jeder könnte sich als jemand anderes ausgeben und fremde Beiträge bearbeiten oder löschen.
|
||||
- Abstimmungen (Likes/Dislikes) müssen pro Nutzer eindeutig sein, auch wenn mehrere Personen denselben Namen eingeben.
|
||||
- Abstimmungen sollen nach dem Neuladen der Seite sichtbar bleiben (farbliche Markierung der Buttons).
|
||||
|
||||
### Lösung: Zwei-Schicht-Identifikation
|
||||
|
||||
| Schicht | Zweck | Speicherort |
|
||||
|---------|-------|-------------|
|
||||
| **Browser-ID (UUID)** | Technische Identifikation für Rechte und Abstimmungen | Cookie (`webgis_browser_id`, 1 Jahr gültig) |
|
||||
| **Benutzername** | Anzeigename in Beiträgen und Abstimmungen | sessionStorage (`webgis_user`) |
|
||||
|
||||
### Rollen
|
||||
|
||||
| Rolle | Zugang | Rechte |
|
||||
|-------|--------|--------|
|
||||
| **Bürger** | Name eingeben (freiwillig) | Beiträge erstellen, abstimmen, eigene Beiträge bearbeiten/löschen |
|
||||
| **Moderator/Admin** | Passwort-Login über Lock-Icon im Header | Alle Beiträge moderieren, bearbeiten, löschen, News verwalten |
|
||||
|
||||
---
|
||||
|
||||
## Betroffene Dateien
|
||||
|
||||
| Datei | Änderung |
|
||||
|-------|----------|
|
||||
| `migrations/005_browser_id.sql` | Neue Spalten und Constraints in der Datenbank |
|
||||
| `public/js/app.js` | Browser-ID generieren, bei API-Calls mitsenden, Rechte prüfen |
|
||||
| `public/index.php` | `IS_ADMIN`-Variable für JavaScript bereitstellen |
|
||||
| `api/contributions.php` | Browser-ID bei Create, Vote und Read verarbeiten |
|
||||
|
||||
---
|
||||
|
||||
## Datenbankänderungen (Migration 005)
|
||||
|
||||
### Neue Spalten
|
||||
|
||||
```sql
|
||||
-- Speichert die Browser-ID des Erstellers eines Beitrags
|
||||
ALTER TABLE contributions
|
||||
ADD COLUMN browser_id VARCHAR(36) DEFAULT NULL;
|
||||
|
||||
-- Speichert die Browser-ID des Abstimmenden
|
||||
ALTER TABLE votes
|
||||
ADD COLUMN browser_id VARCHAR(36) DEFAULT NULL;
|
||||
```
|
||||
|
||||
### Neuer Index
|
||||
|
||||
```sql
|
||||
-- Beschleunigt die Suche nach Abstimmungen eines bestimmten Browsers
|
||||
CREATE INDEX idx_votes_browser ON votes(browser_id);
|
||||
```
|
||||
|
||||
### Constraint-Änderung
|
||||
|
||||
```sql
|
||||
-- VORHER: Ein Vote pro voter_name + contribution_id
|
||||
-- → Problem: Zwei verschiedene Browser mit gleichem Namen
|
||||
-- konnten nicht unabhängig abstimmen.
|
||||
|
||||
-- Entfernt den alten Constraint (Name-basiert)
|
||||
ALTER TABLE votes
|
||||
DROP CONSTRAINT IF EXISTS votes_unique_per_voter;
|
||||
|
||||
-- NACHHER: Ein Vote pro browser_id + contribution_id
|
||||
-- → Jeder Browser kann genau einmal pro Beitrag abstimmen,
|
||||
-- unabhängig vom eingegebenen Namen.
|
||||
ALTER TABLE votes
|
||||
ADD CONSTRAINT votes_contribution_browser_unique
|
||||
UNIQUE (contribution_id, browser_id);
|
||||
```
|
||||
|
||||
### Auswirkung auf bestehende Daten
|
||||
|
||||
- Bestehende Beiträge erhalten `browser_id = NULL`. Diese Beiträge können von niemandem über das Portal bearbeitet werden (nur über das Moderationsportal).
|
||||
- Bestehende Votes erhalten `browser_id = NULL`. Diese Votes werden beim Neuladen nicht als farbliche Markierung angezeigt, bleiben aber in der Datenbank und zählen weiterhin.
|
||||
|
||||
---
|
||||
|
||||
## JavaScript-Änderungen (app.js)
|
||||
|
||||
### Block 1: Browser-ID generieren
|
||||
|
||||
```javascript
|
||||
// Browser Identification Number for anonymous User Identification stored as Cookie
|
||||
let browserId = getBrowserId();
|
||||
|
||||
function getBrowserId() {
|
||||
// Versucht, bestehende Browser-ID aus Cookie zu lesen
|
||||
let id = document.cookie.replace(
|
||||
/(?:(?:^|.*;\s*)webgis_browser_id\s*=\s*([^;]*).*$)|^.*$/, '$1'
|
||||
);
|
||||
|
||||
// Falls keine ID vorhanden, wird eine neue UUID v4 generiert
|
||||
if (!id) {
|
||||
id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
const r = Math.random() * 16 | 0;
|
||||
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
|
||||
});
|
||||
// Cookie wird für ein Jahr gespeichert
|
||||
document.cookie = 'webgis_browser_id=' + id + ';path=/;max-age=31536000;SameSite=Lax';
|
||||
}
|
||||
return id;
|
||||
}
|
||||
```
|
||||
|
||||
**Wie es funktioniert:**
|
||||
|
||||
1. Beim ersten Seitenaufruf existiert kein Cookie → neue UUID wird generiert (z.B. `b5f326e1-8c83-4780-99a6-c98c6586ec07`).
|
||||
2. Die UUID wird als Cookie `webgis_browser_id` gespeichert (1 Jahr Laufzeit).
|
||||
3. Bei jedem weiteren Aufruf wird die bestehende UUID aus dem Cookie gelesen.
|
||||
4. Die UUID ist pro Browser einzigartig — verschiedene Browser auf demselben Computer haben verschiedene IDs.
|
||||
|
||||
### Block 7: Browser-ID bei Read mitsenden
|
||||
|
||||
```javascript
|
||||
function loadContributions() {
|
||||
const readParams = { action: 'read', municipality_id: MUNICIPALITY.id };
|
||||
|
||||
// Sendet Browser-ID, damit die API die eigenen Votes zurückgeben kann
|
||||
readParams.browser_id = browserId;
|
||||
|
||||
apiCall(readParams, function (data) {
|
||||
// ...
|
||||
contributionsData = data.features || [];
|
||||
|
||||
// Stellt die Vote-Markierungen aus der API-Antwort wieder her
|
||||
if (data.user_votes) {
|
||||
userVotes = {};
|
||||
for (const key in data.user_votes) {
|
||||
userVotes[key] = data.user_votes[key];
|
||||
}
|
||||
}
|
||||
// ...
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Wie es funktioniert:**
|
||||
|
||||
1. Beim Laden der Beiträge wird die `browser_id` an die API gesendet.
|
||||
2. Die API sucht alle Votes dieser Browser-ID und gibt sie als `user_votes`-Objekt zurück.
|
||||
3. Das `userVotes`-Objekt wird befüllt (z.B. `{ "2": "like", "5": "dislike" }`).
|
||||
4. Wenn ein Popup geöffnet wird, prüft `buildPopupHtml()` dieses Objekt und setzt die CSS-Klassen `liked` / `disliked` auf die Buttons.
|
||||
|
||||
### Block 9: Edit/Delete nur für eigenen Browser oder Admin
|
||||
|
||||
```javascript
|
||||
// VORHER: Prüfte nur den Benutzernamen
|
||||
(currentUser === props.author_name ? /* Buttons anzeigen */ : '')
|
||||
|
||||
// NACHHER: Prüft die Browser-ID oder Admin-Status
|
||||
(props.browser_id === browserId || (typeof IS_ADMIN !== 'undefined' && IS_ADMIN) ? /* Buttons anzeigen */ : '')
|
||||
```
|
||||
|
||||
**Wie es funktioniert:**
|
||||
|
||||
- `props.browser_id` ist die Browser-ID, die beim Erstellen des Beitrags gespeichert wurde.
|
||||
- `browserId` ist die aktuelle Browser-ID des Betrachters.
|
||||
- Stimmen sie überein → der Betrachter ist der Ersteller → Bearbeiten/Löschen-Buttons werden angezeigt.
|
||||
- `IS_ADMIN` ist `true`, wenn der Nutzer über das Moderationsportal als Admin eingeloggt ist → sieht die Buttons bei allen Beiträgen.
|
||||
|
||||
### Block 10: Browser-ID bei Create und Vote mitsenden
|
||||
|
||||
```javascript
|
||||
// CREATE — Browser-ID wird mit dem Beitrag gespeichert
|
||||
apiCall({
|
||||
action: 'create',
|
||||
// ...
|
||||
author_name: currentUser,
|
||||
browser_id: browserId // ← NEU
|
||||
}, function (response) { /* ... */ });
|
||||
|
||||
// VOTE — Browser-ID identifiziert den Abstimmenden
|
||||
apiCall({
|
||||
action: 'vote',
|
||||
contribution_id: contributionId,
|
||||
voter_name: currentUser,
|
||||
vote_type: voteType,
|
||||
browser_id: browserId // ← NEU
|
||||
}, function (response) { /* ... */ });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PHP-Änderungen (index.php)
|
||||
|
||||
### Admin-Status als JavaScript-Variable
|
||||
|
||||
```php
|
||||
<!-- Benötigt auth.php für die is_admin()-Funktion -->
|
||||
<?php
|
||||
require_once __DIR__ . '/api/db.php';
|
||||
require_once __DIR__ . '/api/auth.php';
|
||||
?>
|
||||
|
||||
<script>
|
||||
// Wird von app.js genutzt, um Edit/Delete-Buttons bei allen Beiträgen anzuzeigen
|
||||
const IS_ADMIN = <?= (function_exists('is_admin') && is_admin()) ? 'true' : 'false' ?>;
|
||||
</script>
|
||||
```
|
||||
|
||||
**Wie es funktioniert:**
|
||||
|
||||
- `is_admin()` prüft `$_SESSION['is_admin']`.
|
||||
- Die Session wird gesetzt, wenn sich jemand über `admin.php` mit dem korrekten Passwort anmeldet.
|
||||
- Da `index.php` und `admin.php` dieselbe Session teilen, erkennt das Portal einen eingeloggten Moderator.
|
||||
|
||||
---
|
||||
|
||||
## API-Änderungen (contributions.php)
|
||||
|
||||
### handle_create — Speichert Browser-ID
|
||||
|
||||
```php
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO contributions
|
||||
(municipality_id, geom, geom_type, category, title, description, author_name, browser_id)
|
||||
VALUES
|
||||
(:mid, ST_SetSRID(ST_GeomFromGeoJSON(:geom), 4326), :geom_type,
|
||||
:category, :title, :description, :author_name, :browser_id)
|
||||
");
|
||||
|
||||
$stmt->execute([
|
||||
// ...
|
||||
':browser_id' => $input['browser_id'] ?? null // ← NEU
|
||||
]);
|
||||
```
|
||||
|
||||
### handle_vote — Identifiziert über Browser-ID
|
||||
|
||||
```php
|
||||
// Prüft ob Browser-ID vorhanden ist (Pflichtfeld für Voting)
|
||||
$browser_id = $input['browser_id'] ?? '';
|
||||
if (empty($browser_id)) {
|
||||
error_response('Browser ID required for Voting.');
|
||||
}
|
||||
|
||||
// Sucht bestehenden Vote über Browser-ID statt über Name
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT vote_id, vote_type FROM votes
|
||||
WHERE contribution_id = :cid AND browser_id = :bid
|
||||
");
|
||||
$stmt->execute([':cid' => $input['contribution_id'], ':bid' => $browser_id]);
|
||||
|
||||
// Bei INSERT wird die Browser-ID mitgespeichert
|
||||
$stmt = $pdo->prepare("
|
||||
INSERT INTO votes (contribution_id, voter_name, vote_type, browser_id)
|
||||
VALUES (:cid, :voter, :vtype, :bid)
|
||||
");
|
||||
```
|
||||
|
||||
**Ablauf eines Votes:**
|
||||
|
||||
1. Browser sendet `contribution_id`, `voter_name`, `vote_type` und `browser_id`.
|
||||
2. API prüft ob bereits ein Vote mit dieser `browser_id` für diesen Beitrag existiert.
|
||||
3. Falls ja und gleicher Typ → Vote wird entfernt (Toggle).
|
||||
4. Falls ja und anderer Typ → Vote wird gewechselt (Like → Dislike oder umgekehrt).
|
||||
5. Falls nein → neuer Vote wird eingefügt.
|
||||
6. Der DB-Trigger `update_vote_counts()` aktualisiert `likes_count`/`dislikes_count` automatisch.
|
||||
|
||||
### handle_read — Liefert eigene Votes mit
|
||||
|
||||
```php
|
||||
// Sucht alle Votes der aktuellen Browser-ID
|
||||
$browser_id = $input['browser_id'] ?? '';
|
||||
if ($browser_id !== '') {
|
||||
$stmt = $pdo->prepare("
|
||||
SELECT contribution_id, vote_type
|
||||
FROM votes
|
||||
WHERE browser_id = :bid
|
||||
");
|
||||
$stmt->execute([':bid' => $browser_id]);
|
||||
$user_votes = [];
|
||||
foreach ($stmt->fetchAll() as $v) {
|
||||
$user_votes[$v['contribution_id']] = $v['vote_type'];
|
||||
}
|
||||
// Hängt die Votes an die GeoJSON-Antwort an
|
||||
$featureCollection['user_votes'] = $user_votes;
|
||||
}
|
||||
```
|
||||
|
||||
**Beispiel API-Antwort mit Votes:**
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "FeatureCollection",
|
||||
"features": [ /* ... Beiträge ... */ ],
|
||||
"user_votes": {
|
||||
"2": "like",
|
||||
"5": "dislike",
|
||||
"7": "like"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Datenfluss-Diagramm
|
||||
|
||||
```
|
||||
ERSTER BESUCH:
|
||||
┌──────────┐ Cookie nicht vorhanden ┌──────────────┐
|
||||
│ Browser │ ─────────────────────────────→ │ getBrowserId()│
|
||||
│ │ ← UUID generiert + Cookie ──── │ │
|
||||
└──────────┘ gesetzt (1 Jahr) └──────────────┘
|
||||
|
||||
BEITRAG ERSTELLEN:
|
||||
┌──────────┐ POST: create + browser_id ┌──────────────┐ INSERT mit ┌──────────┐
|
||||
│ Browser │ ─────────────────────────────→│ PHP API │ ──────────────────→ │ DB │
|
||||
│ │ │ │ browser_id │ │
|
||||
└──────────┘ └──────────────┘ └──────────┘
|
||||
|
||||
ABSTIMMEN:
|
||||
┌──────────┐ POST: vote + browser_id ┌──────────────┐ SELECT WHERE bid ┌──────────┐
|
||||
│ Browser │ ─────────────────────────────→│ PHP API │ ──────────────────→ │ DB │
|
||||
│ │ ← action: created/removed ─── │ Prüft Vote │ ← Ergebnis ─────── │ │
|
||||
└──────────┘ │ Toggle-Logik │ INSERT/DELETE │ │
|
||||
└──────────────┘ └──────────┘
|
||||
|
||||
SEITE LADEN (Votes persistent):
|
||||
┌──────────┐ POST: read + browser_id ┌──────────────┐ SELECT votes ┌──────────┐
|
||||
│ Browser │ ────────────────────────────→│ PHP API │ ──────────────────→ │ DB │
|
||||
│ │ ← features + user_votes ──── │ │ ← Votes ─────────── │ │
|
||||
│ │ └──────────────┘ └──────────┘
|
||||
│ │
|
||||
│ buildPopupHtml() liest userVotes
|
||||
│ → Setzt CSS-Klasse "liked"/"disliked"
|
||||
│ → Buttons sind farbig markiert
|
||||
└──────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testszenarien
|
||||
|
||||
### Test 1: Erste Nutzung — Cookie wird generiert
|
||||
|
||||
**Vorbereitung:** Browser-Cookies löschen oder Inkognito-Modus öffnen.
|
||||
|
||||
**Schritte:**
|
||||
1. Portal öffnen.
|
||||
2. Browser-Entwicklertools öffnen (F12) → Reiter "Application" → "Cookies".
|
||||
|
||||
**Erwartung:**
|
||||
- Ein Cookie `webgis_browser_id` existiert mit einem UUID-Wert (z.B. `a1b2c3d4-e5f6-4789-abcd-ef0123456789`).
|
||||
- Der Cookie hat `max-age=31536000` (1 Jahr).
|
||||
- Der Cookie hat `SameSite=Lax`.
|
||||
|
||||
---
|
||||
|
||||
### Test 2: Vote bleibt nach Popup schließen/öffnen
|
||||
|
||||
**Schritte:**
|
||||
1. Anmelden mit einem Namen.
|
||||
2. Einen Beitrag anklicken → Popup öffnet sich.
|
||||
3. Like-Button klicken → Button wird grün, Zähler erhöht sich.
|
||||
4. Popup schließen (Klick auf Karte).
|
||||
5. Denselben Beitrag erneut anklicken.
|
||||
|
||||
**Erwartung:**
|
||||
- Like-Button ist weiterhin grün markiert.
|
||||
- Zähler zeigt den korrekten Wert.
|
||||
|
||||
---
|
||||
|
||||
### Test 3: Vote bleibt nach Seite neu laden
|
||||
|
||||
**Schritte:**
|
||||
1. Einen Beitrag liken (wie Test 2, Schritt 1-3).
|
||||
2. Seite komplett neu laden (F5 oder Strg+R).
|
||||
3. Denselben Beitrag anklicken.
|
||||
|
||||
**Erwartung:**
|
||||
- Like-Button ist weiterhin grün markiert.
|
||||
- Der Vote wurde aus der Datenbank geladen, nicht aus dem Browser-Speicher.
|
||||
|
||||
**Verifikation in der Datenbank:**
|
||||
```sql
|
||||
SELECT * FROM votes WHERE browser_id = 'deine-browser-id-hier';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Test 4: Vote Toggle — Like entfernen
|
||||
|
||||
**Schritte:**
|
||||
1. Einen Beitrag liken (Button wird grün).
|
||||
2. Denselben Like-Button erneut klicken.
|
||||
|
||||
**Erwartung:**
|
||||
- Like-Button wird wieder normal (nicht mehr grün).
|
||||
- Zähler verringert sich um 1.
|
||||
- In der Datenbank existiert kein Vote mehr für diesen Beitrag und diese Browser-ID.
|
||||
|
||||
---
|
||||
|
||||
### Test 5: Vote wechseln — Like zu Dislike
|
||||
|
||||
**Schritte:**
|
||||
1. Einen Beitrag liken (Like-Button wird grün).
|
||||
2. Dislike-Button klicken.
|
||||
|
||||
**Erwartung:**
|
||||
- Like-Button wird normal, Dislike-Button wird rot.
|
||||
- Like-Zähler verringert sich um 1, Dislike-Zähler erhöht sich um 1.
|
||||
|
||||
---
|
||||
|
||||
### Test 6: Eigene Beiträge bearbeiten/löschen
|
||||
|
||||
**Schritte:**
|
||||
1. Anmelden und einen neuen Beitrag erstellen.
|
||||
2. Warten bis der Beitrag vom Moderator freigegeben wird (oder über Admin selbst freigeben).
|
||||
3. Den eigenen Beitrag auf der Karte anklicken.
|
||||
|
||||
**Erwartung:**
|
||||
- Popup zeigt "Bearbeiten" und "Löschen" Buttons.
|
||||
|
||||
---
|
||||
|
||||
### Test 7: Fremde Beiträge — kein Bearbeiten/Löschen
|
||||
|
||||
**Schritte:**
|
||||
1. Einen anderen Browser oder Inkognito-Modus öffnen.
|
||||
2. Einen Beitrag anklicken, der im ersten Browser erstellt wurde.
|
||||
|
||||
**Erwartung:**
|
||||
- Popup zeigt KEINE "Bearbeiten"/"Löschen" Buttons.
|
||||
- Like/Dislike-Buttons sind weiterhin verfügbar.
|
||||
|
||||
---
|
||||
|
||||
### Test 8: Admin sieht alle Bearbeiten/Löschen-Buttons
|
||||
|
||||
**Schritte:**
|
||||
1. Über das Lock-Icon im Header das Moderationsportal öffnen.
|
||||
2. Mit dem Admin-Passwort anmelden.
|
||||
3. Zurück zur Karte navigieren (Link "Bürgerportal" im Mod-Header).
|
||||
4. Einen beliebigen Beitrag anklicken.
|
||||
|
||||
**Erwartung:**
|
||||
- Popup zeigt "Bearbeiten" und "Löschen" Buttons bei ALLEN Beiträgen, nicht nur eigenen.
|
||||
|
||||
---
|
||||
|
||||
### Test 9: Verschiedene Browser = verschiedene Identitäten
|
||||
|
||||
**Schritte:**
|
||||
1. In Chrome einen Beitrag erstellen und liken.
|
||||
2. Denselben Beitrag in Firefox öffnen.
|
||||
|
||||
**Erwartung:**
|
||||
- In Firefox ist der Like-Button NICHT grün markiert (anderer Browser = andere Browser-ID).
|
||||
- In Firefox sind keine Bearbeiten/Löschen-Buttons sichtbar.
|
||||
- In Firefox kann derselbe Beitrag unabhängig geliked werden (eigener Vote).
|
||||
|
||||
---
|
||||
|
||||
### Test 10: Doppel-Vote wird verhindert
|
||||
|
||||
**Schritte:**
|
||||
1. Einen Beitrag liken.
|
||||
2. In der Datenbank nachschauen:
|
||||
```sql
|
||||
SELECT * FROM votes
|
||||
WHERE contribution_id = [ID] AND browser_id = '[BROWSER-ID]';
|
||||
```
|
||||
|
||||
**Erwartung:**
|
||||
- Genau ein Eintrag existiert.
|
||||
- Ein erneuter Like vom selben Browser entfernt den Vote (Toggle).
|
||||
- Die Datenbank-Constraint `votes_contribution_browser_unique` verhindert technisch jeden Versuch, zwei Votes pro Browser pro Beitrag einzufügen.
|
||||
|
||||
---
|
||||
|
||||
### Test 11: Cookie löschen = neuer Nutzer
|
||||
|
||||
**Schritte:**
|
||||
1. Einen Beitrag erstellen und liken.
|
||||
2. Browser-Cookies löschen (Entwicklertools → Application → Cookies → alle löschen).
|
||||
3. Seite neu laden.
|
||||
|
||||
**Erwartung:**
|
||||
- Like-Markierung ist weg (neue Browser-ID).
|
||||
- Bearbeiten/Löschen-Buttons beim eigenen Beitrag sind weg.
|
||||
- Der alte Vote und Beitrag bleiben in der Datenbank (mit der alten Browser-ID).
|
||||
- Der Nutzer kann den Beitrag erneut liken (mit neuer Browser-ID).
|
||||
|
||||
**Hinweis:** Das ist eine bekannte Einschränkung. Für ein Bürgerbeteiligungsportal ohne Registrierung ist das ein akzeptabler Kompromiss.
|
||||
|
||||
---
|
||||
|
||||
## Sicherheitsbetrachtung
|
||||
|
||||
| Szenario | Geschützt? | Erklärung |
|
||||
|----------|------------|-----------|
|
||||
| Jemand gibt einen fremden Namen ein | ✅ Ja | Edit/Delete prüft Browser-ID, nicht den Namen |
|
||||
| Jemand versucht doppelt abzustimmen | ✅ Ja | DB-Constraint `votes_contribution_browser_unique` verhindert das |
|
||||
| Jemand löscht seine Cookies | ⚠️ Teilweise | Wird als neuer Nutzer behandelt, kann erneut abstimmen |
|
||||
| Jemand kopiert die Browser-ID eines anderen | ⚠️ Theoretisch möglich | Erfordert Zugriff auf den anderen Browser — unrealistisch für Bürgerbeteiligung |
|
||||
| Admin-Zugang ohne Passwort | ✅ Ja | `IS_ADMIN` wird serverseitig aus der PHP-Session gesetzt |
|
||||
|
||||
---
|
||||
|
||||
## Zukünftige Verbesserungen
|
||||
|
||||
| Verbesserung | Phase | Beschreibung |
|
||||
|-------------|-------|-------------|
|
||||
| Rate Limiting | Phase 5 | Maximal X Votes pro Stunde pro Browser-ID |
|
||||
| Cookie-Warnung (DSGVO) | Phase 5 | Hinweis auf Cookie-Nutzung beim ersten Besuch |
|
||||
| Fingerprinting als Fallback | Phase 5 | Falls Cookies deaktiviert sind, Browser-Fingerprint als Alternative |
|
||||
| users-Tabelle für Moderatoren | Phase 5 | Mehrere Moderatoren mit individuellen Passwörtern und Audit-Log |
|
||||
Reference in New Issue
Block a user