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:
@@ -352,6 +352,36 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"cabana.AdminPreferences": {
|
||||
"properties": {
|
||||
"locale": {
|
||||
"description": "Locale is an admin locale code from GET /lang meta.locales, or \"\" when\nthe admin follows the request locale.",
|
||||
"type": "string"
|
||||
},
|
||||
"theme": {
|
||||
"description": "Theme is system, light or dark; system (follow the browser) when unset.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"locale",
|
||||
"theme"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"cabana.AdminPreferencesRequest": {
|
||||
"properties": {
|
||||
"locale": {
|
||||
"description": "Locale is \"\" (follow the request locale) or a code from GET /lang\nmeta.locales.",
|
||||
"type": "string"
|
||||
},
|
||||
"theme": {
|
||||
"description": "Theme is system, light or dark.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"cabana.AdminProfile": {
|
||||
"properties": {
|
||||
"avatar": {
|
||||
@@ -380,6 +410,14 @@
|
||||
"login": {
|
||||
"type": "string"
|
||||
},
|
||||
"preferences": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/cabana.AdminPreferences"
|
||||
}
|
||||
],
|
||||
"description": "Preferences are the admin's own UI preferences (theme and language)."
|
||||
},
|
||||
"role": {
|
||||
"$ref": "#/components/schemas/cabana.AdminRoleSummary"
|
||||
},
|
||||
@@ -395,6 +433,7 @@
|
||||
"is_superuser",
|
||||
"last_name",
|
||||
"login",
|
||||
"preferences",
|
||||
"subtitle"
|
||||
],
|
||||
"type": "object"
|
||||
@@ -851,6 +890,21 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"cabana.Envelope-cabana_AdminPreferences": {
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/cabana.AdminPreferences"
|
||||
},
|
||||
"meta": {
|
||||
"$ref": "#/components/schemas/cabana.SuccessMeta"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data",
|
||||
"meta"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"cabana.Envelope-cabana_AdminProfile": {
|
||||
"properties": {
|
||||
"data": {
|
||||
@@ -2963,6 +3017,127 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/account/preferences": {
|
||||
"get": {
|
||||
"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).",
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/cabana.Envelope-cabana_AdminPreferences"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "OK"
|
||||
},
|
||||
"401": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Unauthorized"
|
||||
},
|
||||
"403": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Forbidden"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"BackendBearer": []
|
||||
}
|
||||
],
|
||||
"summary": "My UI preferences",
|
||||
"tags": [
|
||||
"admin"
|
||||
]
|
||||
},
|
||||
"put": {
|
||||
"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).",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/cabana.AdminPreferencesRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Preferences to change",
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/cabana.Envelope-cabana_AdminPreferences"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "OK"
|
||||
},
|
||||
"401": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Unauthorized"
|
||||
},
|
||||
"403": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Forbidden"
|
||||
},
|
||||
"413": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Request Entity Too Large"
|
||||
},
|
||||
"422": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/cabana.ErrorEnvelope"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "Unprocessable Entity"
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"BackendBearer": []
|
||||
}
|
||||
],
|
||||
"summary": "Update my UI preferences",
|
||||
"tags": [
|
||||
"admin"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/account/tokens": {
|
||||
"get": {
|
||||
"description": "Lists the signed-in admin's personal API tokens, newest first. Revoked tokens are hidden unless revoked=1. Personal API tokens are refused (403 interactive_only).",
|
||||
@@ -3305,7 +3480,7 @@
|
||||
},
|
||||
"/auth/me": {
|
||||
"get": {
|
||||
"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.",
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
|
||||
147
admin/src/api/schema.d.ts
vendored
147
admin/src/api/schema.d.ts
vendored
@@ -210,6 +210,127 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/account/preferences": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["cabana.Envelope-cabana_AdminPreferences"];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
|
||||
};
|
||||
};
|
||||
/** @description Forbidden */
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
put: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** @description Preferences to change */
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["cabana.AdminPreferencesRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["cabana.Envelope-cabana_AdminPreferences"];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
|
||||
};
|
||||
};
|
||||
/** @description Forbidden */
|
||||
403: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
|
||||
};
|
||||
};
|
||||
/** @description Request Entity Too Large */
|
||||
413: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
|
||||
};
|
||||
};
|
||||
/** @description Unprocessable Entity */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["cabana.ErrorEnvelope"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/account/tokens": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -576,7 +697,7 @@ export interface paths {
|
||||
};
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
get: {
|
||||
parameters: {
|
||||
@@ -5344,6 +5465,24 @@ export interface components {
|
||||
"cabana.AdminMarkdownPreviewResult": {
|
||||
html: string;
|
||||
};
|
||||
"cabana.AdminPreferences": {
|
||||
/**
|
||||
* @description Locale is an admin locale code from GET /lang meta.locales, or "" when
|
||||
* the admin follows the request locale.
|
||||
*/
|
||||
locale: string;
|
||||
/** @description Theme is system, light or dark; system (follow the browser) when unset. */
|
||||
theme: string;
|
||||
};
|
||||
"cabana.AdminPreferencesRequest": {
|
||||
/**
|
||||
* @description Locale is "" (follow the request locale) or a code from GET /lang
|
||||
* meta.locales.
|
||||
*/
|
||||
locale?: string;
|
||||
/** @description Theme is system, light or dark. */
|
||||
theme?: string;
|
||||
};
|
||||
"cabana.AdminProfile": {
|
||||
/** @description Avatar is present when the admin has a public avatar. */
|
||||
avatar?: components["schemas"]["cabana.AdminAvatar"];
|
||||
@@ -5353,6 +5492,8 @@ export interface components {
|
||||
is_superuser: boolean;
|
||||
last_name: string;
|
||||
login: string;
|
||||
/** @description Preferences are the admin's own UI preferences (theme and language). */
|
||||
preferences: components["schemas"]["cabana.AdminPreferences"];
|
||||
role?: components["schemas"]["cabana.AdminRoleSummary"];
|
||||
/** @description Subtitle is backend_users.metadata.subtitle; "" when not set. */
|
||||
subtitle: string;
|
||||
@@ -5477,6 +5618,10 @@ export interface components {
|
||||
data: components["schemas"]["cabana.AdminMarkdownPreviewResult"];
|
||||
meta: components["schemas"]["cabana.SuccessMeta"];
|
||||
};
|
||||
"cabana.Envelope-cabana_AdminPreferences": {
|
||||
data: components["schemas"]["cabana.AdminPreferences"];
|
||||
meta: components["schemas"]["cabana.SuccessMeta"];
|
||||
};
|
||||
"cabana.Envelope-cabana_AdminProfile": {
|
||||
data: components["schemas"]["cabana.AdminProfile"];
|
||||
meta: components["schemas"]["cabana.SuccessMeta"];
|
||||
|
||||
@@ -11,6 +11,7 @@ export type AdminLoginData = Schemas['cabana.AdminLoginData']
|
||||
export type AdminLoginRequest = Schemas['cabana.AdminLoginRequest']
|
||||
export type AdminLogoutData = Schemas['cabana.AdminLogoutData']
|
||||
export type AdminProfile = Schemas['cabana.AdminProfile']
|
||||
export type AdminPreferences = Schemas['cabana.AdminPreferences']
|
||||
/** An admin's avatar: the public image URL and its 250x250 thumbnail URL. */
|
||||
export type AdminAvatar = Schemas['cabana.AdminAvatar']
|
||||
/** Body of PUT /account: the only keys My account may change. */
|
||||
|
||||
@@ -28,6 +28,7 @@ export const profile = {
|
||||
subtitle: '',
|
||||
is_superuser: false,
|
||||
role: { id: 2, code: 'developer', name: 'Developer' },
|
||||
preferences: { theme: 'system', locale: '' },
|
||||
},
|
||||
meta: {},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user