From d97b3f8da15c8fbcd528b584c69d23fe39e94fa3 Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 17 Aug 2023 00:49:46 +0200 Subject: [PATCH] feat(backend): add /api/v2/users/@me/settings --- backend/db/user.go | 1 + backend/db/user_settings.go | 26 ++++++++++++++ backend/routes.go | 6 +++- backend/routes/v2/user/get_settings.go | 21 +++++++++++ backend/routes/v2/user/patch_settings.go | 45 ++++++++++++++++++++++++ backend/routes/v2/user/routes.go | 23 ++++++++++++ go.mod | 2 ++ go.sum | 4 +++ scripts/migrate/020_settings.sql | 9 +++++ 9 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 backend/db/user_settings.go create mode 100644 backend/routes/v2/user/get_settings.go create mode 100644 backend/routes/v2/user/patch_settings.go create mode 100644 backend/routes/v2/user/routes.go create mode 100644 scripts/migrate/020_settings.sql diff --git a/backend/db/user.go b/backend/db/user.go index 2389a56..95a3355 100644 --- a/backend/db/user.go +++ b/backend/db/user.go @@ -54,6 +54,7 @@ type User struct { ListPrivate bool LastSIDReroll time.Time `db:"last_sid_reroll"` Timezone *string + Settings UserSettings DeletedAt *time.Time SelfDelete *bool diff --git a/backend/db/user_settings.go b/backend/db/user_settings.go new file mode 100644 index 0000000..7411821 --- /dev/null +++ b/backend/db/user_settings.go @@ -0,0 +1,26 @@ +package db + +import ( + "context" + + "emperror.dev/errors" + "github.com/rs/xid" +) + +type UserSettings struct { + ReadChangelog string `json:"read_changelog"` + ReadSettingsNotice string `json:"read_settings_notice"` +} + +func (db *DB) UpdateUserSettings(ctx context.Context, id xid.ID, us UserSettings) error { + sql, args, err := sq.Update("users").Set("settings", us).Where("id = ?", id).ToSql() + if err != nil { + return errors.Wrap(err, "building sql") + } + + _, err = db.Exec(ctx, sql, args...) + if err != nil { + return errors.Wrap(err, "executing query") + } + return nil +} diff --git a/backend/routes.go b/backend/routes.go index c95da85..7ff0cca 100644 --- a/backend/routes.go +++ b/backend/routes.go @@ -9,6 +9,7 @@ import ( "codeberg.org/pronounscc/pronouns.cc/backend/routes/v1/meta" "codeberg.org/pronounscc/pronouns.cc/backend/routes/v1/mod" "codeberg.org/pronounscc/pronouns.cc/backend/routes/v1/user" + user2 "codeberg.org/pronounscc/pronouns.cc/backend/routes/v2/user" "codeberg.org/pronounscc/pronouns.cc/backend/server" "github.com/go-chi/chi/v5" "github.com/go-chi/render" @@ -22,7 +23,6 @@ var openapi string // mountRoutes mounts all API routes on the server's router. // they are all mounted under /v1/ func mountRoutes(s *server.Server) { - // future-proofing for API versions s.Router.Route("/v1", func(r chi.Router) { auth.Mount(s, r) user.Mount(s, r) @@ -32,6 +32,10 @@ func mountRoutes(s *server.Server) { mod.Mount(s, r) }) + s.Router.Route("/v2", func(r chi.Router) { + user2.Mount(s, r) + }) + // API docs s.Router.Get("/", func(w http.ResponseWriter, r *http.Request) { render.HTML(w, r, openapi) diff --git a/backend/routes/v2/user/get_settings.go b/backend/routes/v2/user/get_settings.go new file mode 100644 index 0000000..d618831 --- /dev/null +++ b/backend/routes/v2/user/get_settings.go @@ -0,0 +1,21 @@ +package user + +import ( + "net/http" + + "codeberg.org/pronounscc/pronouns.cc/backend/log" + "codeberg.org/pronounscc/pronouns.cc/backend/server" + "github.com/go-chi/render" +) + +func (s *Server) GetSettings(w http.ResponseWriter, r *http.Request) (err error) { + claims, _ := server.ClaimsFromContext(r.Context()) + u, err := s.DB.User(r.Context(), claims.UserID) + if err != nil { + log.Errorf("getting user: %v", err) + return err + } + + render.JSON(w, r, u.Settings) + return nil +} diff --git a/backend/routes/v2/user/patch_settings.go b/backend/routes/v2/user/patch_settings.go new file mode 100644 index 0000000..777fe09 --- /dev/null +++ b/backend/routes/v2/user/patch_settings.go @@ -0,0 +1,45 @@ +package user + +import ( + "net/http" + + "codeberg.org/pronounscc/pronouns.cc/backend/server" + "emperror.dev/errors" + "github.com/aarondl/opt/omitnull" + "github.com/go-chi/render" +) + +type PatchSettingsRequest struct { + ReadChangelog omitnull.Val[string] `json:"read_changelog"` + ReadSettingsNotice omitnull.Val[string] `json:"read_settings_notice"` +} + +func (s *Server) PatchSettings(w http.ResponseWriter, r *http.Request) (err error) { + ctx := r.Context() + claims, _ := server.ClaimsFromContext(ctx) + u, err := s.DB.User(ctx, claims.UserID) + if err != nil { + return errors.Wrap(err, "getting user") + } + + var req PatchSettingsRequest + err = render.Decode(r, &req) + if err != nil { + return server.APIError{Code: server.ErrBadRequest} + } + + if !req.ReadChangelog.IsUnset() { + u.Settings.ReadChangelog = req.ReadChangelog.GetOrZero() + } + if !req.ReadSettingsNotice.IsUnset() { + u.Settings.ReadSettingsNotice = req.ReadSettingsNotice.GetOrZero() + } + + err = s.DB.UpdateUserSettings(ctx, u.ID, u.Settings) + if err != nil { + return errors.Wrap(err, "updating user settings") + } + + render.JSON(w, r, u.Settings) + return nil +} diff --git a/backend/routes/v2/user/routes.go b/backend/routes/v2/user/routes.go new file mode 100644 index 0000000..2040e22 --- /dev/null +++ b/backend/routes/v2/user/routes.go @@ -0,0 +1,23 @@ +package user + +import ( + "codeberg.org/pronounscc/pronouns.cc/backend/server" + "github.com/go-chi/chi/v5" +) + +type Server struct { + *server.Server +} + +func Mount(srv *server.Server, r chi.Router) { + s := &Server{ + Server: srv, + } + + r.Route("/users", func(r chi.Router) { + r.With(server.MustAuth).Group(func(r chi.Router) { + r.Get("/@me/settings", server.WrapHandler(s.GetSettings)) + r.Patch("/@me/settings", server.WrapHandler(s.PatchSettings)) + }) + }) +} diff --git a/go.mod b/go.mod index bf622b7..862413f 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.18 require ( emperror.dev/errors v0.8.1 github.com/Masterminds/squirrel v1.5.4 + github.com/aarondl/opt v0.0.0-20230313190023-85d93d668fec github.com/bwmarrin/discordgo v0.27.1 github.com/davidbyttow/govips/v2 v2.13.0 github.com/georgysavva/scany/v2 v2.0.0 @@ -32,6 +33,7 @@ require ( require ( cloud.google.com/go/compute v1.19.0 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect + github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf // indirect github.com/ajg/form v1.5.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect diff --git a/go.sum b/go.sum index 28bca5e..3476804 100644 --- a/go.sum +++ b/go.sum @@ -54,6 +54,10 @@ github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8 github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/a8m/expect v1.0.0/go.mod h1:4IwSCMumY49ScypDnjNbYEjgVeqy1/U2cEs3Lat96eA= +github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf h1:+edM69bH/X6JpYPmJYBRLanAMe1V5yRXYU3hHUovGcE= +github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf/go.mod h1:FZqLhJSj2tg0ZN48GB1zvj00+ZYcHPqgsC7yzcgCq6k= +github.com/aarondl/opt v0.0.0-20230313190023-85d93d668fec h1:2NFk5fe52cHyRcUnXSs4CSEAqm+rL/hr3AdflBE3VPU= +github.com/aarondl/opt v0.0.0-20230313190023-85d93d668fec/go.mod h1:l4/5NZtYd/SIohsFhaJQQe+sPOTG22furpZ5FvcYOzk= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= diff --git a/scripts/migrate/020_settings.sql b/scripts/migrate/020_settings.sql new file mode 100644 index 0000000..d8516ae --- /dev/null +++ b/scripts/migrate/020_settings.sql @@ -0,0 +1,9 @@ +-- 2023-08-16: Server-side user settings storage (for now, just whether specific notices are dismissed) + +-- +migrate Up + +alter table users add column settings jsonb not null default '{}'; + +-- +migrate Down + +alter table users drop column settings;