Compare commits

..

3 Commits

Author SHA1 Message Date
8b681922f6 restructure settings code 2025-05-18 10:49:24 -07:00
2b9d0e11b8 generic setting update code 2025-05-09 23:59:54 -07:00
3ce55bb870 user settings 2025-05-09 20:24:53 -07:00
13 changed files with 384 additions and 29 deletions

View File

@ -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") form.CheckField(validator.PermittedValue(form.LocalTimezone, app.timezones...), "timezone", "Invalid value")
if !form.Valid() { if !form.Valid() {
// rerender template with errors // TODO: rerender template with errors
app.clientError(w, http.StatusUnprocessableEntity) app.clientError(w, http.StatusUnprocessableEntity)
} }
err = app.users.SetLocalTimezone(userId, form.LocalTimezone) err = app.users.SetLocalTimezone(userId, form.LocalTimezone)

View File

@ -59,13 +59,19 @@ func main() {
sessionManager: sessionManager, sessionManager: sessionManager,
websites: &models.WebsiteModel{DB: db}, websites: &models.WebsiteModel{DB: db},
guestbooks: &models.GuestbookModel{DB: db}, guestbooks: &models.GuestbookModel{DB: db},
users: &models.UserModel{DB: db}, users: &models.UserModel{DB: db, Settings: make(map[string]models.Setting)},
guestbookComments: &models.GuestbookCommentModel{DB: db}, guestbookComments: &models.GuestbookCommentModel{DB: db},
formDecoder: formDecoder, formDecoder: formDecoder,
debug: *debug, debug: *debug,
timezones: getAvailableTimezones(), timezones: getAvailableTimezones(),
} }
err = app.users.InitializeSettingsMap()
if err != nil {
logger.Error(err.Error())
os.Exit(1)
}
tlsConfig := &tls.Config{ tlsConfig := &tls.Config{
CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256}, CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256},
} }

View File

@ -38,7 +38,7 @@ CREATE TABLE user_settings (
Id integer primary key autoincrement, Id integer primary key autoincrement,
UserId integer NOT NULL, UserId integer NOT NULL,
SettingId integer NOT NULL, SettingId integer NOT NULL,
AllowedSettingValueId integer NOT NULL, AllowedSettingValueId integer,
UnconstrainedValue varchar(256), UnconstrainedValue varchar(256),
FOREIGN KEY (UserId) REFERENCES users(Id) FOREIGN KEY (UserId) REFERENCES users(Id)
ON DELETE RESTRICT ON DELETE RESTRICT
@ -55,7 +55,7 @@ CREATE TABLE guestbook_settings (
Id integer primary key autoincrement, Id integer primary key autoincrement,
GuestbookId integer NOT NULL, GuestbookId integer NOT NULL,
SettingId integer NOT NULL, SettingId integer NOT NULL,
AllowedSettingValueId integer NOT NULL, AllowedSettingValueId integer,
UnconstrainedValue varchar(256), UnconstrainedValue varchar(256),
FOREIGN KEY (GuestbookId) REFERENCES guestbooks(Id) FOREIGN KEY (GuestbookId) REFERENCES guestbooks(Id)
ON DELETE RESTRICT ON DELETE RESTRICT

View File

@ -3,9 +3,11 @@ package models
import "errors" import "errors"
var ( 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")
) )

View File

@ -5,6 +5,14 @@ import (
"time" "time"
) )
type GuestbookSettings struct {
IsCommentingEnabled bool
ReenableCommenting time.Time
IsVisible bool
FilteredWords []string
AllowRemoteHostAccess bool
}
type Guestbook struct { type Guestbook struct {
ID int64 ID int64
ShortId uint64 ShortId uint64
@ -13,6 +21,7 @@ type Guestbook struct {
Created time.Time Created time.Time
Deleted time.Time Deleted time.Time
IsActive bool IsActive bool
Settings GuestbookSettings
} }
type GuestbookModel struct { type GuestbookModel struct {
@ -70,3 +79,47 @@ func (m *GuestbookModel) GetAll(userId int64) ([]Guestbook, error) {
} }
return guestbooks, nil return guestbooks, nil
} }
func (m *GuestbookModel) initializeGuestbookSettings(guestbookId int64, settings UserSettings) error {
stmt := `INSERT INTO guestbook_settings (GuestbookId, SettingId, AllowedSettingValueId, UnconstrainedValue) VALUES
(?, ?, ?, ?),
(?, ?, ?, ?),
(?, ?, ?, ?),
(?, ?, ?, ?),
(?, ?, ?, ?)`
_ = len(stmt)
return nil
}
func (m *GuestbookModel) UpdateSetting(guestbookId int64, value string) {
stmt := `UPDATE guestbook_settings SET
AllowedSettingValueId=IFNULL((SELECT Id FROM allowed_setting_values WHERE SettingId = guestbook_settings.SettingId AND ItemValue = ?), AllowedSettingValueId),
UnconstrainedValue=(SELECT ? FROM settings WHERE settings.Id = guestbook_settings.SettingId AND settings.Constrained=0)
WHERE GuestbookId = ?
AND SettingId = (SELECT Id from Settings WHERE Description=?);`
_ = len(stmt)
}
func (m *GuestbookModel) SetCommentingEnabled(guestbookId int64, enabled bool) error {
return nil
}
func (m *GuestbookModel) SetReenableCommentingDate(guestbookId int64, reenableTime time.Time) error {
return nil
}
func (m *GuestbookModel) SetVisible(guestbookId int64, visible bool) error {
return nil
}
func (m *GuestbookModel) AddFilteredWord(guestbookId int64, word string) error {
return nil
}
func (m *GuestbookModel) RemoveFilteredWord(guestbookId int64, word string) error {
return nil
}
func (m *GuestbookModel) SetRemoteHostAccess(guestbookId int64, allowed bool) error {
return nil
}

