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

View File

@ -59,13 +59,19 @@ func main() {
sessionManager: sessionManager,
websites: &models.WebsiteModel{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},
formDecoder: formDecoder,
debug: *debug,
timezones: getAvailableTimezones(),
}
err = app.users.InitializeSettingsMap()
if err != nil {
logger.Error(err.Error())
os.Exit(1)
}
tlsConfig := &tls.Config{
CurvePreferences: []tls.CurveID{tls.X25519, tls.CurveP256},
}

View File

@ -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

View File

@ -8,4 +8,6 @@ var (
ErrInvalidCredentials = errors.New("models: invalid credentials")
ErrDuplicateEmail = errors.New("models: duplicate email")
ErrInvalidSettingValue = errors.New("models: invalid setting value")
)

View File

@ -5,6 +5,14 @@ import (
"time"
)
type GuestbookSettings struct {
IsCommentingEnabled bool
ReenableCommenting time.Time
IsVisible bool
FilteredWords []string
AllowRemoteHostAccess bool
}
type Guestbook struct {
ID int64
ShortId uint64
@ -13,6 +21,7 @@ type Guestbook struct {
Created time.Time
Deleted time.Time
IsActive bool
Settings GuestbookSettings
}
type GuestbookModel struct {
@ -70,3 +79,47 @@ func (m *GuestbookModel) GetAll(userId int64) ([]Guestbook, error) {
}
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"
)
const (
u_timezone = 1
)
type UserSettings struct {
LocalTimezone *time.Location
}
const (
USER_TIMEZONE = "local_timezone"
)
type User struct {
ID int64
ShortId uint64
@ -32,6 +32,39 @@ type User struct {
type UserModel struct {
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 {
@ -54,13 +87,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
}
@ -174,7 +201,7 @@ func (m *UserModel) GetSettings(userId int64) (UserSettings, error) {
return settings, err
}
switch id {
case u_timezone:
case m.Settings[USER_TIMEZONE].id:
settings.LocalTimezone, err = time.LoadLocation(unconstrainedValue.String)
if err != nil {
panic(err)
@ -184,9 +211,53 @@ 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, 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 {
return err
}

View File

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