diff --git a/internal/home/config.go b/internal/home/config.go index ddefd61ed94..dd2f0730635 100644 --- a/internal/home/config.go +++ b/internal/home/config.go @@ -106,6 +106,8 @@ type configuration struct { ProxyURL string `yaml:"http_proxy"` // Language is a two-letter ISO 639-1 language code. Language string `yaml:"language"` + // Theme is a UI theme for current user. + Theme Theme `yaml:"theme"` // DebugPProf defines if the profiling HTTP handler will listen on :6060. DebugPProf bool `yaml:"debug_pprof"` @@ -322,6 +324,7 @@ var config = &configuration{ }, OSConfig: &osConfig{}, SchemaVersion: currentSchemaVersion, + Theme: ThemeAuto, } // getConfigFilename returns path to the current config file diff --git a/internal/home/control.go b/internal/home/control.go index f9e7d4d2afa..ef67a7f74fd 100644 --- a/internal/home/control.go +++ b/internal/home/control.go @@ -149,19 +149,6 @@ func handleStatus(w http.ResponseWriter, r *http.Request) { _ = aghhttp.WriteJSONResponse(w, r, resp) } -type profileJSON struct { - Name string `json:"name"` -} - -func handleGetProfile(w http.ResponseWriter, r *http.Request) { - u := Context.auth.getCurrentUser(r) - resp := &profileJSON{ - Name: u.Name, - } - - _ = aghhttp.WriteJSONResponse(w, r, resp) -} - // ------------------------ // registration of handlers // ------------------------ @@ -172,6 +159,7 @@ func registerControlHandlers() { Context.mux.HandleFunc("/control/version.json", postInstall(optionalAuth(handleGetVersionJSON))) httpRegister(http.MethodPost, "/control/update", handleUpdate) httpRegister(http.MethodGet, "/control/profile", handleGetProfile) + httpRegister(http.MethodPut, "/control/profile/update", handlePutProfile) // No auth is necessary for DoH/DoT configurations Context.mux.HandleFunc("/apple/doh.mobileconfig", postInstall(handleMobileConfigDoH)) diff --git a/internal/home/i18n.go b/internal/home/i18n.go index cc8da8fe6e4..d9e3435c826 100644 --- a/internal/home/i18n.go +++ b/internal/home/i18n.go @@ -54,6 +54,7 @@ type languageJSON struct { Language string `json:"language"` } +// TODO(d.kolyshev): Deprecated, remove it later. func handleI18nCurrentLanguage(w http.ResponseWriter, r *http.Request) { log.Printf("home: language is %s", config.Language) @@ -62,6 +63,7 @@ func handleI18nCurrentLanguage(w http.ResponseWriter, r *http.Request) { }) } +// TODO(d.kolyshev): Deprecated, remove it later. func handleI18nChangeLanguage(w http.ResponseWriter, r *http.Request) { if aghhttp.WriteTextPlainDeprecated(w, r) { return diff --git a/internal/home/profilehttp.go b/internal/home/profilehttp.go new file mode 100644 index 00000000000..12e036c3734 --- /dev/null +++ b/internal/home/profilehttp.go @@ -0,0 +1,102 @@ +package home + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/AdguardTeam/AdGuardHome/internal/aghhttp" + "github.com/AdguardTeam/golibs/log" +) + +// Theme is an enum of all allowed UI themes. +type Theme string + +// Allowed [Theme] values. +// +// Keep in sync with client/src/helpers/constants.js. +const ( + ThemeAuto Theme = "auto" + ThemeLight Theme = "light" + ThemeDark Theme = "dark" +) + +// UnmarshalText implements [encoding.TextUnmarshaler] interface for *Theme. +func (t *Theme) UnmarshalText(b []byte) (err error) { + switch string(b) { + case "auto": + *t = ThemeAuto + case "dark": + *t = ThemeDark + case "light": + *t = ThemeLight + default: + return fmt.Errorf("invalid theme %q, supported: %q, %q, %q", b, ThemeAuto, ThemeDark, ThemeLight) + } + + return nil +} + +// profileJSON is an object for /control/profile and /control/profile/update +// endpoints. +type profileJSON struct { + Name string `json:"name"` + Language string `json:"language"` + Theme Theme `json:"theme"` +} + +// handleGetProfile is the handler for GET /control/profile endpoint. +func handleGetProfile(w http.ResponseWriter, r *http.Request) { + u := Context.auth.getCurrentUser(r) + + var resp profileJSON + func() { + config.RLock() + defer config.RUnlock() + + resp = profileJSON{ + Name: u.Name, + Language: config.Language, + Theme: config.Theme, + } + }() + + _ = aghhttp.WriteJSONResponse(w, r, resp) +} + +// handlePutProfile is the handler for PUT /control/profile/update endpoint. +func handlePutProfile(w http.ResponseWriter, r *http.Request) { + if aghhttp.WriteTextPlainDeprecated(w, r) { + return + } + + profileReq := &profileJSON{} + err := json.NewDecoder(r.Body).Decode(profileReq) + if err != nil { + aghhttp.Error(r, w, http.StatusBadRequest, "reading req: %s", err) + + return + } + + lang := profileReq.Language + if !allowedLanguages.Has(lang) { + aghhttp.Error(r, w, http.StatusBadRequest, "unknown language: %q", lang) + + return + } + + theme := profileReq.Theme + + func() { + config.Lock() + defer config.Unlock() + + config.Language = lang + config.Theme = theme + log.Printf("home: language is set to %s", lang) + log.Printf("home: theme is set to %s", theme) + }() + + onConfigModified() + aghhttp.OK(w) +} diff --git a/openapi/CHANGELOG.md b/openapi/CHANGELOG.md index cc30989d2af..b583ad60c5d 100644 --- a/openapi/CHANGELOG.md +++ b/openapi/CHANGELOG.md @@ -6,6 +6,33 @@ +## v0.107.22: API changes + +### `POST /control/i18n/change_language` is deprecated + +Use `PUT /control/profile/update`. + +### `GET /control/i18n/current_language` is deprecated + +Use `GET /control/profile`. + +* The `/control/profile` HTTP API has been changed. + +* The new `PUT /control/profile/update` HTTP API allows user info updates. + +These `control/profile/update` and `control/profile` APIs accept and return a +JSON object with the following format: + +```json +{ + "name":"user name", + "language": "en", + "theme": "auto" +} +``` + + + ## v0.107.20: API Changes ### `POST /control/cache_clear` diff --git a/openapi/openapi.yaml b/openapi/openapi.yaml index b3135477b63..68401830ea0 100644 --- a/openapi/openapi.yaml +++ b/openapi/openapi.yaml @@ -962,6 +962,9 @@ 'description': 'OK.' '/i18n/change_language': 'post': + 'deprecated': true + 'description': > + Deprecated: Use `PUT /control/profile` instead. 'tags': - 'i18n' 'operationId': 'changeLanguage' @@ -980,6 +983,9 @@ 'description': 'OK.' '/i18n/current_language': 'get': + 'deprecated': true + 'description': > + Deprecated: Use `GET /control/profile` instead. 'tags': - 'i18n' 'operationId': 'currentLanguage' @@ -1145,6 +1151,20 @@ 'responses': '302': 'description': 'OK.' + '/profile/update': + 'put': + 'tags': + - 'global' + 'operationId': 'updateProfile' + 'summary': 'Updates current user info' + 'requestBody': + 'content': + 'application/json': + 'schema': + '$ref': '#/components/schemas/ProfileInfo' + 'responses': + '200': + 'description': 'OK' '/profile': 'get': 'tags': @@ -2335,6 +2355,19 @@ 'properties': 'name': 'type': 'string' + 'language': + 'type': 'string' + 'theme': + 'type': 'string' + 'description': 'Interface theme' + 'enum': + - 'auto' + - 'dark' + - 'light' + 'required': + - 'name' + - 'language' + - 'theme' 'Client': 'type': 'object' 'description': 'Client information.'