223
internal/models/settings.go Normal file
View File

@ -0,0 +1,223 @@
package models
import (
"database/sql"
"maps"
"strconv"
"time"
)
type SettingModel struct {
DB *sql.DB
}
type SettingConfig struct {
}
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
dataTypeDesc string
settingGroup SettingGroup
settingGroupDesc string
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 (s Setting) Validate(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 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
}

View File

@ -10,14 +10,14 @@ import (
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
const (
u_timezone = 1
)
type UserSettings struct { type UserSettings struct {
LocalTimezone *time.Location LocalTimezone *time.Location
} }
const (
USER_TIMEZONE = "local_timezone"
)
type User struct { type User struct {
ID int64 ID int64
ShortId uint64 ShortId uint64
@ -31,7 +31,40 @@ type User struct {
} }
type UserModel struct { type UserModel struct {
DB *sql.DB DB *sql.DB
Settings map[string]Setting
}
func (m *UserModel) InitializeSettingsMap() error {
if m.Settings == nil {
m.Settings = make(map[string]Setting)
}
stmt := `SELECT settings.Id, settings.Description, Constrained, d.Id, d.Description, g.Id, g.Description, MinValue, MaxValue
FROM settings
LEFT JOIN setting_data_types d ON settings.DataType = d.Id
LEFT JOIN setting_groups g ON settings.SettingGroup = g.Id
WHERE SettingGroup = (SELECT Id FROM setting_groups WHERE Description = 'user' LIMIT 1)`
result, err := m.DB.Query(stmt)
if err != nil {
return err
}
for result.Next() {
var s Setting
var mn sql.NullString
var mx sql.NullString
err := result.Scan(&s.id, &s.description, &s.constrained, &s.dataType, &s.dataTypeDesc, &s.settingGroup, &s.settingGroupDesc, &mn, &mx)
if mn.Valid {
s.minValue = mn.String
}
if mx.Valid {
s.maxValue = mx.String
}
if err != nil {
return err
}
m.Settings[s.description] = s
}
return nil
} }
func (m *UserModel) Insert(shortId uint64, username string, email string, password string, settings UserSettings) error { func (m *UserModel) Insert(shortId uint64, username string, email string, password string, settings UserSettings) error {
@ -54,13 +87,7 @@ func (m *UserModel) Insert(shortId uint64, username string, email string, passwo
if err != nil { if err != nil {
return err return err
} }
settingsStmt := `INSERT INTO user_settings err = m.initializeUserSettings(id, settings)
(UserId, SettingId, AllowedSettingValueId, UnconstrainedValue)
VALUES (?, ?, ?, ?)`
_, err = m.DB.Exec(settingsStmt, id, u_timezone, nil, settings.LocalTimezone.String())
if err != nil {
return err
}
return nil return nil
} }
@ -174,7 +201,7 @@ func (m *UserModel) GetSettings(userId int64) (UserSettings, error) {
return settings, err return settings, err
} }
switch id { switch id {
case u_timezone: case m.Settings[USER_TIMEZONE].id:
settings.LocalTimezone, err = time.LoadLocation(unconstrainedValue.String) settings.LocalTimezone, err = time.LoadLocation(unconstrainedValue.String)
if err != nil { if err != nil {
panic(err) panic(err)
@ -184,9 +211,53 @@ func (m *UserModel) GetSettings(userId int64) (UserSettings, error) {
return settings, err return settings, err
} }
func (m *UserModel) SetLocalTimezone(userId int64, timezone string) error { func (m *UserModel) initializeUserSettings(userId int64, settings UserSettings) error {
stmt := `UPDATE user_settings SET UnconstrainedValue = ? WHERE UserId = ?` stmt := `INSERT INTO user_settings (UserId, SettingId, AllowedSettingValueId, UnconstrainedValue)
_, err := m.DB.Exec(stmt, timezone, userId) VALUES (?, ?, ?, ?)`
_, err := m.DB.Exec(stmt, userId, m.Settings[USER_TIMEZONE].id, nil, settings.LocalTimezone.String())
if err != nil {
return err
}
return nil
}
func (m *UserModel) UpdateUserSettings(userId int64, settings UserSettings) error {
err := m.UpdateSetting(userId, m.Settings[USER_TIMEZONE], settings.LocalTimezone.String())
if err != nil {
return err
}
return nil
}
func (m *UserModel) UpdateSetting(userId int64, setting Setting, value string) error {
stmt := `UPDATE user_settings SET
AllowedSettingValueId=IFNULL(
(SELECT Id FROM allowed_setting_values WHERE SettingId = user_settings.SettingId AND ItemValue = ?), AllowedSettingValueId
),
UnconstrainedValue=(SELECT ? FROM settings WHERE settings.Id = user_settings.SettingId AND settings.Constrained=0)
WHERE userId = ?
AND SettingId = (SELECT Id from Settings WHERE Description=?);`
result, err := m.DB.Exec(stmt, value, value, userId, setting.description)
if err != nil {
return err
}
rows, err := result.RowsAffected()
if err != nil {
return err
}
if rows != 1 {
return ErrInvalidSettingValue
}
return nil
}
func (m *UserModel) SetLocalTimezone(userId int64, timezone string) error {
setting := m.Settings[USER_TIMEZONE]
valid := setting.Validate(timezone)
if !valid {
return ErrInvalidSettingValue
}
err := m.UpdateSetting(userId, setting, timezone)
if err != nil { if err != nil {
return err return err
} }

View File

@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT. // Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.857 // templ: version: v0.3.833
package views package views
//lint:file-ignore SA4006 This context is only used if a nested component is present. //lint:file-ignore SA4006 This context is only used if a nested component is present.

View File

@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT. // Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.857 // templ: version: v0.3.833
package views package views
//lint:file-ignore SA4006 This context is only used if a nested component is present. //lint:file-ignore SA4006 This context is only used if a nested component is present.

View File

@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT. // Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.857 // templ: version: v0.3.833
package views package views
//lint:file-ignore SA4006 This context is only used if a nested component is present. //lint:file-ignore SA4006 This context is only used if a nested component is present.

View File

@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT. // Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.857 // templ: version: v0.3.833
package views package views
//lint:file-ignore SA4006 This context is only used if a nested component is present. //lint:file-ignore SA4006 This context is only used if a nested component is present.

View File

@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT. // Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.857 // templ: version: v0.3.833
package views package views
//lint:file-ignore SA4006 This context is only used if a nested component is present. //lint:file-ignore SA4006 This context is only used if a nested component is present.

View File

@ -1,6 +1,6 @@
// Code generated by templ - DO NOT EDIT. // Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.857 // templ: version: v0.3.833
package views package views
//lint:file-ignore SA4006 This context is only used if a nested component is present. //lint:file-ignore SA4006 This context is only used if a nested component is present.