feat: add item notes and duplicate detection across platforms

- Add per-item notes editable in the detail and edit screens, synced
  between desktop and Android
- Guard against duplicates by external ID and by type/year/title,
  mirroring the desktop findDuplicate logic in MediaDao
- Wrap multi-step database operations in Room transactions
- Disable Android auto backup and remove the destructive Room
  migration fallback so schema bumps fail loudly instead of wiping data
- Bound crypto envelope KDF parameters when reading untrusted headers
- Handle malformed server responses in SyncClient instead of crashing
- Extend settings, database, and image cache on the desktop side
This commit is contained in:
Tronax 2026-08-16 12:04:28 +02:00
parent 85f5c6dd4e
commit 2b35139364
30 changed files with 837 additions and 124 deletions

View file

@ -28,6 +28,7 @@ type server struct {
oauth oauth2.Config
client *http.Client
store *store // nil when DATABASE_URL is unset (sync disabled)
tmdbLim *rateLimiter
}
func main() {
@ -54,6 +55,8 @@ func main() {
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
},
client: &http.Client{Timeout: 15 * time.Second},
// Generous per-user cap on proxied TMDB calls (shared API key).
tmdbLim: newRateLimiter(120, time.Minute),
}
if cfg.DatabaseURL != "" {
@ -184,7 +187,9 @@ func (s *server) handleCallback(w http.ResponseWriter, r *http.Request) {
oauthToken, err := s.oauth.Exchange(ctx, r.URL.Query().Get("code"),
oauth2.VerifierOption(verifierCookie.Value))
if err != nil {
http.Error(w, "Token-Austausch fehlgeschlagen: "+err.Error(), http.StatusBadGateway)
// Log the details server-side; don't leak upstream internals to clients.
log.Printf("oauth exchange failed: %v", err)
http.Error(w, "Token-Austausch fehlgeschlagen", http.StatusBadGateway)
return
}
@ -195,7 +200,8 @@ func (s *server) handleCallback(w http.ResponseWriter, r *http.Request) {
}
idToken, err := s.verifier.Verify(ctx, rawIDToken)
if err != nil {
http.Error(w, "id_token-Prüfung fehlgeschlagen: "+err.Error(), http.StatusUnauthorized)
log.Printf("id_token verification failed: %v", err)
http.Error(w, "id_token-Prüfung fehlgeschlagen", http.StatusUnauthorized)
return
}
@ -293,10 +299,15 @@ func (s *server) handleProxy(w http.ResponseWriter, r *http.Request) {
}
bearer := strings.TrimSpace(strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer"))
if _, err := verifyToken(s.cfg.SessionKey, bearer); err != nil {
claims, err := verifyToken(s.cfg.SessionKey, bearer)
if err != nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
if !s.tmdbLim.allow(claims.Subject) {
http.Error(w, "rate limit exceeded, try again later", http.StatusTooManyRequests)
return
}
// Build upstream URL: /3/<path> -> <TMDBBase>/<path>, keep the query, force api_key.
upstreamPath := strings.TrimPrefix(r.URL.Path, "/3")
@ -319,7 +330,8 @@ func (s *server) handleProxy(w http.ResponseWriter, r *http.Request) {
resp, err := s.client.Do(req)
if err != nil {
http.Error(w, "upstream error: "+err.Error(), http.StatusBadGateway)
log.Printf("tmdb upstream error for %s: %v", r.URL.Path, err)
http.Error(w, "upstream error", http.StatusBadGateway)
return
}
defer resp.Body.Close()