From 3ce55bb8707a671dc527ee7bff8636956f36d610 Mon Sep 17 00:00:00 2001 From: yequari Date: Fri, 9 May 2025 20:24:53 -0700 Subject: [PATCH] user settings --- cmd/web/handlers_user.go | 2 +- db/create-settings-tables.sql | 4 +- internal/models/errors.go | 8 +- internal/models/settings.go | 202 ++++++++++++++++++++++++++++++++++ internal/models/user.go | 30 +++-- 5 files changed, 230 insertions(+), 16 deletions(-) create mode 100644 internal/models/settings.go diff --git a/cmd/web/handlers_user.go b/cmd/web/handlers_user.go index 18e04bb..45f2087 100644 --- a/cmd/web/handlers_user.go +++ b/cmd/web/handlers_user.go @@ -147,7 +147,7 @@ func (app *application) putUserSettings(w http.ResponseWriter, r *http.Request) } form.CheckField(validator.PermittedValue(form.LocalTimezone, app.timezones...), "timezone", "Invalid value") if !form.Valid() { - // rerender template with errors + // TODO: rerender template with errors app.clientError(w, http.StatusUnprocessableEntity) } err = app.users.SetLocalTimezone(userId, form.LocalTimezone) diff --git a/db/create-settings-tables.sql b/db/create-settings-tables.sql index 07e19f1..4708887 100644 --- a/db/create-settings-tables.sql +++ b/db/create-settings-tables.sql @@ -38,7 +38,7 @@ CREATE TABLE user_settings ( Id integer primary key autoincrement, UserId integer NOT NULL, SettingId integer NOT NULL, - AllowedSettingValueId integer NOT NULL, + AllowedSettingValueId integer, UnconstrainedValue varchar(256), FOREIGN KEY (UserId) REFERENCES users(Id) ON DELETE RESTRICT @@ -55,7 +55,7 @@ CREATE TABLE guestbook_settings ( Id integer primary key autoincrement, GuestbookId integer NOT NULL, SettingId integer NOT NULL, - AllowedSettingValueId integer NOT NULL, + AllowedSettingValueId integer, UnconstrainedValue varchar(256), FOREIGN KEY (GuestbookId) REFERENCES guestbooks(Id) ON DELETE RESTRICT diff --git a/internal/models/errors.go b/internal/models/errors.go index 48a3033..2b02a0b 100644 --- a/internal/models/errors.go +++ b/internal/models/errors.go @@ -3,9 +3,11 @@ package models import "errors" var ( - ErrNoRecord = errors.New("models: no matching record found") + ErrNoRecord = errors.New("models: no matching record found") - ErrInvalidCredentials = errors.New("models: invalid credentials") + ErrInvalidCredentials = errors.New("models: invalid credentials") - ErrDuplicateEmail = errors.New("models: duplicate email") + ErrDuplicateEmail = errors.New("models: duplicate email") + + ErrInvalidSettingValue = errors.New("models: invalid setting value") ) diff --git a/internal/models/settings.go b/internal/models/settings.go new file mode 100644 index 0000000..4e976c3 --- /dev/null +++ b/internal/models/settings.go @@ -0,0 +1,202 @@ +package models + +import ( + "database/sql" + "maps" + "strconv" + "time" +) + +type SettingGroup int + +const ( + SettingGroupUser SettingGroup = 1 + SettingGroupGuestbook SettingGroup = 2 +) + +type SettingDataType int + +const ( + SettingTypeInt SettingDataType = 1 + SettingTypeDate SettingDataType = 2 + SettingTypeAlphanum SettingDataType = 3 +) + +type SettingValue struct { + Id int + SettingId int + AllowedSettingId int + UnconstrainedValue string +} + +type AllowedSettingValue struct { + id int + itemValue string + caption string +} + +func (s *AllowedSettingValue) Id() int { + return s.id +} + +func (s *AllowedSettingValue) ItemValue() string { + return s.itemValue +} + +func (s *AllowedSettingValue) Caption() string { + return s.caption +} + +type Setting struct { + id int + description string + constrained bool + dataType SettingDataType + settingGroup SettingGroup + minValue string // TODO: Maybe should be int? + maxValue string + allowedValues map[int]AllowedSettingValue +} + +func (s *Setting) Id() int { + return s.id +} + +func (s *Setting) Description() string { + return s.description +} + +func (s *Setting) Constrained() bool { + return s.constrained +} + +func (s *Setting) DataType() SettingDataType { + return s.dataType +} + +func (s *Setting) SettingGroup() SettingGroup { + return s.settingGroup +} + +func (s *Setting) MinValue() string { + return s.minValue +} + +func (s *Setting) MaxValue() string { + return s.maxValue +} + +func (s *Setting) AllowedValues() map[int]AllowedSettingValue { + result := make(map[int]AllowedSettingValue, 50) + maps.Copy(result, s.allowedValues) + return result +} + +func (s *Setting) ValidateUnconstrained(value string) bool { + switch s.dataType { + case SettingTypeInt: + return s.validateInt(value) + case SettingTypeAlphanum: + return s.validateAlphanum(value) + case SettingTypeDate: + return s.validateDatetime(value) + } + return false +} + +func (s *Setting) ValidateConstrained(value *AllowedSettingValue) bool { + _, exists := s.allowedValues[value.id] + if s.constrained && exists { + return true + } + return false +} + +func (s *Setting) validateInt(value string) bool { + v, err := strconv.ParseInt(value, 10, 0) + if err != nil { + return false + } + var min int64 + var max int64 + if len(s.minValue) > 0 { + min, err = strconv.ParseInt(s.minValue, 10, 0) + if err != nil { + return false + } + if v < min { + return false + } + } + if len(s.maxValue) > 0 { + max, err = strconv.ParseInt(s.maxValue, 10, 0) + if err != nil { + return false + } + if v < max { + return false + } + } + return true +} + +func (s *Setting) validateDatetime(value string) bool { + v, err := time.Parse(time.DateTime, value) + if err != nil { + return false + } + var min time.Time + var max time.Time + + if len(s.minValue) > 0 { + min, err = time.Parse(time.DateTime, s.minValue) + if err != nil { + return false + } + if v.Before(min) { + return false + } + } + if len(s.maxValue) > 0 { + max, err = time.Parse(time.DateTime, s.maxValue) + if err != nil { + return false + } + if v.After(max) { + return false + } + } + return false +} + +func (s *Setting) validateAlphanum(value string) bool { + return true +} + +func validateSetting(db *sql.DB, settingId int, value string) (bool, error) { + stmt := `SELECT s.Id, Description, Constrained, DataType, SettingGroup, MinValue, MaxValue, a.Id + FROM settings AS s LEFT JOIN allowed_setting_values AS a ON s.Id = a.SettingId AND a.ItemValue = ? + WHERE s.Id = ?` + result := db.QueryRow(stmt, value, settingId) + + var s Setting + var minval sql.NullString + var maxval sql.NullString + var allowedId sql.NullInt64 + err := result.Scan(&s.id, &s.description, &s.constrained, &s.dataType, &s.settingGroup, &minval, &maxval, &allowedId) + if err != nil { + return false, err + } + if s.constrained && allowedId.Valid { + return true, nil + } + switch s.dataType { + case SettingTypeInt: + return s.validateInt(value), nil + case SettingTypeAlphanum: + return s.validateAlphanum(value), nil + case SettingTypeDate: + return s.validateDatetime(value), nil + } + return false, nil +} diff --git a/internal/models/user.go b/internal/models/user.go index 35ce1fd..3d6432e 100644 --- a/internal/models/user.go +++ b/internal/models/user.go @@ -54,13 +54,7 @@ func (m *UserModel) Insert(shortId uint64, username string, email string, passwo if err != nil { return err } - settingsStmt := `INSERT INTO user_settings - (UserId, SettingId, AllowedSettingValueId, UnconstrainedValue) - VALUES (?, ?, ?, ?)` - _, err = m.DB.Exec(settingsStmt, id, u_timezone, nil, settings.LocalTimezone.String()) - if err != nil { - return err - } + err = m.initializeUserSettings(id, settings) return nil } @@ -184,9 +178,25 @@ func (m *UserModel) GetSettings(userId int64) (UserSettings, error) { return settings, err } -func (m *UserModel) SetLocalTimezone(userId int64, timezone string) error { - stmt := `UPDATE user_settings SET UnconstrainedValue = ? WHERE UserId = ?` - _, err := m.DB.Exec(stmt, timezone, userId) +func (m *UserModel) initializeUserSettings(userId int64, settings UserSettings) error { + stmt := `INSERT INTO user_settings (UserId, SettingId, AllowedSettingValueId, UnconstrainedValue) VALUES (?, ?, ?, ?)` + _, err := m.DB.Exec(stmt, userId, u_timezone, nil, settings.LocalTimezone.String()) + if err != nil { + return err + } + return nil +} + +func (m *UserModel) SetLocalTimezone(userId int64, timezone string) error { + valid, err := validateSetting(m.DB, u_timezone, timezone) + if err != nil { + return err + } + if !valid { + return ErrInvalidSettingValue + } + stmt := `UPDATE user_settings SET UnconstrainedValue = ? WHERE UserId = ?` + _, err = m.DB.Exec(stmt, timezone, userId) if err != nil { return err }