From fa2ecacce528e2ea1172d004a2729358a1146b42 Mon Sep 17 00:00:00 2001 From: Andrew-71 Date: Thu, 9 May 2024 23:44:37 +0300 Subject: [PATCH] Add config editing to UI --- CHANGELOG.md | 7 +++++++ README.md | 1 + TODO.md | 11 +---------- api.go | 10 +++++----- config.go | 27 ++++++++++++++------------- config/config.txt | 5 ----- files.go | 20 +++++++++++--------- i18n/en.json | 2 +- i18n/ru.json | 2 +- info.go | 2 +- pages/base.html | 3 ++- pages/info.html | 4 ++-- public/main.js | 3 ++- routes.go | 30 ++++++++++++++++++++++++------ serve.go | 2 ++ 15 files changed, 74 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ea4e69..33eb255 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ # Changelog This file keeps track of changes in more human-readable fashion +## v0.6.0 +* Replaced config reload with edit in info (api method still available, config reloads on save) +* Bug fixes + * Filenames are now sanitized when writing files + * "Tomorrow" in days list is now also displayed if Timezone is changed and grace period is off + * Frontend date display now uses configured timezone + ## v0.5.0 * Added a JS prompt to create new note * "Sanitization" for this method is basic and assumes a well-meaning user diff --git a/README.md b/README.md index d0d99e5..d4a3352 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ timezone=Local # IANA Time zone database identifier ("UTC", Local", "Europe/Mos grace_period=0s # Time after a new day begins, but before the switch to next day's file. e.g. 2h30m - files will change at 2:30am language=en # ISO-639 language code (pre-installed - en, ru) theme=default # Picked theme (pre-installed - default, gruvbox, high-contrast) +title=🌺 Hibiscus.txt # The text in the header log_to_file=false # Whether to write logs to a file log_file=config/log.txt # Where to store the log file if it is enabled enable_scram=false # Whether the app should shut down if there are 3 or more failed login attempts within 100 seconds diff --git a/TODO.md b/TODO.md index c3bd910..f85d679 100644 --- a/TODO.md +++ b/TODO.md @@ -3,22 +3,13 @@ List of things to add to this project ## v1.0.0 * a logo so I can enable PWA (and look cool) -* Themes - * Theme in config [X] - * Themes - * Default [X] - * Gruvbox [X] - * High contrast [ ] - * Seasons? - * Check for bugs * Versioned containers via `ghcr.io` or `dockerhub`, with automatic CI/CD build on release -* Test test test !!!! +* ...QA? And polishing. ## Brainstorming Don't expect any of this, these are ideas floating inside my head * Further info page functionality - * Edit config * Statistics e.g. mb taken, number of day pages/notes * Test telegram link * Multi-user support through several username-pass keys diff --git a/api.go b/api.go index 237be2c..51e8a50 100644 --- a/api.go +++ b/api.go @@ -72,17 +72,17 @@ func GetDayApi(w http.ResponseWriter, r *http.Request) { HandleWrite(w.Write([]byte("day not specified"))) return } - GetFile("day/"+dayString, w) + GetFile(DataFile("day/"+dayString), w) } // GetTodayApi runs GetFile with today's date as filename func GetTodayApi(w http.ResponseWriter, _ *http.Request) { - GetFile("day/"+TodayDate(), w) + GetFile(DataFile("day/"+TodayDate()), w) } // PostTodayApi runs PostFile with today's date as filename func PostTodayApi(w http.ResponseWriter, r *http.Request) { - PostFile("day/"+TodayDate(), w, r) + PostFile(DataFile("day/"+TodayDate()), w, r) } // GetNoteApi returns contents of a note specified in URL @@ -93,7 +93,7 @@ func GetNoteApi(w http.ResponseWriter, r *http.Request) { HandleWrite(w.Write([]byte("note not specified"))) return } - GetFile("notes/"+noteString, w) + GetFile(DataFile("notes/"+noteString), w) } // PostNoteApi writes request's body contents to a note specified in URL @@ -104,7 +104,7 @@ func PostNoteApi(w http.ResponseWriter, r *http.Request) { HandleWrite(w.Write([]byte("note not specified"))) return } - PostFile("notes/"+noteString, w, r) + PostFile(DataFile("notes/"+noteString), w, r) } // GraceActiveApi returns "true" if grace period is active, and "false" otherwise diff --git a/config.go b/config.go index 2e7e687..db21472 100644 --- a/config.go +++ b/config.go @@ -49,10 +49,9 @@ var DefaultConfig = Config{ TelegramChat: "", } -// Save puts modified and mandatory config options into the config.txt file -func (c *Config) Save() error { +// String returns text version of modified and mandatory config options +func (c *Config) String() string { output := "" - v := reflect.ValueOf(*c) vDefault := reflect.ValueOf(DefaultConfig) typeOfS := v.Type() @@ -64,22 +63,14 @@ func (c *Config) Save() error { output += fmt.Sprintf("%s=%v\n", key, value) } } - - f, err := os.OpenFile(ConfigFile, os.O_CREATE|os.O_WRONLY, 0644) - if err != nil { - return err - } - if _, err := f.Write([]byte(output)); err != nil { - return err - } - return nil + return output } func (c *Config) Reload() error { *c = DefaultConfig // Reset config if _, err := os.Stat(ConfigFile); errors.Is(err, os.ErrNotExist) { - err := c.Save() + err := c.Save([]byte(c.String())) if err != nil { return err } @@ -156,6 +147,16 @@ func (c *Config) Reload() error { return LoadLanguage(c.Language) // Load selected language } +// Read gets raw contents from ConfigFile +func (c *Config) Read() ([]byte, error) { + return ReadFile(ConfigFile) +} + +// Save writes to ConfigFile +func (c *Config) Save(contents []byte) error { + return SaveFile(ConfigFile, contents) +} + // ConfigInit loads config on startup func ConfigInit() Config { cfg := Config{} diff --git a/config/config.txt b/config/config.txt index d6238b0..8b6b5d3 100644 --- a/config/config.txt +++ b/config/config.txt @@ -2,9 +2,4 @@ username=admin password=admin port=7101 timezone=Local -grace_period=0s language=en -theme=default -log_to_file=false -log_file=config/log.txt -enable_scram=false diff --git a/files.go b/files.go index f2868d8..a7d2564 100644 --- a/files.go +++ b/files.go @@ -11,14 +11,16 @@ import ( "time" ) +// DataFile modifies file path to ensure it's a .txt inside the data folder +func DataFile(filename string) string { + return "data/" + path.Clean(filename) + ".txt" +} + // ReadFile returns raw contents of a file func ReadFile(filename string) ([]byte, error) { - filename = "data/" + path.Clean(filename) + ".txt" // Does this sanitize the path? - if _, err := os.Stat(filename); errors.Is(err, os.ErrNotExist) { return nil, err } - fileContents, err := os.ReadFile(filename) if err != nil { slog.Error("error reading file", @@ -26,14 +28,12 @@ func ReadFile(filename string) ([]byte, error) { "file", filename) return nil, err } - return fileContents, nil } -// SaveFile Writes request's contents to a file +// SaveFile Writes contents to a file func SaveFile(filename string, contents []byte) error { contents = bytes.TrimSpace(contents) - filename = "data/" + filename + ".txt" if len(contents) == 0 { // Delete empty files err := os.Remove(filename) slog.Error("error deleting empty file", @@ -63,8 +63,9 @@ func SaveFile(filename string, contents []byte) error { } // ListFiles returns slice of filenames in a directory without extensions or path +// NOTE: What if I ever want to list non-text files or those outside data directory? func ListFiles(directory string) ([]string, error) { - filenames, err := filepath.Glob("data/" + directory + "/*.txt") + filenames, err := filepath.Glob("data/" + path.Clean(directory) + "/*.txt") if err != nil { return nil, err } @@ -93,15 +94,16 @@ func TodayDate() string { if GraceActive() { dateFormatted = time.Now().In(Cfg.Timezone).AddDate(0, 0, -1).Format(time.DateOnly) } + slog.Debug("today", "time", time.Now().In(Cfg.Timezone).Format(time.DateTime)) return dateFormatted } // ReadToday runs ReadFile with today's date as filename func ReadToday() ([]byte, error) { - return ReadFile("day/" + TodayDate()) + return ReadFile(DataFile("day/" + TodayDate())) } // SaveToday runs SaveFile with today's date as filename func SaveToday(contents []byte) error { - return SaveFile("day/"+TodayDate(), contents) + return SaveFile(DataFile("day/"+TodayDate()), contents) } diff --git a/i18n/en.json b/i18n/en.json index 79092b6..2bb68c4 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -23,5 +23,5 @@ "info.version.link": "source and changelog", "info.export": "Export data", "info.readme": "Edit readme.txt", - "info.reload": "Reload config" + "info.config": "Edit config" } \ No newline at end of file diff --git a/i18n/ru.json b/i18n/ru.json index 4240be5..2806d34 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -23,5 +23,5 @@ "info.version.link": "исходный ΠΊΠΎΠ΄", "info.export": "Экспорт Π΄Π°Π½Π½Ρ‹Ρ…", "info.readme": "Π Π΅Π΄Π°ΠΊΡ‚ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ readme.txt", - "info.reload": "ΠŸΠ΅Ρ€Π΅Π·Π°Π³Ρ€ΡƒΠ·ΠΈΡ‚ΡŒ ΠΊΠΎΠ½Ρ„ΠΈΠ³" + "info.config": "Π Π΅Π΄Π°ΠΊΡ‚ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ ΠΊΠΎΠ½Ρ„ΠΈΠ³" } \ No newline at end of file diff --git a/info.go b/info.go index 27649b2..2d22989 100644 --- a/info.go +++ b/info.go @@ -15,7 +15,7 @@ type AppInfo struct { // Info contains app information var Info = AppInfo{ - Version: "0.5.0", + Version: "0.6.0", SourceLink: "https://git.a71.su/Andrew71/hibiscus", } diff --git a/pages/base.html b/pages/base.html index 39fc9a6..84e7144 100644 --- a/pages/base.html +++ b/pages/base.html @@ -25,7 +25,8 @@ {{template "footer" .}} diff --git a/pages/info.html b/pages/info.html index 12feaa1..92ab91a 100644 --- a/pages/info.html +++ b/pages/info.html @@ -2,8 +2,8 @@

