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:
parent
3f187f1ede
commit
729865be78
13 changed files with 717 additions and 12 deletions
|
|
@ -114,6 +114,27 @@ func (s *UserStore) GetByOIDCSubject(ctx context.Context, issuer, subject string
|
|||
return u, nil
|
||||
}
|
||||
|
||||
// UpdateDisplayName sets the display_name of the user with the given id.
|
||||
// An empty displayName clears the field (stores NULL).
|
||||
func (s *UserStore) UpdateDisplayName(ctx context.Context, id uuid.UUID, displayName string) (User, error) {
|
||||
const q = `
|
||||
UPDATE users SET display_name = NULLIF($2, ''), updated_at = now()
|
||||
WHERE id = $1
|
||||
RETURNING id, email, password_hash, oidc_subject, oidc_issuer, display_name`
|
||||
var u User
|
||||
var dn *string
|
||||
err := s.pool.QueryRow(ctx, q, id, displayName).
|
||||
Scan(&u.ID, &u.Email, &u.PasswordHash, &u.OIDCSubject, &u.OIDCIssuer, &dn)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return User{}, ErrUserNotFound
|
||||
}
|
||||
return User{}, fmt.Errorf("update display name: %w", err)
|
||||
}
|
||||
u.DisplayName = dn
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// CreateOIDCUser inserts a new user for an OIDC login. The user has no password
|
||||
// (password_hash is NULL) and is identified by (issuer, subject). email may be
|
||||
// empty if the IdP did not provide one; we store a synthesized placeholder so
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ type API struct {
|
|||
ops *OpsHandler
|
||||
suggest *SuggestHandler
|
||||
config *ConfigHandler
|
||||
me *MeHandler
|
||||
}
|
||||
|
||||
// NewAPI constructs the API with all handler groups.
|
||||
|
|
@ -51,6 +52,7 @@ func NewAPI(cfg *config.Config, pool *pgxpool.Pool) *API {
|
|||
ops: NewOpsHandler(opStore, listStore),
|
||||
suggest: NewSuggestHandler(suggestStore),
|
||||
config: NewConfigHandler(cfg),
|
||||
me: NewMeHandler(users),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -85,6 +87,8 @@ func (a *API) Handler() http.Handler {
|
|||
mux.HandleFunc("POST /auth/logout", a.auth.Logout)
|
||||
|
||||
// --- authenticated API endpoints ---
|
||||
mux.Handle("GET /api/me", a.RequireAuth(http.HandlerFunc(a.me.Get)))
|
||||
mux.Handle("PUT /api/me", a.RequireAuth(http.HandlerFunc(a.me.Update)))
|
||||
mux.Handle("GET /api/lists", a.RequireAuth(http.HandlerFunc(a.lists.List)))
|
||||
mux.Handle("POST /api/lists", a.RequireAuth(http.HandlerFunc(a.lists.Create)))
|
||||
mux.Handle("POST /api/lists/join", a.RequireAuth(http.HandlerFunc(a.lists.Join)))
|
||||
|
|
|
|||
76
backend/internal/httpapi/me.go
Normal file
76
backend/internal/httpapi/me.go
Normal 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))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue