diff options
-rw-r--r-- | pkg/database/accounts.go | 18 | ||||
-rw-r--r-- | pkg/database/sql/000_init.sql | 2 | ||||
-rw-r--r-- | pkg/model/account.go | 3 | ||||
-rw-r--r-- | pkg/model/settings.go | 7 | ||||
-rw-r--r-- | pkg/webui/html/base.html | 6 | ||||
-rw-r--r-- | pkg/webui/html/settings.html | 25 | ||||
-rw-r--r-- | pkg/webui/index.go | 9 | ||||
-rw-r--r-- | pkg/webui/login.go | 7 | ||||
-rw-r--r-- | pkg/webui/routes.go | 2 | ||||
-rw-r--r-- | pkg/webui/settings.go | 53 | ||||
-rw-r--r-- | pkg/webui/state.go | 6 | ||||
-rw-r--r-- | pkg/webui/states.go | 4 | ||||
-rw-r--r-- | pkg/webui/version.go | 8 |
13 files changed, 136 insertions, 14 deletions
diff --git a/pkg/database/accounts.go b/pkg/database/accounts.go index 9adb32d..e6363f1 100644 --- a/pkg/database/accounts.go +++ b/pkg/database/accounts.go @@ -2,6 +2,7 @@ package database import ( "database/sql" + "encoding/json" "errors" "fmt" "log/slog" @@ -31,12 +32,13 @@ func (db *DB) InitAdminAccount() error { salt := helpers.GenerateSalt() hash := helpers.HashPassword(password.String(), salt) if _, err := tx.ExecContext(db.ctx, - `INSERT INTO accounts(username, salt, password_hash, is_admin) - VALUES ("admin", :salt, :hash, TRUE) + `INSERT INTO accounts(username, salt, password_hash, is_admin, settings) + VALUES ("admin", :salt, :hash, TRUE, :settings) ON CONFLICT DO UPDATE SET password_hash = :hash WHERE username = "admin";`, sql.Named("salt", salt), sql.Named("hash", hash), + []byte("{}"), ); err == nil { AdvertiseAdminPassword(password.String()) } else { @@ -136,6 +138,18 @@ func (db *DB) LoadAccountByUsername(username string) (*model.Account, error) { return &account, nil } +func (db *DB) SaveAccountSettings(account *model.Account, settings *model.Settings) error { + data, err := json.Marshal(settings) + if err != nil { + return fmt.Errorf("failed to marshal settings for user %s: %w", account.Username, err) + } + _, err = db.Exec(`UPDATE accounts SET settings = ? WHERE id = ?`, data, account.Id) + if err != nil { + return fmt.Errorf("failed to update settings for user %s: %w", account.Username, err) + } + return nil +} + func (db *DB) TouchAccount(account *model.Account) error { now := time.Now().UTC() _, err := db.Exec(`UPDATE accounts SET last_login = ? WHERE id = ?`, now.Unix(), account.Id) diff --git a/pkg/database/sql/000_init.sql b/pkg/database/sql/000_init.sql index 0bcdee1..80e8c8c 100644 --- a/pkg/database/sql/000_init.sql +++ b/pkg/database/sql/000_init.sql @@ -10,7 +10,7 @@ CREATE TABLE accounts ( is_admin INTEGER NOT NULL DEFAULT FALSE, created INTEGER NOT NULL DEFAULT (unixepoch()), last_login INTEGER NOT NULL DEFAULT (unixepoch()), - settings TEXT + settings BLOB NOT NULL ) STRICT; CREATE UNIQUE INDEX accounts_username on accounts(username); diff --git a/pkg/model/account.go b/pkg/model/account.go index 7a69685..5055943 100644 --- a/pkg/model/account.go +++ b/pkg/model/account.go @@ -2,6 +2,7 @@ package model import ( "crypto/subtle" + "encoding/json" "time" "git.adyxax.org/adyxax/tfstated/pkg/helpers" @@ -17,7 +18,7 @@ type Account struct { IsAdmin bool Created time.Time LastLogin time.Time - Settings any + Settings json.RawMessage } func (account *Account) CheckPassword(password string) bool { diff --git a/pkg/model/settings.go b/pkg/model/settings.go new file mode 100644 index 0000000..9da655b --- /dev/null +++ b/pkg/model/settings.go @@ -0,0 +1,7 @@ +package model + +type SettingsContextKey struct{} + +type Settings struct { + LightMode bool `json:"light_mode"` +} diff --git a/pkg/webui/html/base.html b/pkg/webui/html/base.html index c0138ac..4ec6565 100644 --- a/pkg/webui/html/base.html +++ b/pkg/webui/html/base.html @@ -16,6 +16,10 @@ <i>home_storage</i> <span>States</span> </a> +<a href="/settings"{{ if eq .Page.Section "settings" }} class="fill"{{ end}}> + <i>settings</i> + <span>Settings</span> +</a> <a href="/logout"> <i>logout</i> <span>Logout</span> @@ -32,7 +36,7 @@ <link href="https://cdn.jsdelivr.net/npm/beercss@3.8.0/dist/cdn/beer.min.css" rel="stylesheet"> <title>TFSTATED - {{ .Page.Title }}</title> </head> - <body class="dark"> + <body class="{{ if .Page.LightMode }}light{{ else }}dark{{ end }}"> <nav class="left drawer l">{{ template "nav" . }}</nav> <nav class="left m">{{ template "nav" . }}</nav> <nav class="bottom s">{{ template "nav" . }}</nav> diff --git a/pkg/webui/html/settings.html b/pkg/webui/html/settings.html new file mode 100644 index 0000000..4040b9b --- /dev/null +++ b/pkg/webui/html/settings.html @@ -0,0 +1,25 @@ +{{ define "main" }} +<main class="responsive"> + <form action="/settings" method="post"> + <fieldset> + <div class="field middle-align"> + <nav> + <div class="max"> + <h6>Dark Mode</h6> + </div> + <label class="switch icon"> + <input {{ if not .Settings.LightMode }} checked{{ end }} + name="dark-mode" + type="checkbox" + value="1" /> + <span> + <i>dark_mode</i> + </span> + </label> + </nav> + </div> + <button class="small-round" type="submit" value="login">Save</button> + </fieldset> + </form> +</main> +{{ end }} diff --git a/pkg/webui/index.go b/pkg/webui/index.go index 89e5ad6..1168098 100644 --- a/pkg/webui/index.go +++ b/pkg/webui/index.go @@ -3,14 +3,23 @@ package webui import ( "fmt" "net/http" + + "git.adyxax.org/adyxax/tfstated/pkg/model" ) type Page struct { + LightMode bool Precedent string Section string Title string } +func makePage(r *http.Request, page *Page) *Page { + settings := r.Context().Value(model.SettingsContextKey{}).(*model.Settings) + page.LightMode = settings.LightMode + return page +} + func handleIndexGET() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { diff --git a/pkg/webui/login.go b/pkg/webui/login.go index 6f688c1..18864b2 100644 --- a/pkg/webui/login.go +++ b/pkg/webui/login.go @@ -2,8 +2,10 @@ package webui import ( "context" + "encoding/json" "fmt" "html/template" + "log/slog" "net/http" "regexp" @@ -113,6 +115,11 @@ func loginMiddleware(db *database.DB, requireSession func(http.Handler) http.Han return } ctx := context.WithValue(r.Context(), model.AccountContextKey{}, account) + var settings model.Settings + if err := json.Unmarshal(account.Settings, &settings); err != nil { + slog.Error("failed to unmarshal account settings", "err", err, "accountId", account.Id) + } + ctx = context.WithValue(ctx, model.SettingsContextKey{}, &settings) next.ServeHTTP(w, r.WithContext(ctx)) })) } diff --git a/pkg/webui/routes.go b/pkg/webui/routes.go index 84017dd..e84f33a 100644 --- a/pkg/webui/routes.go +++ b/pkg/webui/routes.go @@ -16,6 +16,8 @@ func addRoutes( mux.Handle("GET /login", requireSession(handleLoginGET())) mux.Handle("POST /login", requireSession(handleLoginPOST(db))) mux.Handle("GET /logout", requireLogin(handleLogoutGET(db))) + mux.Handle("GET /settings", requireLogin(handleSettingsGET(db))) + mux.Handle("POST /settings", requireLogin(handleSettingsPOST(db))) mux.Handle("GET /states", requireLogin(handleStatesGET(db))) mux.Handle("GET /state/{id}", requireLogin(handleStateGET(db))) mux.Handle("GET /static/", cache(http.FileServer(http.FS(staticFS)))) diff --git a/pkg/webui/settings.go b/pkg/webui/settings.go new file mode 100644 index 0000000..eb0910f --- /dev/null +++ b/pkg/webui/settings.go @@ -0,0 +1,53 @@ +package webui + +import ( + "html/template" + "net/http" + + "git.adyxax.org/adyxax/tfstated/pkg/database" + "git.adyxax.org/adyxax/tfstated/pkg/model" +) + +type SettingsPage struct { + Page *Page + Settings *model.Settings +} + +var settingsTemplates = template.Must(template.ParseFS(htmlFS, "html/base.html", "html/settings.html")) + +func handleSettingsGET(db *database.DB) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + settings := r.Context().Value(model.SettingsContextKey{}).(*model.Settings) + render(w, settingsTemplates, http.StatusOK, SettingsPage{ + Page: makePage(r, &Page{Title: "Settings", Section: "settings"}), + Settings: settings, + }) + }) +} + +func handleSettingsPOST(db *database.DB) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := r.ParseForm(); err != nil { + errorResponse(w, http.StatusBadRequest, err) + return + } + darkMode := r.FormValue("dark-mode") + settings := model.Settings{ + LightMode: darkMode != "1", + } + account := r.Context().Value(model.AccountContextKey{}).(*model.Account) + err := db.SaveAccountSettings(account, &settings) + if err != nil { + errorResponse(w, http.StatusInternalServerError, err) + return + } + render(w, settingsTemplates, http.StatusOK, SettingsPage{ + Page: &Page{ + LightMode: settings.LightMode, + Title: "Settings", + Section: "settings", + }, + Settings: &settings, + }) + }) +} diff --git a/pkg/webui/state.go b/pkg/webui/state.go index c7a6aaf..141d2d8 100644 --- a/pkg/webui/state.go +++ b/pkg/webui/state.go @@ -13,7 +13,7 @@ var stateTemplate = template.Must(template.ParseFS(htmlFS, "html/base.html", "ht func handleStateGET(db *database.DB) http.Handler { type StatesData struct { - Page + Page *Page State *model.State Usernames map[int]string Versions []model.Version @@ -41,11 +41,11 @@ func handleStateGET(db *database.DB) http.Handler { return } render(w, stateTemplate, http.StatusOK, StatesData{ - Page: Page{ + Page: makePage(r, &Page{ Precedent: "/states", Section: "states", Title: state.Path, - }, + }), State: state, Usernames: usernames, Versions: versions, diff --git a/pkg/webui/states.go b/pkg/webui/states.go index d99d310..a0d16ca 100644 --- a/pkg/webui/states.go +++ b/pkg/webui/states.go @@ -12,7 +12,7 @@ var statesTemplates = template.Must(template.ParseFS(htmlFS, "html/base.html", " func handleStatesGET(db *database.DB) http.Handler { type StatesData struct { - Page + Page *Page States []model.State } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -22,7 +22,7 @@ func handleStatesGET(db *database.DB) http.Handler { return } render(w, statesTemplates, http.StatusOK, StatesData{ - Page: Page{Title: "States", Section: "states"}, + Page: makePage(r, &Page{Title: "States", Section: "states"}), States: states, }) }) diff --git a/pkg/webui/version.go b/pkg/webui/version.go index 04c3e6d..2aa1422 100644 --- a/pkg/webui/version.go +++ b/pkg/webui/version.go @@ -14,7 +14,7 @@ var versionTemplate = template.Must(template.ParseFS(htmlFS, "html/base.html", " func handleVersionGET(db *database.DB) http.Handler { type VersionsData struct { - Page + Page *Page Account *model.Account State *model.State Version *model.Version @@ -44,11 +44,11 @@ func handleVersionGET(db *database.DB) http.Handler { } versionData := string(version.Data[:]) render(w, versionTemplate, http.StatusOK, VersionsData{ - Page: Page{ + Page: makePage(r, &Page{ Precedent: fmt.Sprintf("/state/%d", state.Id), - Section: "versions", + Section: "states", Title: state.Path, - }, + }), Account: account, State: state, Version: version, |