diff --git a/database/db-scanner.go b/database/db-scanner.go index c0dd7eb..b4848b6 100644 --- a/database/db-scanner.go +++ b/database/db-scanner.go @@ -27,7 +27,7 @@ type NullStringScanner struct{ sql.NullString } func (s *NullStringScanner) Null() bool { return !s.Valid } func (s *NullStringScanner) Scan(src any) error { return s.NullString.Scan(src) } func (s NullStringScanner) MarshalJSON() ([]byte, error) { - return marshalValueOrNull(s.Null(), s.String) + return marshalValueOrNull(s.Null(), s.NullString.String) } func (s *NullStringScanner) UnmarshalJSON(bytes []byte) error { if string(bytes) == "null" { @@ -40,6 +40,12 @@ func (s *NullStringScanner) UnmarshalJSON(bytes []byte) error { } return s.Scan(&a) } +func (s NullStringScanner) String() string { + if s.Null() { + return "" + } + return s.NullString.String +} type NullDateScanner struct{ sql.NullTime } @@ -59,6 +65,12 @@ func (t *NullDateScanner) UnmarshalJSON(bytes []byte) error { } return t.Scan(&a) } +func (t NullDateScanner) String() string { + if t.Null() { + return "" + } + return t.NullTime.Time.UTC().Format(time.DateOnly) +} type LocationScanner struct{ *time.Location } diff --git a/database/tx.go b/database/tx.go index 17c27c5..fb42ae1 100644 --- a/database/tx.go +++ b/database/tx.go @@ -9,6 +9,10 @@ import ( "time" ) +func updatedAt() string { + return time.Now().UTC().Format(time.DateTime) +} + type Tx struct{ tx *sql.Tx } func (t *Tx) Commit() error { @@ -37,7 +41,7 @@ func (t *Tx) InsertUser(name, un, pw, email string) error { if err != nil { return err } - _, err = t.tx.Exec(`INSERT INTO users (subject, name, username, password, email) VALUES (?, ?, ?, ?, ?)`, uuid.NewString(), name, un, pwHash, email) + _, err = t.tx.Exec(`INSERT INTO users (subject, name, username, password, email, updated_at) VALUES (?, ?, ?, ?, ?, ?)`, uuid.NewString(), name, un, pwHash, email, updatedAt()) return err } @@ -96,7 +100,7 @@ func (t *Tx) ChangeUserPassword(sub uuid.UUID, pwOld, pwNew string) error { if err != nil { return err } - exec, err := t.tx.Exec(`UPDATE users SET password = ?, updated_at = ? WHERE subject = ? AND password = ?`, pwNewHash, time.Now().Format(time.DateTime), sub, pwHash) + exec, err := t.tx.Exec(`UPDATE users SET password = ?, updated_at = ? WHERE subject = ? AND password = ?`, pwNewHash, updatedAt(), sub, pwHash) if err != nil { return err } @@ -129,7 +133,7 @@ WHERE subject = ?`, v.Birthdate, v.ZoneInfo.String(), v.Locale.String(), - time.Now().Format(time.DateTime), + updatedAt(), sub, ) if err != nil { diff --git a/database/tx_test.go b/database/tx_test.go index 5a87ff1..789d1da 100644 --- a/database/tx_test.go +++ b/database/tx_test.go @@ -16,7 +16,7 @@ func TestTx_ChangeUserPassword(t *testing.T) { assert.NoError(t, err) d, err := Open("file::memory:") assert.NoError(t, err) - _, err = d.db.Exec(`INSERT INTO users (subject, name, username, password, email) VALUES (?, ?, ?, ?, ?)`, u.String(), "Test", "test", pw, "test@localhost") + _, err = d.db.Exec(`INSERT INTO users (subject, name, username, password, email, updated_at) VALUES (?, ?, ?, ?, ?, ?)`, u.String(), "Test", "test", pw, "test@localhost", updatedAt()) assert.NoError(t, err) tx, err := d.Begin() assert.NoError(t, err) @@ -39,7 +39,7 @@ func TestTx_ModifyUser(t *testing.T) { assert.NoError(t, err) d, err := Open("file::memory:") assert.NoError(t, err) - _, err = d.db.Exec(`INSERT INTO users (subject, name, username, password, email) VALUES (?, ?, ?, ?, ?)`, u.String(), "Test", "test", pw, "test@localhost") + _, err = d.db.Exec(`INSERT INTO users (subject, name, username, password, email, updated_at) VALUES (?, ?, ?, ?, ?, ?)`, u.String(), "Test", "test", pw, "test@localhost", updatedAt()) assert.NoError(t, err) tx, err := d.Begin() assert.NoError(t, err) diff --git a/lists/locales.go b/lists/locales.go new file mode 100644 index 0000000..abf8fdc --- /dev/null +++ b/lists/locales.go @@ -0,0 +1,104 @@ +package lists + +import ( + "golang.org/x/text/language" + "golang.org/x/text/language/display" + "sync" +) + +var ( + localeOnce sync.Once + localeNames []struct{ Value, Label string } +) + +func ListLocale() []struct{ Value, Label string } { + localeOnce.Do(func() { + localeNames = make([]struct{ Value, Label string }, len(localeList)) + for i := range localeList { + localeNames[i] = struct{ Value, Label string }{Value: localeList[i].String(), Label: display.Self.Name(localeList[i])} + } + }) + return localeNames +} + +var localeList = []language.Tag{ + language.Afrikaans, + language.Amharic, + language.Arabic, + language.ModernStandardArabic, + language.Azerbaijani, + language.Bulgarian, + language.Bengali, + language.Catalan, + language.Czech, + language.Danish, + language.German, + language.Greek, + language.English, + language.AmericanEnglish, + language.BritishEnglish, + language.Spanish, + language.EuropeanSpanish, + language.LatinAmericanSpanish, + language.Estonian, + language.Persian, + language.Finnish, + language.Filipino, + language.French, + language.CanadianFrench, + language.Gujarati, + language.Hebrew, + language.Hindi, + language.Croatian, + language.Hungarian, + language.Armenian, + language.Indonesian, + language.Icelandic, + language.Italian, + language.Japanese, + language.Georgian, + language.Kazakh, + language.Khmer, + language.Kannada, + language.Korean, + language.Kirghiz, + language.Lao, + language.Lithuanian, + language.Latvian, + language.Macedonian, + language.Malayalam, + language.Mongolian, + language.Marathi, + language.Malay, + language.Burmese, + language.Nepali, + language.Dutch, + language.Norwegian, + language.Punjabi, + language.Polish, + language.Portuguese, + language.BrazilianPortuguese, + language.EuropeanPortuguese, + language.Romanian, + language.Russian, + language.Sinhala, + language.Slovak, + language.Slovenian, + language.Albanian, + language.Serbian, + language.SerbianLatin, + language.Swedish, + language.Swahili, + language.Tamil, + language.Telugu, + language.Thai, + language.Turkish, + language.Ukrainian, + language.Urdu, + language.Uzbek, + language.Vietnamese, + language.Chinese, + language.SimplifiedChinese, + language.TraditionalChinese, + language.Zulu, +} diff --git a/lists/locales_test.go b/lists/locales_test.go new file mode 100644 index 0000000..d97a3d1 --- /dev/null +++ b/lists/locales_test.go @@ -0,0 +1,15 @@ +package lists + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestListLocale(t *testing.T) { + locales := ListLocale() + assert.True(t, len(locales) > 4) + assert.Equal(t, struct{ Value, Label string }{Value: "af", Label: "Afrikaans"}, locales[0]) + assert.Equal(t, struct{ Value, Label string }{Value: "am", Label: "አማርኛ"}, locales[1]) + assert.Equal(t, struct{ Value, Label string }{Value: "zh-Hant", Label: "繁體中文"}, locales[len(locales)-2]) + assert.Equal(t, struct{ Value, Label string }{Value: "zu", Label: "isiZulu"}, locales[len(locales)-1]) +} diff --git a/lists/zoneinfo.go b/lists/zoneinfo.go new file mode 100644 index 0000000..96d378b --- /dev/null +++ b/lists/zoneinfo.go @@ -0,0 +1,53 @@ +package lists + +import ( + "os" + "path/filepath" + "sort" + "strings" + "sync" +) + +var ( + zoneDirs = []string{ + // Update path according to your OS + "/usr/share/zoneinfo/", + "/usr/share/lib/zoneinfo/", + "/usr/lib/locale/TZ/", + } + zoneInfoOnce sync.Once + zoneNames []string +) + +func ListZoneInfo() []string { + zoneInfoOnce.Do(func() { + zoneNames = make([]string, 0) + for _, zoneDir := range zoneDirs { + zoneNames = append(zoneNames, FindTimeZoneFiles(zoneDir)...) + } + sort.Strings(zoneNames) + }) + return zoneNames +} + +func FindTimeZoneFiles(zoneDir string) []string { + dArr := make([]string, 0) + dArr = append(dArr, "") + arr := make([]string, 0) + + for i := 0; i < len(dArr); i++ { + dir := dArr[i] + files, _ := os.ReadDir(filepath.Join(zoneDir, dir)) + for _, f := range files { + if f.Name() != strings.ToUpper(f.Name()[:1])+f.Name()[1:] { + continue + } + if f.IsDir() { + dArr = append(dArr, filepath.Join(dir, f.Name())) + } else { + arr = append(arr, filepath.Join(dir, f.Name())) + } + } + } + return arr +} diff --git a/lists/zoneinfo_test.go b/lists/zoneinfo_test.go new file mode 100644 index 0000000..83e594b --- /dev/null +++ b/lists/zoneinfo_test.go @@ -0,0 +1,15 @@ +package lists + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestListZoneInfo(t *testing.T) { + zoneinfos := ListZoneInfo() + assert.True(t, len(zoneinfos) > 4) + assert.Equal(t, "Africa/Abidjan", zoneinfos[0]) + assert.Equal(t, "Africa/Accra", zoneinfos[1]) + assert.Equal(t, "WET", zoneinfos[len(zoneinfos)-2]) + assert.Equal(t, "Zulu", zoneinfos[len(zoneinfos)-1]) +} diff --git a/pages/edit.go.html b/pages/edit.go.html index 107ec54..cc01ec4 100644 --- a/pages/edit.go.html +++ b/pages/edit.go.html @@ -37,15 +37,16 @@
- +
- - + {{range .ListZoneInfo}} + + {{end}}
@@ -53,8 +54,9 @@ - - + {{range .ListLocale}} + + {{end}} diff --git a/server/server.go b/server/server.go index 7f41875..c101b8e 100644 --- a/server/server.go +++ b/server/server.go @@ -8,6 +8,7 @@ import ( errors2 "errors" "fmt" "github.com/1f349/tulip/database" + "github.com/1f349/tulip/lists" "github.com/1f349/tulip/openid" "github.com/1f349/tulip/pages" "github.com/go-oauth2/oauth2/v4" @@ -112,20 +113,23 @@ func NewHttpServer(listen, domain string, db *database.DB, privKey []byte, clien return } - hs.DbTx(rw, func(tx *database.Tx) error { - userWithName, err := tx.GetUserDisplayName(auth.ID) + var userWithName *database.User + if hs.DbTx(rw, func(tx *database.Tx) (err error) { + userWithName, err = tx.GetUserDisplayName(auth.ID) if err != nil { return fmt.Errorf("failed to get user display name: %w", err) } - if err := pages.RenderPageTemplate(rw, "index", map[string]any{ - "Auth": auth, - "User": userWithName, - "Nonce": lNonce, - }); err != nil { - log.Printf("Failed to render page: edit: %s\n", err) - } - return nil - }) + return + }) { + return + } + if err := pages.RenderPageTemplate(rw, "index", map[string]any{ + "Auth": auth, + "User": userWithName, + "Nonce": lNonce, + }); err != nil { + log.Printf("Failed to render page: edit: %s\n", err) + } })) r.POST("/logout", hs.RequireAuthentication("403 Forbidden", http.StatusForbidden, func(rw http.ResponseWriter, req *http.Request, params httprouter.Params, auth UserAuth) { lNonce, ok := auth.Session.Get("action-nonce") @@ -228,8 +232,10 @@ func NewHttpServer(listen, domain string, db *database.DB, privKey []byte, clien return } if err := pages.RenderPageTemplate(rw, "edit", map[string]any{ - "User": user, - "Nonce": lNonce, + "User": user, + "Nonce": lNonce, + "ListZoneInfo": lists.ListZoneInfo(), + "ListLocale": lists.ListLocale(), }); err != nil { log.Printf("Failed to render page: edit: %s\n", err) }