Commit Graph

15 Commits

Author SHA1 Message Date
irrlicht 662285c8b5 Statistik: anonymes Impression-Tracking mit ASN-Einordnung
Seitenaufrufe werden serverseitig pro (Tag, anonymisiertes Netz, Pfad)
gezaehlt -- nur auf den HTML-Seiten-Routen, nicht auf API/Assets. Die
Client-IP wird sofort maskiert (IPv4 /16, IPv6 /32); die vollstaendige
IP wird nie gespeichert oder weiterverwendet.

Optional loest eine lokale GeoLite2-ASN-DB (Env KVER_GEOIP_ASN) den
Netzbetreiber aus dem bereits anonymisierten Netz auf -- auch dafuer wird
nicht die volle IP herangezogen. Fehlt die DB, bleibt das Feld leer.

Neuer Endpunkt GET /stats/detail liefert die Aggregate; die Tabelle
impression speichert nur Zaehlwerte (kein Eventlog).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 23:21:17 +02:00
irrlicht d9028a8400 Header: Statuszeile "Ohne Datenverlust seit n Tagen" + Kennzahlen zurückholen
Schlanke Wiederbelebung des alten Uptime-Schriftzugs aus der Python-Version,
jetzt als kleine Zeile direkt unter dem Titel: Tage seit fixem Startpunkt
(13.12.2025, Kontinuität) plus aktuelle Profil- und Beitragszahlen.

- /stats: neuer öffentlicher Endpunkt (COUNT user / COUNT entry WHERE deleted=0)
- app.js: renderStats() rechnet die Tage und füllt #stats per textContent
- index.html: <p id="stats" class="tagline"> unter der h1
- app.css: kleine graue .tagline-Zeile

Kein Inline-JS/CSS -> strikte CSP bleibt unangetastet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 08:46:05 +02:00
irrlicht 50be342899 Deployment-Härtung + Podman-Container
- http.Server mit Timeouts (Slowloris) und Graceful Shutdown (SIGTERM)
- Adresse und DB-Pfad per Env (KVER_ADDR, KVER_DB)
- Security-Header global: nosniff, X-Frame-Options, Referrer-Policy, CSP
- Upload-Requests hart auf 17 MB gedeckelt (MaxBytesReader statt nur
  Multipart-Speichergrenze)
- Indizes für Feed-, Thread- und Vote-Queries
- middleware.RealIP für Logging/Rate-Limit hinter dem Reverse-Proxy
- Containerfile (Multi-Stage, Alpine, non-root) + Deploy-Doku in notes/

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 07:00:09 +02:00
irrlicht 925b00edf5 Bild-Upload: animierte GIFs erhalten statt aufs erste Frame reduzieren
Animierte GIFs (mehr als 1 Frame) werden jetzt unverändert durchgereicht
statt via Decode+Re-Encode auf das erste Frame reduziert. Da die Bytes
dabei nicht mehr sanitisiert werden, zwei Guardrails:

- gif.DecodeAll läuft über einen io.LimitReader (16 MB) und prüft danach
  ein Frame-Budget (1000) sowie ein Gesamtpixel-Budget (100 MP) -> bremst
  den DoS-Vektor (GIF-Bombe mit vielen Frames). copyUpload schreibt die
  Bytes unter serverseitig erzeugtem Namen und deckelt hart bei 16 MB.
- noSniff-Middleware setzt X-Content-Type-Options: nosniff auf /static/*,
  damit der Browser eine durchgereichte Datei nicht als HTML/JS deutet
  (Polyglot/Stored-XSS).

Standbilder (JPEG/PNG/Einzelbild-GIF) behalten den skalierenden
Re-Encode-Pfad und damit die Sanitisierung.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 07:06:54 +02:00
irrlicht 15ef7db5f7 Profilbilder: Avatar-Upload, Anzeige auf Profil & Feed
- user.avatar-Spalte (Schema, migrate.sh, Migrationsdoku)
- POST /user/avatar (geschützt): Upload via storeImage, ersetzt/löscht altes Bild
- avatar in /u/{name}/info, /user/info und im entrySelect-Join (Feed-Karten)
- Account-Löschung entfernt auch das Avatar-File
- Frontend: Avatar im Profilkopf + Byline der Karten (50px), Upload-Form,
  Platzhalterbild no_profile_pic.jpg als Fallback
- .card img per :not(.avatar) von Beitragsbildern getrennt

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 18:20:14 +02:00
irrlicht 1313e9613c Sicherheit & Robustheit härten
- SQLite-DSN: busy_timeout(5000) + WAL + foreign_keys gegen SQLITE_BUSY
- uid ist nie 0 mehr (0 = Sentinel für gelöscht/anonym), Retry bei Kollision
- scanEntries gibt Scan-Fehler zurück statt Zeilen still zu überspringen
- Session-timeout serverseitig auf 30 Tage gedeckelt (clientgesteuert)
- Bild-Upload: Dimensionen vor Decode prüfen (Decompression-Bomb-Schutz)
- Secure-Cookie via TLS-Erkennung (r.TLS / X-Forwarded-Proto)
- Rate-Limit (httprate, 20/min/IP) auf Login & Registrierung
- Vote nur auf existierende Beiträge (keine Waisen-Votes)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 17:07:17 +02:00
irrlicht ad469ab366 Bearbeiten: POST /entry/{pid}/edit (Inhalt ändern)
- handleEditEntry: Eigentümerprüfung, nicht-leer, content-only Update
- Bild und last_activity bleiben unverändert; gelöschte Posts nicht editierbar
- Frontend: Bearbeiten-Button + Inline-Formular auf der Focus-Seite

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 13:33:22 +02:00
irrlicht cb3da3bb24 Soft-Delete: Beiträge als [deleted] erhalten statt löschen
- entry.deleted; entrySelect LEFT JOIN + COALESCE(username)
- POST /entry/{pid}/delete: deleted=1, Inhalt/Bild/Autor geleert, Zeile bleibt
- Konto-Löschung soft-deletet die eigenen Beiträge (statt hartem DELETE),
  damit fremde Antworten nicht verwaisen
- Frontend: [deleted]-Platzhalter im Feed, Löschen-Button auf Focus-Seite
- notes/migrations.md: ALTER TABLE deleted

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 13:21:42 +02:00
irrlicht ba085eff4e Threading: Antworten (reply_to) + Focus-View
- entry: reply_to/reply_count/last_activity; reply_count bubbelt bis Root
- Hauptfeed nur Roots nach last_activity, Profil-Feed inkl. Antworten
- GET /entry/{pid}/thread (Ahnen+Antworten), GET /e/{pid} Focus-Seite
- Frontend: Antworten-Link im Feed, entry.html/entry.js Focus-Seite
- notes/migrations.md: ALTER TABLE fuer Prod

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 12:21:55 +02:00
irrlicht 844bbb44e7 Frontend+API: Namen ändern (POST /user/rename)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 11:47:44 +02:00
irrlicht 7ffc35808a User 2026-06-02 16:35:47 +02:00
irrlicht bee1305a2b Add endpoint tests with httptest
Two small refactors make the handlers testable:
- extract route registration into routes() http.Handler (was inline in main)
- initDB(dsn) takes a DSN so tests use an isolated temp-file DB

loginJitter var lets tests disable the anti-timing sleep for speed.

endpoints_test.go covers the main flows via httptest + cookie jar:
register/login/headerbar, empty-then-populated feed, create requires auth,
voting (new/toggle/switch, per-user tallies), vote auth + invalid mode,
and account deletion (wrong/correct password, login fails after).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 11:16:02 +02:00
irrlicht 38da498754 Split voting into clean read/write endpoints
Replaces the GET-mutates-state interactions route with a proper split:
- GET  /entry/{pid}/votes  reads the tally (public, read-only)
- POST /entry/{pid}/vote   casts/toggles the vote (auth, mode in body)

Shared voteTally/writeTally helpers back both handlers. Invalid mode now
returns 400. Frontend updated to read via GET and vote via POST.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 11:03:33 +02:00
irrlicht c904e3e2db Implement user account deletion flow
POST /user/delete (auth required) verifies the password via pass1 and
transactionally removes the user's sessions, votes, entries and the user
row, then clears the session cookie. Associated media files are removed
best effort after commit. This was only a stub in the Flask original.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 10:19:29 +02:00
irrlicht cb237c248e Rewrite backend in Go with JSON API and JS frontend
Reimplements the Flask app as a Go HTTP API (chi + modernc sqlite) with a
minimal vanilla-JS frontend in web/. Endpoints mirror the original Flask
routes but return JSON instead of HTML.

- auth: login/logout/register/sessioninfo/headerbar with crypto/rand tokens
- entry: paginated feed (single JOIN), create with image scaling
- user: profile, userinfo; delete still a stub
- requireAuth middleware passes uid via context
- notes/api.md documents the API and schema

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 10:17:02 +02:00