feat(quick-261016-0fm): per-admin theme and locale preferences

- GET and PUT /account/preferences (own row only, interactive sessions,
  CSRF header on cookie writes) store admin_theme and admin_locale in
  backend_users.metadata, keeping every other key
- GET /auth/me and the My account answers carry preferences
- phrasebook.Translator.Locales lists the locales defining a prefix
- route tables, conformance cases, OpenAPI document, TS types and docs
This commit is contained in:
Jakub Zych
2026-10-16 09:26:09 +02:00
parent 1a03399357
commit 047858ae84
21 changed files with 729 additions and 12 deletions

View File

@@ -110,6 +110,28 @@ type AdminProfile struct {
Role *AdminRoleSummary `json:"role,omitempty"`
// Avatar is present when the admin has a public avatar.
Avatar *AdminAvatar `json:"avatar,omitempty"`
// Preferences are the admin's own UI preferences (theme and language).
Preferences AdminPreferences `json:"preferences"`
}
// AdminPreferences are a signed-in admin's UI preferences, stored in
// backend_users.metadata under admin_theme and admin_locale.
type AdminPreferences struct {
// Theme is system, light or dark; system (follow the browser) when unset.
Theme string `json:"theme"`
// Locale is an admin locale code from GET /lang meta.locales, or "" when
// the admin follows the request locale.
Locale string `json:"locale"`
}
// AdminPreferencesRequest is the body of PUT /account/preferences. Only these
// keys are accepted; an absent key keeps its stored value.
type AdminPreferencesRequest struct {
// Theme is system, light or dark.
Theme *string `json:"theme,omitempty"`
// Locale is "" (follow the request locale) or a code from GET /lang
// meta.locales.
Locale *string `json:"locale,omitempty"`
}
// AdminAvatar is an admin's avatar: the public URL of the image and of its
@@ -190,7 +212,7 @@ func AdminLogout() {}
// AdminMe documents GET /auth/me.
//
// @Summary Current admin
// @Description The signed-in admin's profile: login, email, names, the subtitle (backend_users.metadata.subtitle, "" when not set), the superuser flag, the role and, when the admin has one, the avatar with its public url and the 250x250 crop thumb_url.
// @Description The signed-in admin's profile: login, email, names, the subtitle (backend_users.metadata.subtitle, "" when not set), the superuser flag, the role, the UI preferences (theme, "system" when not set, and locale, "" when not set) and, when the admin has one, the avatar with its public url and the 250x250 crop thumb_url.
// @Tags admin
// @Produce json
// @Security BackendBearer
@@ -218,6 +240,36 @@ func AdminMe() {}
// @Router /account [put]
func AdminAccountUpdate() {}
// AdminAccountPreferences documents GET /account/preferences.
//
// @Summary My UI preferences
// @Description The signed-in admin's own UI preferences: theme (system, light or dark; system when not set) and locale (an admin locale code, or "" to follow the request locale). Interactive session only (403 interactive_only for personal API tokens).
// @Tags admin
// @Produce json
// @Security BackendBearer
// @Success 200 {object} Envelope[AdminPreferences]
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Router /account/preferences [get]
func AdminAccountPreferences() {}
// AdminAccountPreferencesUpdate documents PUT /account/preferences.
//
// @Summary Update my UI preferences
// @Description Saves the signed-in admin's own UI preferences in backend_users.metadata (admin_theme, admin_locale); every other metadata key is kept and the row is the token subject, never a request value. Only theme and locale are accepted: any other key is a 422 on that key and nothing is written. Each present key is saved and an absent key keeps its stored value, so {} answers the current preferences and writes nothing. theme must be system, light or dark; locale must be "" (follow the request locale) or a code from GET /lang meta.locales; another value or a non-string is a 422 on that field. Stored metadata that is not a JSON object is never overwritten (422). Interactive session only (403 interactive_only for personal API tokens).
// @Tags admin
// @Accept json
// @Produce json
// @Security BackendBearer
// @Param body body AdminPreferencesRequest true "Preferences to change"
// @Success 200 {object} Envelope[AdminPreferences]
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 413 {object} ErrorEnvelope
// @Failure 422 {object} ErrorEnvelope
// @Router /account/preferences [put]
func AdminAccountPreferencesUpdate() {}
// AdminAccountTokens documents GET /account/tokens.
//
// @Summary List my API tokens