From 408d244471c288a1f658704a350aacf697daff74 Mon Sep 17 00:00:00 2001 From: Rithas K Date: Sun, 2 Jun 2024 10:29:11 +0530 Subject: [PATCH 1/9] Log real IP address Log real IP address of the client when accessed through a reverse proxy --- serve.go | 1 + 1 file changed, 1 insertion(+) diff --git a/serve.go b/serve.go index ab11789..064e98a 100644 --- a/serve.go +++ b/serve.go @@ -12,6 +12,7 @@ import ( // Serve starts the app's web server func Serve() { r := chi.NewRouter() + r.Use(middleware.RealIP) r.Use(middleware.Logger, middleware.CleanPath, middleware.StripSlashes) r.NotFound(NotFound) From e5962ebfe7aed259802ec5043a5d644cb9cd1894 Mon Sep 17 00:00:00 2001 From: Rithas K Date: Sun, 2 Jun 2024 11:53:40 +0530 Subject: [PATCH 2/9] Fix textarea padding in Safari Use `box-sizing: border-box` to include padding in the width of the textarea. Also, > Box-sizing should be border-box by default from [csswg](https://wiki.csswg.org/ideas/mistakes) --- public/main.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/main.css b/public/main.css index 9c48afb..7a3d058 100644 --- a/public/main.css +++ b/public/main.css @@ -25,6 +25,10 @@ --textarea-border-dark: #454545; } +* { + box-sizing: border-box; +} + body { color: var(--text-light); background-color: var(--bg-light); From f33206c99d202e2401271843442edbe290792583 Mon Sep 17 00:00:00 2001 From: Andrew-71 Date: Sun, 2 Jun 2024 12:27:36 +0300 Subject: [PATCH 3/9] Improve comments for embedded filesystems --- CHANGELOG.md | 5 +++++ config.go | 1 + info.go | 2 +- public/main.css | 5 +---- routes.go | 14 +++++++++----- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 015e9cb..16ad037 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # Changelog This file keeps track of changes in more human-readable fashion +## v1.1.2 +This version contains changes from pull request #2 by Rithas K. +* Real IPs are now logged +* Textarea has been fixed Safari +* Done some minor behind-the-scenes housekeeping ## v1.1.1 This release is mostly a technicality, with a move over to GitHub (`ghcr.io/andrew-71/hibiscus`) for packages due to DockerHub's anti-Russian actions making old "CI/CD" impossible. ## v1.1.0 diff --git a/config.go b/config.go index d290c2d..1883dd0 100644 --- a/config.go +++ b/config.go @@ -48,6 +48,7 @@ var DefaultConfig = Config{ TelegramToken: "", TelegramChat: "", + TelegramTopic: "", } // String returns text version of modified and mandatory config options diff --git a/info.go b/info.go index 64ecdad..7e1fc65 100644 --- a/info.go +++ b/info.go @@ -15,7 +15,7 @@ type AppInfo struct { // Info contains app information var Info = AppInfo{ - Version: "1.1.1", + Version: "1.1.2", SourceLink: "https://git.a71.su/Andrew71/hibiscus", } diff --git a/public/main.css b/public/main.css index 7a3d058..19250a4 100644 --- a/public/main.css +++ b/public/main.css @@ -25,10 +25,7 @@ --textarea-border-dark: #454545; } -* { - box-sizing: border-box; -} - +* { box-sizing: border-box; } body { color: var(--text-light); background-color: var(--bg-light); diff --git a/routes.go b/routes.go index 8d16d24..85e4a37 100644 --- a/routes.go +++ b/routes.go @@ -27,17 +27,21 @@ type Entry struct { type formatEntries func([]string) []Entry +// Public contains the static files e.g. CSS, JS +// //go:embed public var Public embed.FS +// Pages contains the HTML templates used by the app +// //go:embed pages var Pages embed.FS -// EmbeddedFile returns a file in Pages while "handling" potential errors -func EmbeddedFile(name string) []byte { +// EmbeddedPage returns contents of a file in Pages while "handling" potential errors +func EmbeddedPage(name string) []byte { data, err := Pages.ReadFile(name) if err != nil { - slog.Error("Error embedded file", "err", err) + slog.Error("error reading embedded file", "err", err) } return data } @@ -54,13 +58,13 @@ var listTemplate = template.Must(template.New("").Funcs(templateFuncs).ParseFS(P // NotFound returns a user-friendly 404 error page func NotFound(w http.ResponseWriter, r *http.Request) { w.WriteHeader(404) - HandleWrite(w.Write(EmbeddedFile("pages/error/404.html"))) + HandleWrite(w.Write(EmbeddedPage("pages/error/404.html"))) } // InternalError returns a user-friendly 500 error page func InternalError(w http.ResponseWriter, r *http.Request) { w.WriteHeader(500) - HandleWrite(w.Write(EmbeddedFile("pages/error/500.html"))) + HandleWrite(w.Write(EmbeddedPage("pages/error/500.html"))) } // GetToday renders HTML page for today's entry From 9cab989b78b2d3d46027c956367c2dcec66cf88b Mon Sep 17 00:00:00 2001 From: Andrew-71 Date: Mon, 17 Jun 2024 00:54:55 +0300 Subject: [PATCH 4/9] Refactor functions and improve comments --- CHANGELOG.md | 21 ++++++--- README.md | 12 ++--- api.go | 56 ++++++----------------- auth.go | 16 +++---- config.go | 40 ++++++++++++++--- config/config.txt | 2 +- export.go | 5 ++- files.go | 38 +++++----------- flags.go | 2 +- i18n.go | 7 +-- i18n/en.json | 3 +- i18n/ru.json | 3 +- info.go | 12 +++-- logger.go | 2 +- routes.go | 112 ++++++++++++---------------------------------- serve.go | 22 ++++----- 16 files changed, 148 insertions(+), 205 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16ad037..0fe3438 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,17 +1,24 @@ # Changelog -This file keeps track of changes in more human-readable fashion +This file keeps track of changes in a human-readable fashion +## v1.1.3 +This release mostly consists of backend improvements +* List items no longer replace hyphens with spaces for consistency +* Telegram message for SCRAM is now translatable +* Ensured HTML escape in list descriptions +* Refactored many methods, improved comments ## v1.1.2 -This version contains changes from pull request #2 by Rithas K. -* Real IPs are now logged -* Textarea has been fixed Safari -* Done some minor behind-the-scenes housekeeping +This release contains a few bug fixes +* Real IPs are now logged (By Rithas K.) +* CSS now has `box-sizing: border-box` to fix textarea in some cases (By Rithas K.) +* Done some minor code housekeeping ## v1.1.1 -This release is mostly a technicality, with a move over to GitHub (`ghcr.io/andrew-71/hibiscus`) for packages due to DockerHub's anti-Russian actions making old "CI/CD" impossible. +This release is mostly a technicality, with a move over to GitHub (`ghcr.io/andrew-71/hibiscus`) for packages due to DockerHub's prior anti-Russian actions making old "CI/CD" unsustainable. ## v1.1.0 -* You can now specify the Telegram *topic* to send notification to via `tg_topic` config key (By Rithas K.!) +* You can now specify the Telegram *topic* to send notification to via `tg_topic` config key (By Rithas K.) * The Telegram message is now partially translated * Fixed CSS `margin` and `text-align` inherited from my website + ## v1.0.0 This release includes several **breaking** changes * Made a new favicon diff --git a/README.md b/README.md index 1f683f9..d577bbb 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ As a result, I can't guarantee that it's either secure or stable. * You can easily export the files in a `.zip` archive for backups * Everything is plain(text) and simple. No databases, encryption, OAuth, or anything fancy. Even the password is plainte- *wait is this a feature?* -* Docker support (in fact, that's probably the best way to run this) +* [Docker support](#docker-deployment) (in fact, that's probably the best way to run this) * Optional Telegram notifications for failed login attempts ## Technical details @@ -19,7 +19,7 @@ As a result, I can't guarantee that it's either secure or stable. You can read a relevant entry in my blog [here](https://a71.su/notes/hibiscus/). It provides some useful information and context for why this app exists in the first place. -This repository is [mirrored to GitHub](https://github.com/Andrew-71/hibiscus) in case my server goes down. +This repository is [self-hosted by me](https://git.a71.su/Andrew71/hibiscus), but [mirrored to GitHub](https://github.com/Andrew-71/hibiscus) in case my server goes down. ### Data format: ``` @@ -49,7 +49,7 @@ username=admin # Your username password=admin # Your password port=7101 # What port to run on (probably leave on 7101 if using docker) timezone=Local # IANA Time zone database identifier ("UTC", Local", "Europe/Moscow" etc.), Defaults to Local if can't parse. -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 +grace_period=0s # Time after a new day begins, but before the switch to next day's file. e.g. 3h26m - files will change at 3:26am language=en # ISO-639 language code (available - en, ru) theme=default # Picked theme (available - default, high-contrast, lavender, gruvbox, sans) title=🌺 Hibiscus.txt # The text in the header @@ -68,10 +68,10 @@ tg_topic=message_thread_id The Docker images are hosted via GitHub over at `ghcr.io/andrew-71/hibiscus:`, built from the [Dockerfile](./Dockerfile). This repo contains the [compose.yml](./compose.yml) that I personally use. -*Note: an outdated personally hosted [package](https://git.a71.su/Andrew71/hibiscus/packages) will be provided for some time.* +*Note: an extremely outdated self-hosted [package](https://git.a71.su/Andrew71/hibiscus/packages) will be provided for some time.* ### Executable flags -If you for some reason decide to run plain executable instead of docker, it supports following flags: +If you decide to use plain executable instead of docker, it supports the following flags: ``` -config string override config file location @@ -86,7 +86,7 @@ If you for some reason decide to run plain executable instead of docker, it supp ``` ### API methods -You can access the API at `/api/`. They are protected by same HTTP Basic Auth as "normal" site. +You can access the API at `/api/`. It is protected by same HTTP Basic Auth as "normal" routes. ``` GET /today - get file contents for today POST /today - save request body into today's file diff --git a/api.go b/api.go index 51e8a50..6138cae 100644 --- a/api.go +++ b/api.go @@ -10,15 +10,15 @@ import ( "os" ) -// HandleWrite checks for error in ResponseWriter.Write output +// HandleWrite handles error in output of ResponseWriter.Write. func HandleWrite(_ int, err error) { if err != nil { slog.Error("error writing response", "error", err) } } -// GetFile returns raw contents of a file -func GetFile(filename string, w http.ResponseWriter) { +// GetFileApi returns raw contents of a file. +func GetFileApi(filename string, w http.ResponseWriter) { fileContents, err := ReadFile(filename) if err != nil { if errors.Is(err, os.ErrNotExist) { @@ -31,8 +31,8 @@ func GetFile(filename string, w http.ResponseWriter) { HandleWrite(w.Write(fileContents)) } -// PostFile writes request's body contents to a file -func PostFile(filename string, w http.ResponseWriter, r *http.Request) { +// PostFileApi writes contents of Request.Body to a file. +func PostFileApi(filename string, w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { w.WriteHeader(http.StatusInternalServerError) @@ -49,7 +49,7 @@ func PostFile(filename string, w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } -// GetFileList returns JSON list of filenames in a directory without extensions or path +// GetFileList returns JSON list of filenames in a directory without extensions or path. func GetFileList(directory string, w http.ResponseWriter) { filenames, err := ListFiles(directory) if err != nil { @@ -64,7 +64,7 @@ func GetFileList(directory string, w http.ResponseWriter) { HandleWrite(w.Write(filenamesJson)) } -// GetDayApi returns a contents of a daily file specified in URL +// GetDayApi returns raw contents of a daily file specified in URL. func GetDayApi(w http.ResponseWriter, r *http.Request) { dayString := chi.URLParam(r, "day") if dayString == "" { @@ -72,20 +72,10 @@ func GetDayApi(w http.ResponseWriter, r *http.Request) { HandleWrite(w.Write([]byte("day not specified"))) return } - GetFile(DataFile("day/"+dayString), w) + GetFileApi(DataFile("day/"+dayString), w) } -// GetTodayApi runs GetFile with today's date as filename -func GetTodayApi(w http.ResponseWriter, _ *http.Request) { - GetFile(DataFile("day/"+TodayDate()), w) -} - -// PostTodayApi runs PostFile with today's date as filename -func PostTodayApi(w http.ResponseWriter, r *http.Request) { - PostFile(DataFile("day/"+TodayDate()), w, r) -} - -// GetNoteApi returns contents of a note specified in URL +// GetNoteApi returns contents of a note specified in URL. func GetNoteApi(w http.ResponseWriter, r *http.Request) { noteString := chi.URLParam(r, "note") if noteString == "" { @@ -93,10 +83,10 @@ func GetNoteApi(w http.ResponseWriter, r *http.Request) { HandleWrite(w.Write([]byte("note not specified"))) return } - GetFile(DataFile("notes/"+noteString), w) + GetFileApi(DataFile("notes/"+noteString), w) } -// PostNoteApi writes request's body contents to a note specified in URL +// PostNoteApi writes contents of Request.Body to a note specified in URL. func PostNoteApi(w http.ResponseWriter, r *http.Request) { noteString := chi.URLParam(r, "note") if noteString == "" { @@ -104,10 +94,10 @@ func PostNoteApi(w http.ResponseWriter, r *http.Request) { HandleWrite(w.Write([]byte("note not specified"))) return } - PostFile(DataFile("notes/"+noteString), w, r) + PostFileApi(DataFile("notes/"+noteString), w, r) } -// GraceActiveApi returns "true" if grace period is active, and "false" otherwise +// GraceActiveApi returns "true" if grace period is active, and "false" otherwise. func GraceActiveApi(w http.ResponseWriter, r *http.Request) { value := "false" if GraceActive() { @@ -116,23 +106,3 @@ func GraceActiveApi(w http.ResponseWriter, r *http.Request) { HandleWrite(w.Write([]byte(value))) w.WriteHeader(http.StatusOK) } - -// GetVersionApi returns current app version -func GetVersionApi(w http.ResponseWriter, r *http.Request) { - HandleWrite(w.Write([]byte(Info.Version))) - w.WriteHeader(http.StatusOK) -} - -// ConfigReloadApi reloads the config -func ConfigReloadApi(w http.ResponseWriter, r *http.Request) { - err := Cfg.Reload() - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - HandleWrite(w.Write([]byte(err.Error()))) - } - if r.Referer() != "" { - http.Redirect(w, r, r.Header.Get("Referer"), 302) - return - } - w.WriteHeader(http.StatusOK) -} diff --git a/auth.go b/auth.go index c28cd05..cc255e2 100644 --- a/auth.go +++ b/auth.go @@ -19,10 +19,10 @@ type failedLogin struct { var failedLogins []failedLogin -// NoteLoginFail attempts to log and counteract bruteforce/spam attacks +// NoteLoginFail attempts to log and counteract bruteforce attacks. func NoteLoginFail(username string, password string, r *http.Request) { slog.Warn("failed auth", "username", username, "password", password, "address", r.RemoteAddr) - NotifyTelegram(fmt.Sprintf(TranslatableText("info.telegram_notification")+":\nusername=%s\npassword=%s\nremote=%s", username, password, r.RemoteAddr)) + NotifyTelegram(fmt.Sprintf(TranslatableText("info.telegram.auth_fail")+":\nusername=%s\npassword=%s\nremote=%s", username, password, r.RemoteAddr)) attempt := failedLogin{username, password, time.Now()} updatedLogins := []failedLogin{attempt} @@ -40,8 +40,8 @@ func NoteLoginFail(username string, password string, r *http.Request) { } // BasicAuth is a middleware that handles authentication & authorization for the app. -// It uses BasicAuth because I doubt there is a need for something sophisticated in a small hobby project -// Originally taken from Alex Edwards's https://www.alexedwards.net/blog/basic-authentication-in-go, MIT Licensed. (13.03.2024) +// It uses BasicAuth because I doubt there is a need for something sophisticated in a small hobby project. +// Originally taken from Alex Edwards's https://www.alexedwards.net/blog/basic-authentication-in-go, MIT Licensed (13.03.2024). func BasicAuth(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { username, password, ok := r.BasicAuth() @@ -69,14 +69,14 @@ func BasicAuth(next http.Handler) http.Handler { }) } -// Scram shuts down the service, useful in case of suspected attack +// Scram shuts down the service, useful in case of suspected attack. func Scram() { slog.Warn("SCRAM triggered, shutting down") - NotifyTelegram("Hibiscus SCRAM triggered, shutting down") - os.Exit(0) // TODO: should this be 0 or 1? + NotifyTelegram(TranslatableText("info.telegram.scram")) + os.Exit(0) } -// NotifyTelegram attempts to send a message to admin through Telegram +// NotifyTelegram attempts to send a message to admin through Telegram. func NotifyTelegram(msg string) { if Cfg.TelegramChat == "" || Cfg.TelegramToken == "" { slog.Debug("ignoring telegram request due to lack of credentials") diff --git a/config.go b/config.go index 1883dd0..ab96f02 100644 --- a/config.go +++ b/config.go @@ -6,6 +6,7 @@ import ( "fmt" "log" "log/slog" + "net/http" "os" "reflect" "strconv" @@ -51,7 +52,7 @@ var DefaultConfig = Config{ TelegramTopic: "", } -// String returns text version of modified and mandatory config options +// String returns string representation of modified and mandatory config options. func (c *Config) String() string { output := "" v := reflect.ValueOf(*c) @@ -68,11 +69,13 @@ func (c *Config) String() string { return output } +// Reload resets, then loads config from the ConfigFile. +// It creates the file with mandatory options if it is missing. func (c *Config) Reload() error { *c = DefaultConfig // Reset config if _, err := os.Stat(ConfigFile); errors.Is(err, os.ErrNotExist) { - err := c.Save([]byte(c.String())) + err := c.Save() if err != nil { return err } @@ -149,17 +152,40 @@ func (c *Config) Reload() error { return SetLanguage(c.Language) // Load selected language } -// Read gets raw contents from ConfigFile +// 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) +// Save writes config's contents to the ConfigFile. +func (c *Config) Save() error { + return SaveFile(ConfigFile, []byte(c.String())) } -// ConfigInit loads config on startup +// PostConfig calls PostEntry for config file, then reloads the config. +func PostConfig(w http.ResponseWriter, r *http.Request) { + PostEntry(ConfigFile, w, r) + err := Cfg.Reload() + if err != nil { + slog.Error("error reloading config", "error", err) + } +} + +// ConfigReloadApi reloads the config. It then redirects back if Referer field is present. +func ConfigReloadApi(w http.ResponseWriter, r *http.Request) { + err := Cfg.Reload() + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + HandleWrite(w.Write([]byte(err.Error()))) + } + if r.Referer() != "" { + http.Redirect(w, r, r.Header.Get("Referer"), 302) + return + } + w.WriteHeader(http.StatusOK) +} + +// ConfigInit loads config on startup. func ConfigInit() Config { cfg := Config{} err := cfg.Reload() diff --git a/config/config.txt b/config/config.txt index 8b6b5d3..53588c0 100644 --- a/config/config.txt +++ b/config/config.txt @@ -2,4 +2,4 @@ username=admin password=admin port=7101 timezone=Local -language=en +language=en \ No newline at end of file diff --git a/export.go b/export.go index 783e7d4..6a5a32a 100644 --- a/export.go +++ b/export.go @@ -11,7 +11,7 @@ import ( var ExportPath = "data/export.zip" -// Export saves a .zip archive of the data folder to the passed filename +// Export saves a .zip archive of the data folder to a file. func Export(filename string) error { file, err := os.Create(filename) if err != nil { @@ -61,7 +61,8 @@ func Export(filename string) error { return file.Close() } -// GetExport returns a .zip archive with contents of the data folder +// GetExport returns a .zip archive with contents of the data folder. +// As a side effect, it creates the file in there. func GetExport(w http.ResponseWriter, r *http.Request) { err := Export(ExportPath) if err != nil { diff --git a/files.go b/files.go index a7d2564..375c54c 100644 --- a/files.go +++ b/files.go @@ -11,34 +11,30 @@ import ( "time" ) -// DataFile modifies file path to ensure it's a .txt inside the data folder +// 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 +// ReadFile returns contents of a file. func ReadFile(filename string) ([]byte, error) { 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", - "error", err, - "file", filename) + slog.Error("error reading file", "error", err, "file", filename) return nil, err } return fileContents, nil } -// SaveFile Writes contents to a file +// SaveFile Writes contents to a file. func SaveFile(filename string, contents []byte) error { contents = bytes.TrimSpace(contents) if len(contents) == 0 { // Delete empty files err := os.Remove(filename) - slog.Error("error deleting empty file", - "error", err, - "file", filename) + slog.Error("error deleting empty file", "error", err, "file", filename) return err } err := os.MkdirAll(path.Dir(filename), 0755) // Create dir in case it doesn't exist yet to avoid errors @@ -48,21 +44,17 @@ func SaveFile(filename string, contents []byte) error { } f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) if err != nil { - slog.Error("error opening/making file", - "error", err, - "file", filename) + slog.Error("error opening/creating file", "error", err, "file", filename) return err } if _, err := f.Write(contents); err != nil { - slog.Error("error writing to file", - "error", err, - "file", filename) + slog.Error("error writing to file", "error", err, "file", filename) return err } return nil } -// ListFiles returns slice of filenames in a directory without extensions or path +// 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/" + path.Clean(directory) + "/*.txt") @@ -76,7 +68,7 @@ func ListFiles(directory string) ([]string, error) { return filenames, nil } -// GraceActive returns whether the grace period (Cfg.GraceTime) is active +// GraceActive returns whether the grace period (Cfg.GraceTime) is active. Grace period has minute precision func GraceActive() bool { t := time.Now().In(Cfg.Timezone) active := (60*t.Hour() + t.Minute()) < int(Cfg.GraceTime.Minutes()) @@ -88,7 +80,7 @@ func GraceActive() bool { return active } -// TodayDate returns today's formatted date. It accounts for Config.GraceTime +// TodayDate returns today's formatted date. It accounts for Config.GraceTime. func TodayDate() string { dateFormatted := time.Now().In(Cfg.Timezone).Format(time.DateOnly) if GraceActive() { @@ -97,13 +89,3 @@ func TodayDate() string { 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(DataFile("day/" + TodayDate())) -} - -// SaveToday runs SaveFile with today's date as filename -func SaveToday(contents []byte) error { - return SaveFile(DataFile("day/"+TodayDate()), contents) -} diff --git a/flags.go b/flags.go index 4d56b51..921fa57 100644 --- a/flags.go +++ b/flags.go @@ -5,7 +5,7 @@ import ( "log" ) -// FlagInit processes app flags +// FlagInit processes app flags. func FlagInit() { config := flag.String("config", "", "override config file") username := flag.String("user", "", "override username") diff --git a/i18n.go b/i18n.go index f819c65..fd97164 100644 --- a/i18n.go +++ b/i18n.go @@ -10,7 +10,7 @@ import ( var I18n embed.FS var Translations = map[string]string{} -// SetLanguage loads a json file for selected language into the Translations map, with english language as a fallback +// SetLanguage loads a json file for selected language into the Translations map, with English language as a fallback. func SetLanguage(language string) error { loadLanguage := func(language string) error { filename := "i18n/" + language + ".json" @@ -23,14 +23,15 @@ func SetLanguage(language string) error { } return json.Unmarshal(fileContents, &Translations) } - err := loadLanguage("en") // Load english as fallback language + Translations = map[string]string{} // Clear the map to avoid previous language remaining + err := loadLanguage("en") // Load English as fallback if err != nil { return err } return loadLanguage(language) } -// TranslatableText attempts to match an id to a string in current language +// TranslatableText attempts to match an id to a string in current language. func TranslatableText(id string) string { if v, ok := Translations[id]; !ok { return id diff --git a/i18n/en.json b/i18n/en.json index 89585cd..b058dae 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -24,5 +24,6 @@ "info.readme": "Edit readme.txt", "info.config": "Edit config", - "info.telegram_notification": "Failed auth attempt in Hibiscus.txt" + "info.telegram.auth_fail": "Failed auth attempt in Hibiscus.txt", + "info.telegram.scram": "Hibiscus SCRAM triggered, shutting down" } \ No newline at end of file diff --git a/i18n/ru.json b/i18n/ru.json index 98c6cd8..b450647 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -24,5 +24,6 @@ "info.readme": "Редактировать readme.txt", "info.config": "Редактировать конфиг", - "info.telegram_notification": "Неверная попытка авторизации в Hibiscus.txt" + "info.telegram_notification": "Неверная попытка авторизации в Hibiscus.txt", + "info.telegram.scram": "Активирована функция SCRAM в Hibiscus.txt, сервер выключается" } \ No newline at end of file diff --git a/info.go b/info.go index 7e1fc65..4691f46 100644 --- a/info.go +++ b/info.go @@ -13,13 +13,13 @@ type AppInfo struct { SourceLink string } -// Info contains app information +// Info contains app information. var Info = AppInfo{ - Version: "1.1.2", + Version: "1.1.3", SourceLink: "https://git.a71.su/Andrew71/hibiscus", } -// GetInfo renders the info page +// GetInfo renders the info page. func GetInfo(w http.ResponseWriter, r *http.Request) { err := infoTemplate.ExecuteTemplate(w, "base", Info) if err != nil { @@ -28,3 +28,9 @@ func GetInfo(w http.ResponseWriter, r *http.Request) { return } } + +// GetVersionApi returns current app version. +func GetVersionApi(w http.ResponseWriter, r *http.Request) { + HandleWrite(w.Write([]byte(Info.Version))) + w.WriteHeader(http.StatusOK) +} diff --git a/logger.go b/logger.go index 2c90028..4633ac1 100644 --- a/logger.go +++ b/logger.go @@ -10,7 +10,7 @@ import ( var DebugMode = false -// LogInit makes slog output to both stdout and a file if needed, and enables debug mode if selected +// LogInit makes slog output to both os.Stdout and a file if needed, and sets slog.LevelDebug if enabled. func LogInit() { var w io.Writer if Cfg.LogToFile { diff --git a/routes.go b/routes.go index 85e4a37..d2ff112 100644 --- a/routes.go +++ b/routes.go @@ -27,17 +27,17 @@ type Entry struct { type formatEntries func([]string) []Entry -// Public contains the static files e.g. CSS, JS +// Public contains the static files e.g. CSS, JS. // //go:embed public var Public embed.FS -// Pages contains the HTML templates used by the app +// Pages contains the HTML templates used by the app. // //go:embed pages var Pages embed.FS -// EmbeddedPage returns contents of a file in Pages while "handling" potential errors +// EmbeddedPage returns contents of a file in Pages while "handling" potential errors. func EmbeddedPage(name string) []byte { data, err := Pages.ReadFile(name) if err != nil { @@ -55,49 +55,19 @@ var editTemplate = template.Must(template.New("").Funcs(templateFuncs).ParseFS(P var viewTemplate = template.Must(template.New("").Funcs(templateFuncs).ParseFS(Pages, "pages/base.html", "pages/entry.html")) var listTemplate = template.Must(template.New("").Funcs(templateFuncs).ParseFS(Pages, "pages/base.html", "pages/list.html")) -// NotFound returns a user-friendly 404 error page +// NotFound returns a user-friendly 404 error page. func NotFound(w http.ResponseWriter, r *http.Request) { w.WriteHeader(404) HandleWrite(w.Write(EmbeddedPage("pages/error/404.html"))) } -// InternalError returns a user-friendly 500 error page +// InternalError returns a user-friendly 500 error page. func InternalError(w http.ResponseWriter, r *http.Request) { w.WriteHeader(500) HandleWrite(w.Write(EmbeddedPage("pages/error/500.html"))) } -// GetToday renders HTML page for today's entry -func GetToday(w http.ResponseWriter, r *http.Request) { - day, err := ReadToday() - if err != nil { - if errors.Is(err, os.ErrNotExist) { - day = []byte("") - } else { - slog.Error("error reading today's file", "error", err) - InternalError(w, r) - return - } - } - - err = editTemplate.ExecuteTemplate(w, "base", Entry{Title: TranslatableText("title.today"), Content: string(day)}) - if err != nil { - slog.Error("error executing template", "error", err) - InternalError(w, r) - return - } -} - -// PostToday saves today's entry from form and redirects back to GET -func PostToday(w http.ResponseWriter, r *http.Request) { - err := SaveToday([]byte(r.FormValue("text"))) - if err != nil { - slog.Error("error saving today's file", "error", err) - } - http.Redirect(w, r, r.Header.Get("Referer"), 302) -} - -// GetEntries is a generic HTML renderer for a list +// GetEntries handles showing a list. func GetEntries(w http.ResponseWriter, r *http.Request, title string, description template.HTML, dir string, format formatEntries) { filesList, err := ListFiles(dir) if err != nil { @@ -115,10 +85,10 @@ func GetEntries(w http.ResponseWriter, r *http.Request, title string, descriptio } } -// GetDays renders HTML list of previous days' entries +// GetDays calls GetEntries for previous days' entries. func GetDays(w http.ResponseWriter, r *http.Request) { description := template.HTML( - "" + TranslatableText("prompt.days") + "") + "" + template.HTMLEscapeString(TranslatableText("prompt.days")) + "") GetEntries(w, r, TranslatableText("title.days"), description, "day", func(files []string) []Entry { var filesFormatted []Entry for i := range files { @@ -145,23 +115,23 @@ func GetDays(w http.ResponseWriter, r *http.Request) { }) } -// GetNotes renders HTML list of all notes +// GetNotes calls GetEntries for all notes. func GetNotes(w http.ResponseWriter, r *http.Request) { // This is suboptimal, but will do... description := template.HTML( - "" + TranslatableText("button.notes") + "" + + "" + template.HTMLEscapeString(TranslatableText("button.notes")) + "" + " ") GetEntries(w, r, TranslatableText("title.notes"), description, "notes", func(files []string) []Entry { var filesFormatted []Entry for _, v := range files { - titleString := strings.Replace(v, "-", " ", -1) // FIXME: what if I need a hyphen? - filesFormatted = append(filesFormatted, Entry{Title: titleString, Link: "notes/" + v}) + // titleString := strings.Replace(v, "-", " ", -1) // This would be cool, but what if I need a hyphen? + filesFormatted = append(filesFormatted, Entry{Title: v, Link: "notes/" + v}) } return filesFormatted }) } -// GetEntry handles showing a single file, editable or otherwise +// GetEntry handles showing a single file, editable or otherwise. func GetEntry(w http.ResponseWriter, r *http.Request, title string, filename string, editable bool) { entry, err := ReadFile(filename) if err != nil { @@ -185,7 +155,19 @@ func GetEntry(w http.ResponseWriter, r *http.Request, title string, filename str } } -// GetDay renders HTML page for a specific day entry +// PostEntry saves value of "text" HTML form component to a file and redirects back to Referer if present. +func PostEntry(filename string, w http.ResponseWriter, r *http.Request) { + err := SaveFile(filename, []byte(r.FormValue("text"))) + if err != nil { + slog.Error("error saving file", "error", err, "file", filename) + } + if r.Referer() != "" { + http.Redirect(w, r, r.Header.Get("Referer"), 302) + return + } +} + +// GetDay calls GetEntry for a day entry. func GetDay(w http.ResponseWriter, r *http.Request) { dayString := chi.URLParam(r, "day") if dayString == "" { @@ -207,7 +189,7 @@ func GetDay(w http.ResponseWriter, r *http.Request) { GetEntry(w, r, title, DataFile("day/"+dayString), false) } -// GetNote renders HTML page for a note +// GetNote calls GetEntry for a note. func GetNote(w http.ResponseWriter, r *http.Request) { noteString := chi.URLParam(r, "note") if noteString == "" { @@ -223,7 +205,7 @@ func GetNote(w http.ResponseWriter, r *http.Request) { GetEntry(w, r, noteString, DataFile("notes/"+noteString), true) } -// PostNote saves a note form and redirects back to GET +// PostNote calls PostEntry for a note. func PostNote(w http.ResponseWriter, r *http.Request) { noteString := chi.URLParam(r, "note") if noteString == "" { @@ -231,41 +213,5 @@ func PostNote(w http.ResponseWriter, r *http.Request) { HandleWrite(w.Write([]byte("note not specified"))) return } - err := SaveFile(DataFile("notes/"+noteString), []byte(r.FormValue("text"))) - if err != nil { - slog.Error("error saving a note", "note", noteString, "error", err) - } - http.Redirect(w, r, r.Header.Get("Referer"), 302) -} - -// GetReadme calls GetEntry for readme.txt -func GetReadme(w http.ResponseWriter, r *http.Request) { - 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(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) + PostEntry(DataFile("notes/"+noteString), w, r) } diff --git a/serve.go b/serve.go index 064e98a..68278d4 100644 --- a/serve.go +++ b/serve.go @@ -9,7 +9,7 @@ import ( "strconv" ) -// Serve starts the app's web server +// Serve starts the app's web server. func Serve() { r := chi.NewRouter() r.Use(middleware.RealIP) @@ -19,32 +19,34 @@ func Serve() { // Routes ========== userRouter := chi.NewRouter() userRouter.Use(BasicAuth) - userRouter.Get("/", GetToday) - userRouter.Post("/", PostToday) + userRouter.Get("/", func(w http.ResponseWriter, r *http.Request) { + GetEntry(w, r, TranslatableText("title.today"), DataFile("day/"+TodayDate()), true) + }) + userRouter.Post("/", func(w http.ResponseWriter, r *http.Request) { PostEntry(DataFile("day/"+TodayDate()), w, r) }) userRouter.Get("/day", GetDays) userRouter.Get("/day/{day}", GetDay) userRouter.Get("/notes", GetNotes) userRouter.Get("/notes/{note}", GetNote) userRouter.Post("/notes/{note}", PostNote) userRouter.Get("/info", GetInfo) - userRouter.Get("/readme", GetReadme) - userRouter.Post("/readme", PostReadme) - userRouter.Get("/config", GetConfig) + userRouter.Get("/readme", func(w http.ResponseWriter, r *http.Request) { GetEntry(w, r, "readme.txt", DataFile("readme"), true) }) + userRouter.Post("/readme", func(w http.ResponseWriter, r *http.Request) { PostEntry(DataFile("readme"), w, r) }) + userRouter.Get("/config", func(w http.ResponseWriter, r *http.Request) { GetEntry(w, r, "config.txt", ConfigFile, true) }) userRouter.Post("/config", PostConfig) r.Mount("/", userRouter) // API ============= apiRouter := chi.NewRouter() apiRouter.Use(BasicAuth) - apiRouter.Get("/readme", func(w http.ResponseWriter, r *http.Request) { GetFile("readme", w) }) - apiRouter.Post("/readme", func(w http.ResponseWriter, r *http.Request) { PostFile("readme", w, r) }) + apiRouter.Get("/readme", func(w http.ResponseWriter, r *http.Request) { GetFileApi("readme", w) }) + apiRouter.Post("/readme", func(w http.ResponseWriter, r *http.Request) { PostFileApi("readme", w, r) }) apiRouter.Get("/day", func(w http.ResponseWriter, r *http.Request) { GetFileList("day", w) }) apiRouter.Get("/day/{day}", GetDayApi) apiRouter.Get("/notes", func(w http.ResponseWriter, r *http.Request) { GetFileList("notes", w) }) apiRouter.Get("/notes/{note}", GetNoteApi) apiRouter.Post("/notes/{note}", PostNoteApi) - apiRouter.Get("/today", GetTodayApi) - apiRouter.Post("/today", PostTodayApi) + apiRouter.Get("/today", func(w http.ResponseWriter, r *http.Request) { GetFileApi(DataFile("day/"+TodayDate()), w) }) + apiRouter.Post("/today", func(w http.ResponseWriter, r *http.Request) { PostEntry(DataFile("day/"+TodayDate()), w, r) }) apiRouter.Get("/export", GetExport) apiRouter.Get("/grace", GraceActiveApi) apiRouter.Get("/version", GetVersionApi) From 2b0f9c139a7728d30cf587d65b81a37fca054dd0 Mon Sep 17 00:00:00 2001 From: Andrew-71 Date: Tue, 6 Aug 2024 00:51:05 +0300 Subject: [PATCH 5/9] Improve README.md --- README.md | 8 ++++---- TODO.md | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d577bbb..51e5fc9 100644 --- a/README.md +++ b/README.md @@ -39,11 +39,11 @@ config Deleting notes is done by clearing contents and clicking "Save" - the app deletes empty files when saving. ### Config options: -Below are available configuration options and their defaults. -The settings are defined as newline separated key=value pairs in config.txt. -If you do not provide an option in your config, it will be using the default. +Below are the available configuration options and their defaults. +The settings are defined as newline separated `key=value` pairs in the config file. +If you do not provide an option, the default will be used. Please don't include the bash-style "comments" in your actual config, -they are provided purely for demonstration only and **will break the config if present**. +they are provided purely for demonstration and **will break the config if present**. ``` username=admin # Your username password=admin # Your password diff --git a/TODO.md b/TODO.md index 1c7cd1a..de2299c 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,7 @@ # TODO List of things to add to this project +* Auth improvement so it DOESN'T ASK ME FOR PASSWORD EVERY DAY UGH XD * Forward/backward buttons for days ## Brainstorming From 8ae76cc8e886806e9dcc030afa056f129c5588d3 Mon Sep 17 00:00:00 2001 From: Andrew71 Date: Wed, 28 Aug 2024 14:53:54 +0300 Subject: [PATCH 6/9] Fix language tag and improve templates --- CHANGELOG.md | 5 +++++ README.md | 2 +- config.go | 2 +- i18n/en.json | 2 ++ i18n/ru.json | 4 +++- info.go | 2 +- pages/base.html | 22 +++++++++++----------- pages/edit.html | 4 ++-- pages/entry.html | 4 ++-- pages/info.html | 4 ++-- pages/list.html | 10 +++++----- public/themes/default.css | 1 - 12 files changed, 35 insertions(+), 27 deletions(-) delete mode 100644 public/themes/default.css diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fe3438..366a6d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # Changelog This file keeps track of changes in a human-readable fashion +## v1.1.4 +* Fixed HTML `lang` tag +* Theme CSS link is now only present if non-default is set +* Improved template consistency (backend) + ## v1.1.3 This release mostly consists of backend improvements * List items no longer replace hyphens with spaces for consistency diff --git a/README.md b/README.md index 51e5fc9..46674fa 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ port=7101 # What port to run on (probably leave on 7101 if using docker) timezone=Local # IANA Time zone database identifier ("UTC", Local", "Europe/Moscow" etc.), Defaults to Local if can't parse. grace_period=0s # Time after a new day begins, but before the switch to next day's file. e.g. 3h26m - files will change at 3:26am language=en # ISO-639 language code (available - en, ru) -theme=default # Picked theme (available - default, high-contrast, lavender, gruvbox, sans) +theme="" # Picked theme (available - default (if left empty), high-contrast, lavender, gruvbox, sans) 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 diff --git a/config.go b/config.go index ab96f02..6316dee 100644 --- a/config.go +++ b/config.go @@ -41,7 +41,7 @@ var DefaultConfig = Config{ Timezone: time.Local, GraceTime: 0, Language: "en", - Theme: "default", + Theme: "", Title: "🌺 Hibiscus.txt", LogToFile: false, LogFile: "config/log.txt", diff --git a/i18n/en.json b/i18n/en.json index b058dae..6ce3567 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1,4 +1,6 @@ { + "lang": "en-UK", + "title.today": "Your day so far", "title.days": "Previous days", "title.notes": "Notes", diff --git a/i18n/ru.json b/i18n/ru.json index b450647..ac34428 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -1,4 +1,6 @@ { + "lang": "ru", + "title.today": "Сегодняшний день", "title.days": "Предыдущие дни", "title.notes": "Заметки", @@ -11,7 +13,7 @@ "link.info": "системная информация", "time.date": "Сегодня", - "time.grace": "льготный период", + "time.grace": "редактируется вчерашний день", "button.save": "Сохранить", "button.notes": "Новая заметка", "prompt.notes": "Название заметки", diff --git a/info.go b/info.go index 4691f46..53fc8f5 100644 --- a/info.go +++ b/info.go @@ -15,7 +15,7 @@ type AppInfo struct { // Info contains app information. var Info = AppInfo{ - Version: "1.1.3", + Version: "1.1.4", SourceLink: "https://git.a71.su/Andrew71/hibiscus", } diff --git a/pages/base.html b/pages/base.html index 280ff4d..d1f0f67 100644 --- a/pages/base.html +++ b/pages/base.html @@ -1,11 +1,11 @@ -{{define "header"}} +{{ define "header" }}

{{ config.Title }}

-

{{translatableText "time.date"}} a place

+

{{ translatableText "time.date" }} a place

-{{end}} +{{ end }} -{{define "base"}} +{{- define "base" -}} @@ -14,16 +14,16 @@ - + {{- if config.Theme -}}{{ end }} Hibiscus.txt - {{template "header" .}} + {{- template "header" . -}}
- {{template "main" .}} + {{- template "main" . -}}
- {{template "footer" .}} + {{- template "footer" . -}} -{{end}} +{{ end }} -{{define "footer"}} +{{ define "footer" }} -{{end}} \ No newline at end of file +{{ end }} \ No newline at end of file diff --git a/pages/edit.html b/pages/edit.html index dca9637..0598395 100644 --- a/pages/edit.html +++ b/pages/edit.html @@ -1,7 +1,7 @@ -{{define "main"}} +{{ define "main" }}

-{{end}} \ No newline at end of file +{{ end }} \ No newline at end of file diff --git a/pages/entry.html b/pages/entry.html index 11834e5..e61e9fd 100644 --- a/pages/entry.html +++ b/pages/entry.html @@ -1,4 +1,4 @@ -{{define "main"}} +{{ define "main" }}

-{{end}} \ No newline at end of file +{{ end }} \ No newline at end of file diff --git a/pages/info.html b/pages/info.html index 92ab91a..584e8e4 100644 --- a/pages/info.html +++ b/pages/info.html @@ -1,4 +1,4 @@ -{{define "main"}} +{{ define "main" }}

{{ translatableText "title.info" }}

-{{end}} \ No newline at end of file +{{ end }} \ No newline at end of file diff --git a/pages/list.html b/pages/list.html index f902047..5513cb2 100644 --- a/pages/list.html +++ b/pages/list.html @@ -1,9 +1,9 @@ -{{define "main"}} -

{{.Title}}

-

{{.Description}}

+{{ define "main" }} +

{{ .Title }}

+

{{ .Description }}

    - {{range .Entries}} + {{ range .Entries }}
  • {{.Title}}
  • - {{end}} + {{ end }}
{{end}} \ No newline at end of file diff --git a/public/themes/default.css b/public/themes/default.css deleted file mode 100644 index d8ee906..0000000 --- a/public/themes/default.css +++ /dev/null @@ -1 +0,0 @@ -/* Default theme is defined in main.css */ \ No newline at end of file From 6d37c363bb9c5bfd36903e14e9f739c0f9e152f3 Mon Sep 17 00:00:00 2001 From: Andrew-71 Date: Mon, 7 Oct 2024 16:04:39 +0300 Subject: [PATCH 7/9] Translate error pages --- CHANGELOG.md | 29 +++++++++++++++++++++++++++-- TODO.md | 9 ++++++++- config.go | 10 ++-------- go.mod | 2 +- i18n/en.json | 6 +++++- i18n/ru.json | 6 +++++- pages/error/404.html | 8 +++++--- pages/error/500.html | 8 +++++--- public/main.css | 24 ++++++++++++++++-------- routes.go | 18 ++++++++++++++++-- 10 files changed, 90 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 366a6d0..39e733a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,30 +1,45 @@ # Changelog This file keeps track of changes in a human-readable fashion +## Upcoming +These changes were not yet released + +* Brought default CSS up to date with my personal website +* Error pages are now translated + ## v1.1.4 + * Fixed HTML `lang` tag * Theme CSS link is now only present if non-default is set * Improved template consistency (backend) ## v1.1.3 + This release mostly consists of backend improvements * List items no longer replace hyphens with spaces for consistency * Telegram message for SCRAM is now translatable * Ensured HTML escape in list descriptions * Refactored many methods, improved comments + ## v1.1.2 + This release contains a few bug fixes * Real IPs are now logged (By Rithas K.) * CSS now has `box-sizing: border-box` to fix textarea in some cases (By Rithas K.) * Done some minor code housekeeping + ## v1.1.1 + This release is mostly a technicality, with a move over to GitHub (`ghcr.io/andrew-71/hibiscus`) for packages due to DockerHub's prior anti-Russian actions making old "CI/CD" unsustainable. + ## v1.1.0 + * You can now specify the Telegram *topic* to send notification to via `tg_topic` config key (By Rithas K.) * The Telegram message is now partially translated * Fixed CSS `margin` and `text-align` inherited from my website ## v1.0.0 + This release includes several **breaking** changes * Made a new favicon * English is now used as a fallback language, making incomplete translations somewhat usable @@ -38,16 +53,20 @@ This release includes several **breaking** changes * Comic Sans MS for *everything* * sorry +## v0.6.1 + +* Fixed date display when using `Local` timezone + ## 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.6.1 -* Fixed date display when using `Local` timezone ## v0.5.0 + * Added a JS prompt to create new note * "Sanitization" for this method is basic and assumes a well-meaning user * Old instructions appear if JS is disabled @@ -56,6 +75,7 @@ This release includes several **breaking** changes * Config reload now sets removed values to defaults ## v0.4.0 + * Customisation changes * Added `title` option to config * Controls the text in the header, "🌺 Hibiscus.txt" by default @@ -66,17 +86,20 @@ This release includes several **breaking** changes * Spaces in config options are now supported (basically just for `title`) ## v0.3.0 + * Added themes * Picked theme is set by `theme` key in config. Default is ...`default` * Themes are defined in `/public/themes/.css` and modify colours (or, theoretically, do more) * Current pre-made themes are `default`, `gruvbox` and `high-contrast` ## v0.2.0 + * Added config reload * Can be reloaded in info page * Can be reloaded with new `reload` api method (be aware of the redirect if referer is present) ## 7 May 2024 - v0.1.0 + * Began move towards [semantic versioning](https://semver.org/) * Current version is now 0.1.0 * Added `version` api method @@ -91,6 +114,7 @@ This release includes several **breaking** changes * Fixed export function failing ## 6 May 2024 + * Grace period is now non-inclusive (so `4h` means the switch will happen right at `4:00`, not `4:01`) * Added API method to check if grace period is active * Made changes to date display on frontend @@ -100,6 +124,7 @@ This release includes several **breaking** changes But I think it's fine. ## 5 May 2024 + * Added this changelog * Added grace period (as per suggestions) * Set in config like `grace_period=3h26m` (via Go's `time.ParseDuration`) diff --git a/TODO.md b/TODO.md index de2299c..f5303b4 100644 --- a/TODO.md +++ b/TODO.md @@ -1,8 +1,15 @@ # TODO List of things to add to this project +## Urgent (1.1.5-2.0.0) +* `style.css` in config instead of theme (provide themes as examples in repo) +* man page, maybe try packaging for fun * Auth improvement so it DOESN'T ASK ME FOR PASSWORD EVERY DAY UGH XD + +## Nice to have * Forward/backward buttons for days +* Changelog included +* Refactor code ## Brainstorming Don't expect any of this, these are ideas floating inside my head @@ -17,4 +24,4 @@ Don't expect any of this, these are ideas floating inside my head Is this even a feature that fits the vision? * Better, *multi-page* docs in case others want to use this for some reason * Check export function for improvements -* *Go* dependency-less? <-- this is a terrible idea +* *Go* dependency-less? <-- this is a terrible idea \ No newline at end of file diff --git a/config.go b/config.go index 6316dee..0426092 100644 --- a/config.go +++ b/config.go @@ -120,13 +120,7 @@ func (c *Config) Reload() error { } } case "bool": - { - if v == "true" { - fieldElem.SetBool(true) - } else { - fieldElem.SetBool(false) - } - } + fieldElem.SetBool(v == "true") case "location": timezone = v case "duration": @@ -179,7 +173,7 @@ func ConfigReloadApi(w http.ResponseWriter, r *http.Request) { HandleWrite(w.Write([]byte(err.Error()))) } if r.Referer() != "" { - http.Redirect(w, r, r.Header.Get("Referer"), 302) + http.Redirect(w, r, r.Header.Get("Referer"), http.StatusFound) return } w.WriteHeader(http.StatusOK) diff --git a/go.mod b/go.mod index c39d590..d798fb8 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module hibiscus +module hibiscus-txt go 1.22 diff --git a/i18n/en.json b/i18n/en.json index 6ce3567..c6fac67 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -27,5 +27,9 @@ "info.config": "Edit config", "info.telegram.auth_fail": "Failed auth attempt in Hibiscus.txt", - "info.telegram.scram": "Hibiscus SCRAM triggered, shutting down" + "info.telegram.scram": "Hibiscus SCRAM triggered, shutting down", + + "error.404": "The page you were looking for doesn't exist", + "error.500": "It's probably not your fault, but something broke", + "error.prompt": "Go home?" } \ No newline at end of file diff --git a/i18n/ru.json b/i18n/ru.json index ac34428..3ac0302 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -27,5 +27,9 @@ "info.config": "Редактировать конфиг", "info.telegram_notification": "Неверная попытка авторизации в Hibiscus.txt", - "info.telegram.scram": "Активирована функция SCRAM в Hibiscus.txt, сервер выключается" + "info.telegram.scram": "Активирована функция SCRAM в Hibiscus.txt, сервер выключается", + + "error.404": "Страница, которую вы ищете, не существует", + "error.500": "Что-то сломалось", + "error.prompt": "На главную?" } \ No newline at end of file diff --git a/pages/error/404.html b/pages/error/404.html index bbde5d1..48af44e 100644 --- a/pages/error/404.html +++ b/pages/error/404.html @@ -1,3 +1,4 @@ +{{- define "404" -}} @@ -10,8 +11,9 @@

Error 404 - Not Found

-

The page you were looking for doesn't exist or was moved

-

Go home?

+

{{ translatableText "error.404" }}

+

{{ translatableText "error.prompt" }}

- \ No newline at end of file + +{{ end }} \ No newline at end of file diff --git a/pages/error/500.html b/pages/error/500.html index a58daf5..4da0874 100644 --- a/pages/error/500.html +++ b/pages/error/500.html @@ -1,3 +1,4 @@ +{{- define "500" -}} @@ -10,8 +11,9 @@

Error 500 - Internal Server Error

-

It's probably not your fault, but something broke

-

Go home?

+

{{ translatableText "error.500" }}

+

{{ translatableText "error.prompt" }}

- \ No newline at end of file + +{{ end }} \ No newline at end of file diff --git a/public/main.css b/public/main.css index 19250a4..e92af6a 100644 --- a/public/main.css +++ b/public/main.css @@ -1,12 +1,12 @@ /* Default theme */ :root { /* Light theme */ - --text-light: #454545; - --bg-light: #f5f0e1; + --text-light: #2b2a2a; + --bg-light: #f4edd7; --clickable-light: #f85552; --clickable-hover-light: #e66868; - --clickable-label-light: #f5f2ee; + --clickable-label-light: #f4edd7; --text-hover-light: #656565; --textarea-bg-light: #f5f2ee; @@ -14,12 +14,12 @@ /* Dark theme */ --text-dark: #f5f0e1; - --bg-dark: #2c2825; + --bg-dark: #1b1916; --clickable-dark: #f85552; --clickable-hover-dark: #e66868; --clickable-label-dark: #f5f2ee; - --text-hover-dark: #656565; + --text-hover-dark: #a9a8a4; --textarea-bg-dark: #383030; --textarea-border-dark: #454545; @@ -29,21 +29,29 @@ body { color: var(--text-light); background-color: var(--bg-light); + font-size: 18px; margin: auto auto; max-width: 640px; - padding: 1em; + padding: 15px; line-height: 1.4; font-family: serif; min-height: 85vh; display: flex; flex-direction: column; } +h1,h2,h3,h4,h5,h6 { line-height: 1.2 } + a, a:visited { color: var(--clickable-light); } a:hover, a:visited:hover { color: var(--clickable-hover-light); } a.no-accent, a.no-accent:visited { color: var(--text-light); } a.no-accent:hover, a.no-accent:visited:hover { color: var(--text-hover-light); } -h2 { margin-bottom:12px; } +ul:not(li ul), ol:not(li ol){ + margin-left: 0; + padding-left: 0; + list-style-position: inside; +} + .list-title { margin-bottom: 0} .list-desc { margin-top: 0 } @@ -55,7 +63,7 @@ textarea, input { resize: vertical; outline: 0; box-shadow: none; - border: 0.0625em solid var(--textarea-border-light); + border: 2px solid var(--textarea-border-light); margin-bottom: 1em; font-size: 18px; } diff --git a/routes.go b/routes.go index d2ff112..ba347ca 100644 --- a/routes.go +++ b/routes.go @@ -55,16 +55,30 @@ var editTemplate = template.Must(template.New("").Funcs(templateFuncs).ParseFS(P var viewTemplate = template.Must(template.New("").Funcs(templateFuncs).ParseFS(Pages, "pages/base.html", "pages/entry.html")) var listTemplate = template.Must(template.New("").Funcs(templateFuncs).ParseFS(Pages, "pages/base.html", "pages/list.html")) +var template404 = template.Must(template.New("404").Funcs(templateFuncs).ParseFS(Pages, "pages/error/404.html")) // NotFound returns a user-friendly 404 error page. func NotFound(w http.ResponseWriter, r *http.Request) { w.WriteHeader(404) - HandleWrite(w.Write(EmbeddedPage("pages/error/404.html"))) + + err := template404.Execute(w, nil) + if err != nil { + slog.Error("error rendering error 404 page", "error", err) + InternalError(w, r) + return + } } +var template500 = template.Must(template.New("500").Funcs(templateFuncs).ParseFS(Pages, "pages/error/500.html")) // InternalError returns a user-friendly 500 error page. func InternalError(w http.ResponseWriter, r *http.Request) { w.WriteHeader(500) - HandleWrite(w.Write(EmbeddedPage("pages/error/500.html"))) + + err := template500.Execute(w, nil) + if err != nil { // Well this is awkward + slog.Error("error rendering error 500 page", "error", err) + HandleWrite(w.Write([]byte("500. Something went *very* wrong."))) + return + } } // GetEntries handles showing a list. From eb3c1fb32a1cda83419b04b688a2eabf73c6af26 Mon Sep 17 00:00:00 2001 From: Andrew-71 Date: Tue, 8 Oct 2024 14:49:49 +0300 Subject: [PATCH 8/9] Improve default theme --- CHANGELOG.md | 2 +- Makefile | 8 ++++++++ README.md | 19 ++++++++++++++----- auth.go | 2 +- public/main.css | 16 ++++++++-------- routes.go | 4 ++-- 6 files changed, 34 insertions(+), 17 deletions(-) create mode 100644 Makefile diff --git a/CHANGELOG.md b/CHANGELOG.md index 39e733a..40264af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ This file keeps track of changes in a human-readable fashion ## Upcoming These changes were not yet released -* Brought default CSS up to date with my personal website +* Adjusted default theme * Error pages are now translated ## v1.1.4 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..51564d3 --- /dev/null +++ b/Makefile @@ -0,0 +1,8 @@ +build: + go build + +run: + go build & ./hibiscus-txt + +dev: + go build & ./hibiscus-txt --config config/dev-config.txt \ No newline at end of file diff --git a/README.md b/README.md index 46674fa..de0c9b6 100644 --- a/README.md +++ b/README.md @@ -6,22 +6,27 @@ This project is *very* opinionated and minimal, and is designed primarily for my As a result, I can't guarantee that it's either secure or stable. ## Features: + * Each day, you get a new text file. You have until the end of that very day to finalise it. * You can save named notes to document milestones, big events, or just a nice game you played this month * You can easily export the files in a `.zip` archive for backups -* Everything is plain(text) and simple. No databases, encryption, OAuth, or anything fancy. Even the password is plainte- *wait is this a feature?* -* [Docker support](#docker-deployment) (in fact, that's probably the best way to run this) +* Everything is plain(text) and simple. +No databases, encryption, OAuth, or anything fancy. Even the password is plainte- *wait is this a feature?* +* [Docker support](#docker-deployment) * Optional Telegram notifications for failed login attempts ## Technical details -[CHANGELOG.md](./CHANGELOG.md) provides a good overview of updates, and [TODO.md](./TODO.md) file shows what I will (or *may*) work on in the future. -You can read a relevant entry in my blog [here](https://a71.su/notes/hibiscus/). +[CHANGELOG.md](./CHANGELOG.md) provides a good overview of updates, and [TODO.md](./TODO.md) file shows my plans for the future. + +You can read a relevant entry in my blog [here](https://a71.su/notes/hibiscus/). It provides some useful information and context for why this app exists in the first place. -This repository is [self-hosted by me](https://git.a71.su/Andrew71/hibiscus), but [mirrored to GitHub](https://github.com/Andrew-71/hibiscus) in case my server goes down. +This repository is [self-hosted by me](https://git.a71.su/Andrew71/hibiscus), +but [mirrored to GitHub](https://github.com/Andrew-71/hibiscus). ### Data format: + ``` data +-- day @@ -39,6 +44,7 @@ config Deleting notes is done by clearing contents and clicking "Save" - the app deletes empty files when saving. ### Config options: + Below are the available configuration options and their defaults. The settings are defined as newline separated `key=value` pairs in the config file. If you do not provide an option, the default will be used. @@ -65,12 +71,14 @@ tg_topic=message_thread_id ``` ### Docker deployment: + The Docker images are hosted via GitHub over at `ghcr.io/andrew-71/hibiscus:`, built from the [Dockerfile](./Dockerfile). This repo contains the [compose.yml](./compose.yml) that I personally use. *Note: an extremely outdated self-hosted [package](https://git.a71.su/Andrew71/hibiscus/packages) will be provided for some time.* ### Executable flags + If you decide to use plain executable instead of docker, it supports the following flags: ``` -config string @@ -86,6 +94,7 @@ If you decide to use plain executable instead of docker, it supports the followi ``` ### API methods + You can access the API at `/api/`. It is protected by same HTTP Basic Auth as "normal" routes. ``` GET /today - get file contents for today diff --git a/auth.go b/auth.go index cc255e2..0f2aee7 100644 --- a/auth.go +++ b/auth.go @@ -27,7 +27,7 @@ func NoteLoginFail(username string, password string, r *http.Request) { attempt := failedLogin{username, password, time.Now()} updatedLogins := []failedLogin{attempt} for _, attempt := range failedLogins { - if 100 > time.Now().Sub(attempt.Timestamp).Abs().Seconds() { + if 100 > time.Since(attempt.Timestamp).Seconds() { updatedLogins = append(updatedLogins, attempt) } } diff --git a/public/main.css b/public/main.css index e92af6a..993eb06 100644 --- a/public/main.css +++ b/public/main.css @@ -4,25 +4,25 @@ --text-light: #2b2a2a; --bg-light: #f4edd7; - --clickable-light: #f85552; - --clickable-hover-light: #e66868; + --clickable-light: #ed3e3b; + --clickable-hover-light: #e55552; --clickable-label-light: #f4edd7; --text-hover-light: #656565; - --textarea-bg-light: #f5f2ee; - --textarea-border-light: #454545; + --textarea-bg-light: #f9f5e4; + --textarea-border-light: #c3c3c2; /* Dark theme */ --text-dark: #f5f0e1; --bg-dark: #1b1916; - --clickable-dark: #f85552; - --clickable-hover-dark: #e66868; + --clickable-dark: #ed3e3b; + --clickable-hover-dark: #ae3836; --clickable-label-dark: #f5f2ee; --text-hover-dark: #a9a8a4; - --textarea-bg-dark: #383030; - --textarea-border-dark: #454545; + --textarea-bg-dark: #201d1b; /* 252020 f5f0e1 */ + --textarea-border-dark: #2c2727; } * { box-sizing: border-box; } diff --git a/routes.go b/routes.go index ba347ca..4386e57 100644 --- a/routes.go +++ b/routes.go @@ -176,7 +176,7 @@ func PostEntry(filename string, w http.ResponseWriter, r *http.Request) { slog.Error("error saving file", "error", err, "file", filename) } if r.Referer() != "" { - http.Redirect(w, r, r.Header.Get("Referer"), 302) + http.Redirect(w, r, r.Header.Get("Referer"), http.StatusFound) return } } @@ -190,7 +190,7 @@ func GetDay(w http.ResponseWriter, r *http.Request) { return } if dayString == TodayDate() { // Today can still be edited - http.Redirect(w, r, "/", 302) + http.Redirect(w, r, "/", http.StatusFound) return } From b56ce43c80d000e7fe10b8697982fa7f56169aee Mon Sep 17 00:00:00 2001 From: Andrew-71 Date: Fri, 11 Oct 2024 11:13:55 +0300 Subject: [PATCH 9/9] Fix Makefile --- Makefile | 4 ++-- TODO.md | 5 ++++- routes.go | 7 +++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 51564d3..4adeae8 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ build: go build run: - go build & ./hibiscus-txt + go build && ./hibiscus-txt dev: - go build & ./hibiscus-txt --config config/dev-config.txt \ No newline at end of file + go build && ./hibiscus-txt --config config/dev-config.txt \ No newline at end of file diff --git a/TODO.md b/TODO.md index f5303b4..1245c97 100644 --- a/TODO.md +++ b/TODO.md @@ -3,13 +3,16 @@ List of things to add to this project ## Urgent (1.1.5-2.0.0) * `style.css` in config instead of theme (provide themes as examples in repo) -* man page, maybe try packaging for fun * Auth improvement so it DOESN'T ASK ME FOR PASSWORD EVERY DAY UGH XD ## Nice to have * Forward/backward buttons for days * Changelog included * Refactor code +* API field for version +* Notifications late in the day +* man page, maybe try packaging for fun +* store passwords as hash ## Brainstorming Don't expect any of this, these are ideas floating inside my head diff --git a/routes.go b/routes.go index 4386e57..237b089 100644 --- a/routes.go +++ b/routes.go @@ -3,7 +3,6 @@ package main import ( "embed" "errors" - "github.com/go-chi/chi/v5" "html/template" "log/slog" "net/http" @@ -11,6 +10,8 @@ import ( "os" "strings" "time" + + "github.com/go-chi/chi/v5" ) type EntryList struct { @@ -56,6 +57,7 @@ var viewTemplate = template.Must(template.New("").Funcs(templateFuncs).ParseFS(P var listTemplate = template.Must(template.New("").Funcs(templateFuncs).ParseFS(Pages, "pages/base.html", "pages/list.html")) var template404 = template.Must(template.New("404").Funcs(templateFuncs).ParseFS(Pages, "pages/error/404.html")) + // NotFound returns a user-friendly 404 error page. func NotFound(w http.ResponseWriter, r *http.Request) { w.WriteHeader(404) @@ -69,12 +71,13 @@ func NotFound(w http.ResponseWriter, r *http.Request) { } var template500 = template.Must(template.New("500").Funcs(templateFuncs).ParseFS(Pages, "pages/error/500.html")) + // InternalError returns a user-friendly 500 error page. func InternalError(w http.ResponseWriter, r *http.Request) { w.WriteHeader(500) err := template500.Execute(w, nil) - if err != nil { // Well this is awkward + if err != nil { // Well this is awkward slog.Error("error rendering error 500 page", "error", err) HandleWrite(w.Write([]byte("500. Something went *very* wrong."))) return