Feature: Settings screen with account, theme, profile, reset + credits

Backend:
- New GET/PUT /api/me endpoint to read and update the authenticated
  user's profile (currently display_name). UserStore.UpdateDisplayName.
- Wired into the authed router.

App:
- New SettingsScreen + SettingsViewModel with five sections:
  * Account & Sync: login status, email, server URL, logout, connect.
  * Profile: edit display name (pushed to PUT /api/me when logged in).
  * Appearance: System / Light / Dark theme switch, persisted in
    SessionManager and applied via MitbringslTheme(sessionManager).
  * Data: 'Reset local data' wipes Room tables (server data kept).
  * About: version, 'Developed by Janik Dietz', and credits to
    GLM-5.2 + Gemini 3.6 Flash.
- Theme.kt now reads the user's theme preference (StateFlow) instead
  of only the system default; MainActivity passes SessionManager in.
- Navigation: new SettingsNavKey; settings gear icon in ListsScreen
  top bar; Settings links back to the Sync/Account screen.
- DTOs/API: UpdateMeRequestDto + getMe()/updateMe() for /api/me.
This commit is contained in:
Tronax 2026-08-06 10:39:12 +02:00
parent 3f187f1ede
commit 729865be78
Signed by: Tronax
SSH key fingerprint: SHA256:2pKKXDZucWvaF/GzXNz0FY53EAO1YDLN80bqS+TTz/o
13 changed files with 717 additions and 12 deletions

View file

@ -0,0 +1,76 @@
package httpapi
import (
"log/slog"
"net/http"
"strings"
"github.com/mitbringsl/backend/internal/auth"
)
// MeHandler exposes the authenticated user's own profile (/api/me).
type MeHandler struct {
users *auth.UserStore
}
// NewMeHandler constructs a MeHandler.
func NewMeHandler(users *auth.UserStore) *MeHandler {
return &MeHandler{users: users}
}
type updateMeRequest struct {
DisplayName *string `json:"display_name"` // nil = unchanged, "" = clear
}
// Get returns the current user's profile.
// GET /api/me
func (h *MeHandler) Get(w http.ResponseWriter, r *http.Request) {
userID, ok := userIDFromCtx(r)
if !ok {
renderError(w, http.StatusUnauthorized, "Unauthorized", "Missing user context.")
return
}
u, err := h.users.GetUserByID(r.Context(), userID)
if err != nil {
slog.Error("get me failed", "error", err, "user_id", userID)
renderError(w, http.StatusInternalServerError, "Internal error", "Could not load user.")
return
}
renderJSON(w, http.StatusOK, toUserDTO(u))
}
// Update changes editable fields of the current user (currently display_name).
// PUT /api/me
func (h *MeHandler) Update(w http.ResponseWriter, r *http.Request) {
userID, ok := userIDFromCtx(r)
if !ok {
renderError(w, http.StatusUnauthorized, "Unauthorized", "Missing user context.")
return
}
var req updateMeRequest
if !decodeJSON(w, r, &req) {
return
}
// Only update when display_name is provided in the body (pointer non-nil).
// Trimming to keep stored names tidy; an empty string clears the field.
if req.DisplayName == nil {
// Nothing to change return the current profile.
u, err := h.users.GetUserByID(r.Context(), userID)
if err != nil {
renderError(w, http.StatusInternalServerError, "Internal error", "Could not load user.")
return
}
renderJSON(w, http.StatusOK, toUserDTO(u))
return
}
name := strings.TrimSpace(*req.DisplayName)
u, err := h.users.UpdateDisplayName(r.Context(), userID, name)
if err != nil {
slog.Error("update me failed", "error", err, "user_id", userID)
renderError(w, http.StatusInternalServerError, "Internal error", "Could not update user.")
return
}
renderJSON(w, http.StatusOK, toUserDTO(u))
}