{{ translatableText "title.info" }}

{{end}} \ No newline at end of file diff --git a/public/main.js b/public/main.js index ea339a2..5089383 100644 --- a/public/main.js +++ b/public/main.js @@ -3,7 +3,8 @@ function formatDate(date) { let dateFormat = new Intl.DateTimeFormat([langName, "en"], { year: 'numeric', month: 'short', - day: 'numeric' + day: 'numeric', + timeZone: timeZone }) return dateFormat.format(date) } diff --git a/routes.go b/routes.go index 5d3fa3d..6c8ca79 100644 --- a/routes.go +++ b/routes.go @@ -113,7 +113,7 @@ func GetDays(w http.ResponseWriter, r *http.Request) { if v == TodayDate() { dayString = TranslatableText("link.today") dayString = strings.ToTitle(string([]rune(dayString)[0])) + string([]rune(dayString)[1:]) - } else if GraceActive() && v > TodayDate() { + } else if v > TodayDate() { dayString = TranslatableText("link.tomorrow") dayString = strings.ToTitle(string([]rune(dayString)[0])) + string([]rune(dayString)[1:]) } @@ -189,7 +189,7 @@ func GetDay(w http.ResponseWriter, r *http.Request) { title = t.Format("02 Jan 2006") } - GetEntry(w, r, title, "day/"+dayString, false) + GetEntry(w, r, title, DataFile("day/"+dayString), false) } // GetNote renders HTML page for a note @@ -205,7 +205,7 @@ func GetNote(w http.ResponseWriter, r *http.Request) { noteString = decodedNote } - GetEntry(w, r, noteString, "notes/"+noteString, true) + GetEntry(w, r, noteString, DataFile("notes/"+noteString), true) } // PostNote saves a note form and redirects back to GET @@ -216,7 +216,7 @@ func PostNote(w http.ResponseWriter, r *http.Request) { HandleWrite(w.Write([]byte("note not specified"))) return } - err := SaveFile("notes/"+noteString, []byte(r.FormValue("text"))) + err := SaveFile(DataFile("notes/"+noteString), []byte(r.FormValue("text"))) if err != nil { slog.Error("error saving a note", "note", noteString, "error", err) } @@ -225,14 +225,32 @@ func PostNote(w http.ResponseWriter, r *http.Request) { // GetReadme calls GetEntry for readme.txt func GetReadme(w http.ResponseWriter, r *http.Request) { - GetEntry(w, r, "readme.txt", "readme", true) + GetEntry(w, r, "readme.txt", DataFile("readme"), true) } // PostReadme saves contents of readme.txt file func PostReadme(w http.ResponseWriter, r *http.Request) { - err := SaveFile("readme", []byte(r.FormValue("text"))) + err := SaveFile(DataFile("readme"), []byte(r.FormValue("text"))) if err != nil { slog.Error("error saving readme", "error", err) } http.Redirect(w, r, r.Header.Get("Referer"), 302) } + +// GetConfig calls GetEntry for Cfg +func GetConfig(w http.ResponseWriter, r *http.Request) { + GetEntry(w, r, "config.txt", ConfigFile, true) +} + +// PostConfig saves new Cfg +func PostConfig(w http.ResponseWriter, r *http.Request) { + err := SaveFile(ConfigFile, []byte(r.FormValue("text"))) + if err != nil { + slog.Error("error saving config", "error", err) + } + err = Cfg.Reload() + if err != nil { + slog.Error("error reloading config", "error", err) + } + http.Redirect(w, r, r.Header.Get("Referer"), 302) +} diff --git a/serve.go b/serve.go index b05e0e3..3ce0bd7 100644 --- a/serve.go +++ b/serve.go @@ -28,6 +28,8 @@ func Serve() { userRouter.Get("/info", GetInfo) userRouter.Get("/readme", GetReadme) userRouter.Post("/readme", PostReadme) + userRouter.Get("/config", GetConfig) + userRouter.Post("/config", PostConfig) r.Mount("/", userRouter) // API =============