Refactor functions and improve comments
This commit is contained in:
parent
f33206c99d
commit
9cab989b78
16 changed files with 148 additions and 205 deletions
21
CHANGELOG.md
21
CHANGELOG.md
|
@ -1,17 +1,24 @@
|
||||||
# Changelog
|
# 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
|
## v1.1.2
|
||||||
This version contains changes from pull request #2 by Rithas K.
|
This release contains a few bug fixes
|
||||||
* Real IPs are now logged
|
* Real IPs are now logged (By Rithas K.)
|
||||||
* Textarea has been fixed Safari
|
* CSS now has `box-sizing: border-box` to fix textarea in some cases (By Rithas K.)
|
||||||
* Done some minor behind-the-scenes housekeeping
|
* Done some minor code housekeeping
|
||||||
## v1.1.1
|
## 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
|
## 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
|
* The Telegram message is now partially translated
|
||||||
* Fixed CSS `margin` and `text-align` inherited from my website
|
* Fixed CSS `margin` and `text-align` inherited from my website
|
||||||
|
|
||||||
## v1.0.0
|
## v1.0.0
|
||||||
This release includes several **breaking** changes
|
This release includes several **breaking** changes
|
||||||
* Made a new favicon
|
* Made a new favicon
|
||||||
|
|
12
README.md
12
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
|
* 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?*
|
* 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
|
* Optional Telegram notifications for failed login attempts
|
||||||
|
|
||||||
## Technical details
|
## 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/).
|
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.
|
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:
|
### Data format:
|
||||||
```
|
```
|
||||||
|
@ -49,7 +49,7 @@ username=admin # Your username
|
||||||
password=admin # Your password
|
password=admin # Your password
|
||||||
port=7101 # What port to run on (probably leave on 7101 if using docker)
|
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.
|
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)
|
language=en # ISO-639 language code (available - en, ru)
|
||||||
theme=default # Picked theme (available - default, high-contrast, lavender, gruvbox, sans)
|
theme=default # Picked theme (available - default, high-contrast, lavender, gruvbox, sans)
|
||||||
title=🌺 Hibiscus.txt # The text in the header
|
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:<tag>`,
|
The Docker images are hosted via GitHub over at `ghcr.io/andrew-71/hibiscus:<tag>`,
|
||||||
built from the [Dockerfile](./Dockerfile).
|
built from the [Dockerfile](./Dockerfile).
|
||||||
This repo contains the [compose.yml](./compose.yml) that I personally use.
|
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
|
### 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
|
-config string
|
||||||
override config file location
|
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
|
### API methods
|
||||||
You can access the API at `/api/<method>`. They are protected by same HTTP Basic Auth as "normal" site.
|
You can access the API at `/api/<method>`. It is protected by same HTTP Basic Auth as "normal" routes.
|
||||||
```
|
```
|
||||||
GET /today - get file contents for today
|
GET /today - get file contents for today
|
||||||
POST /today - save request body into today's file
|
POST /today - save request body into today's file
|
||||||
|
|
56
api.go
56
api.go
|
@ -10,15 +10,15 @@ import (
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HandleWrite checks for error in ResponseWriter.Write output
|
// HandleWrite handles error in output of ResponseWriter.Write.
|
||||||
func HandleWrite(_ int, err error) {
|
func HandleWrite(_ int, err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("error writing response", "error", err)
|
slog.Error("error writing response", "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFile returns raw contents of a file
|
// GetFileApi returns raw contents of a file.
|
||||||
func GetFile(filename string, w http.ResponseWriter) {
|
func GetFileApi(filename string, w http.ResponseWriter) {
|
||||||
fileContents, err := ReadFile(filename)
|
fileContents, err := ReadFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
@ -31,8 +31,8 @@ func GetFile(filename string, w http.ResponseWriter) {
|
||||||
HandleWrite(w.Write(fileContents))
|
HandleWrite(w.Write(fileContents))
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostFile writes request's body contents to a file
|
// PostFileApi writes contents of Request.Body to a file.
|
||||||
func PostFile(filename string, w http.ResponseWriter, r *http.Request) {
|
func PostFileApi(filename string, w http.ResponseWriter, r *http.Request) {
|
||||||
body, err := io.ReadAll(r.Body)
|
body, err := io.ReadAll(r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
|
@ -49,7 +49,7 @@ func PostFile(filename string, w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(http.StatusOK)
|
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) {
|
func GetFileList(directory string, w http.ResponseWriter) {
|
||||||
filenames, err := ListFiles(directory)
|
filenames, err := ListFiles(directory)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -64,7 +64,7 @@ func GetFileList(directory string, w http.ResponseWriter) {
|
||||||
HandleWrite(w.Write(filenamesJson))
|
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) {
|
func GetDayApi(w http.ResponseWriter, r *http.Request) {
|
||||||
dayString := chi.URLParam(r, "day")
|
dayString := chi.URLParam(r, "day")
|
||||||
if dayString == "" {
|
if dayString == "" {
|
||||||
|
@ -72,20 +72,10 @@ func GetDayApi(w http.ResponseWriter, r *http.Request) {
|
||||||
HandleWrite(w.Write([]byte("day not specified")))
|
HandleWrite(w.Write([]byte("day not specified")))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
GetFile(DataFile("day/"+dayString), w)
|
GetFileApi(DataFile("day/"+dayString), w)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTodayApi runs GetFile with today's date as filename
|
// GetNoteApi returns contents of a note specified in URL.
|
||||||
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
|
|
||||||
func GetNoteApi(w http.ResponseWriter, r *http.Request) {
|
func GetNoteApi(w http.ResponseWriter, r *http.Request) {
|
||||||
noteString := chi.URLParam(r, "note")
|
noteString := chi.URLParam(r, "note")
|
||||||
if noteString == "" {
|
if noteString == "" {
|
||||||
|
@ -93,10 +83,10 @@ func GetNoteApi(w http.ResponseWriter, r *http.Request) {
|
||||||
HandleWrite(w.Write([]byte("note not specified")))
|
HandleWrite(w.Write([]byte("note not specified")))
|
||||||
return
|
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) {
|
func PostNoteApi(w http.ResponseWriter, r *http.Request) {
|
||||||
noteString := chi.URLParam(r, "note")
|
noteString := chi.URLParam(r, "note")
|
||||||
if noteString == "" {
|
if noteString == "" {
|
||||||
|
@ -104,10 +94,10 @@ func PostNoteApi(w http.ResponseWriter, r *http.Request) {
|
||||||
HandleWrite(w.Write([]byte("note not specified")))
|
HandleWrite(w.Write([]byte("note not specified")))
|
||||||
return
|
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) {
|
func GraceActiveApi(w http.ResponseWriter, r *http.Request) {
|
||||||
value := "false"
|
value := "false"
|
||||||
if GraceActive() {
|
if GraceActive() {
|
||||||
|
@ -116,23 +106,3 @@ func GraceActiveApi(w http.ResponseWriter, r *http.Request) {
|
||||||
HandleWrite(w.Write([]byte(value)))
|
HandleWrite(w.Write([]byte(value)))
|
||||||
w.WriteHeader(http.StatusOK)
|
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)
|
|
||||||
}
|
|
||||||
|
|
16
auth.go
16
auth.go
|
@ -19,10 +19,10 @@ type failedLogin struct {
|
||||||
|
|
||||||
var failedLogins []failedLogin
|
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) {
|
func NoteLoginFail(username string, password string, r *http.Request) {
|
||||||
slog.Warn("failed auth", "username", username, "password", password, "address", r.RemoteAddr)
|
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()}
|
attempt := failedLogin{username, password, time.Now()}
|
||||||
updatedLogins := []failedLogin{attempt}
|
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.
|
// 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
|
// 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)
|
// 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 {
|
func BasicAuth(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
username, password, ok := r.BasicAuth()
|
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() {
|
func Scram() {
|
||||||
slog.Warn("SCRAM triggered, shutting down")
|
slog.Warn("SCRAM triggered, shutting down")
|
||||||
NotifyTelegram("Hibiscus SCRAM triggered, shutting down")
|
NotifyTelegram(TranslatableText("info.telegram.scram"))
|
||||||
os.Exit(0) // TODO: should this be 0 or 1?
|
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) {
|
func NotifyTelegram(msg string) {
|
||||||
if Cfg.TelegramChat == "" || Cfg.TelegramToken == "" {
|
if Cfg.TelegramChat == "" || Cfg.TelegramToken == "" {
|
||||||
slog.Debug("ignoring telegram request due to lack of credentials")
|
slog.Debug("ignoring telegram request due to lack of credentials")
|
||||||
|
|
40
config.go
40
config.go
|
@ -6,6 +6,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -51,7 +52,7 @@ var DefaultConfig = Config{
|
||||||
TelegramTopic: "",
|
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 {
|
func (c *Config) String() string {
|
||||||
output := ""
|
output := ""
|
||||||
v := reflect.ValueOf(*c)
|
v := reflect.ValueOf(*c)
|
||||||
|
@ -68,11 +69,13 @@ func (c *Config) String() string {
|
||||||
return output
|
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 {
|
func (c *Config) Reload() error {
|
||||||
*c = DefaultConfig // Reset config
|
*c = DefaultConfig // Reset config
|
||||||
|
|
||||||
if _, err := os.Stat(ConfigFile); errors.Is(err, os.ErrNotExist) {
|
if _, err := os.Stat(ConfigFile); errors.Is(err, os.ErrNotExist) {
|
||||||
err := c.Save([]byte(c.String()))
|
err := c.Save()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -149,17 +152,40 @@ func (c *Config) Reload() error {
|
||||||
return SetLanguage(c.Language) // Load selected language
|
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) {
|
func (c *Config) Read() ([]byte, error) {
|
||||||
return ReadFile(ConfigFile)
|
return ReadFile(ConfigFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save writes to ConfigFile
|
// Save writes config's contents to the ConfigFile.
|
||||||
func (c *Config) Save(contents []byte) error {
|
func (c *Config) Save() error {
|
||||||
return SaveFile(ConfigFile, contents)
|
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 {
|
func ConfigInit() Config {
|
||||||
cfg := Config{}
|
cfg := Config{}
|
||||||
err := cfg.Reload()
|
err := cfg.Reload()
|
||||||
|
|
|
@ -2,4 +2,4 @@ username=admin
|
||||||
password=admin
|
password=admin
|
||||||
port=7101
|
port=7101
|
||||||
timezone=Local
|
timezone=Local
|
||||||
language=en
|
language=en
|
|
@ -11,7 +11,7 @@ import (
|
||||||
|
|
||||||
var ExportPath = "data/export.zip"
|
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 {
|
func Export(filename string) error {
|
||||||
file, err := os.Create(filename)
|
file, err := os.Create(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -61,7 +61,8 @@ func Export(filename string) error {
|
||||||
return file.Close()
|
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) {
|
func GetExport(w http.ResponseWriter, r *http.Request) {
|
||||||
err := Export(ExportPath)
|
err := Export(ExportPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
38
files.go
38
files.go
|
@ -11,34 +11,30 @@ import (
|
||||||
"time"
|
"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 {
|
func DataFile(filename string) string {
|
||||||
return "data/" + path.Clean(filename) + ".txt"
|
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) {
|
func ReadFile(filename string) ([]byte, error) {
|
||||||
if _, err := os.Stat(filename); errors.Is(err, os.ErrNotExist) {
|
if _, err := os.Stat(filename); errors.Is(err, os.ErrNotExist) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
fileContents, err := os.ReadFile(filename)
|
fileContents, err := os.ReadFile(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("error reading file",
|
slog.Error("error reading file", "error", err, "file", filename)
|
||||||
"error", err,
|
|
||||||
"file", filename)
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return fileContents, nil
|
return fileContents, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveFile Writes contents to a file
|
// SaveFile Writes contents to a file.
|
||||||
func SaveFile(filename string, contents []byte) error {
|
func SaveFile(filename string, contents []byte) error {
|
||||||
contents = bytes.TrimSpace(contents)
|
contents = bytes.TrimSpace(contents)
|
||||||
if len(contents) == 0 { // Delete empty files
|
if len(contents) == 0 { // Delete empty files
|
||||||
err := os.Remove(filename)
|
err := os.Remove(filename)
|
||||||
slog.Error("error deleting empty file",
|
slog.Error("error deleting empty file", "error", err, "file", filename)
|
||||||
"error", err,
|
|
||||||
"file", filename)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err := os.MkdirAll(path.Dir(filename), 0755) // Create dir in case it doesn't exist yet to avoid errors
|
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)
|
f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("error opening/making file",
|
slog.Error("error opening/creating file", "error", err, "file", filename)
|
||||||
"error", err,
|
|
||||||
"file", filename)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := f.Write(contents); err != nil {
|
if _, err := f.Write(contents); err != nil {
|
||||||
slog.Error("error writing to file",
|
slog.Error("error writing to file", "error", err, "file", filename)
|
||||||
"error", err,
|
|
||||||
"file", filename)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
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?
|
// NOTE: What if I ever want to list non-text files or those outside data directory?
|
||||||
func ListFiles(directory string) ([]string, error) {
|
func ListFiles(directory string) ([]string, error) {
|
||||||
filenames, err := filepath.Glob("data/" + path.Clean(directory) + "/*.txt")
|
filenames, err := filepath.Glob("data/" + path.Clean(directory) + "/*.txt")
|
||||||
|
@ -76,7 +68,7 @@ func ListFiles(directory string) ([]string, error) {
|
||||||
return filenames, nil
|
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 {
|
func GraceActive() bool {
|
||||||
t := time.Now().In(Cfg.Timezone)
|
t := time.Now().In(Cfg.Timezone)
|
||||||
active := (60*t.Hour() + t.Minute()) < int(Cfg.GraceTime.Minutes())
|
active := (60*t.Hour() + t.Minute()) < int(Cfg.GraceTime.Minutes())
|
||||||
|
@ -88,7 +80,7 @@ func GraceActive() bool {
|
||||||
return active
|
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 {
|
func TodayDate() string {
|
||||||
dateFormatted := time.Now().In(Cfg.Timezone).Format(time.DateOnly)
|
dateFormatted := time.Now().In(Cfg.Timezone).Format(time.DateOnly)
|
||||||
if GraceActive() {
|
if GraceActive() {
|
||||||
|
@ -97,13 +89,3 @@ func TodayDate() string {
|
||||||
slog.Debug("today", "time", time.Now().In(Cfg.Timezone).Format(time.DateTime))
|
slog.Debug("today", "time", time.Now().In(Cfg.Timezone).Format(time.DateTime))
|
||||||
return dateFormatted
|
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)
|
|
||||||
}
|
|
||||||
|
|
2
flags.go
2
flags.go
|
@ -5,7 +5,7 @@ import (
|
||||||
"log"
|
"log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FlagInit processes app flags
|
// FlagInit processes app flags.
|
||||||
func FlagInit() {
|
func FlagInit() {
|
||||||
config := flag.String("config", "", "override config file")
|
config := flag.String("config", "", "override config file")
|
||||||
username := flag.String("user", "", "override username")
|
username := flag.String("user", "", "override username")
|
||||||
|
|
7
i18n.go
7
i18n.go
|
@ -10,7 +10,7 @@ import (
|
||||||
var I18n embed.FS
|
var I18n embed.FS
|
||||||
var Translations = map[string]string{}
|
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 {
|
func SetLanguage(language string) error {
|
||||||
loadLanguage := func(language string) error {
|
loadLanguage := func(language string) error {
|
||||||
filename := "i18n/" + language + ".json"
|
filename := "i18n/" + language + ".json"
|
||||||
|
@ -23,14 +23,15 @@ func SetLanguage(language string) error {
|
||||||
}
|
}
|
||||||
return json.Unmarshal(fileContents, &Translations)
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return loadLanguage(language)
|
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 {
|
func TranslatableText(id string) string {
|
||||||
if v, ok := Translations[id]; !ok {
|
if v, ok := Translations[id]; !ok {
|
||||||
return id
|
return id
|
||||||
|
|
|
@ -24,5 +24,6 @@
|
||||||
"info.readme": "Edit readme.txt",
|
"info.readme": "Edit readme.txt",
|
||||||
"info.config": "Edit config",
|
"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"
|
||||||
}
|
}
|
|
@ -24,5 +24,6 @@
|
||||||
"info.readme": "Редактировать readme.txt",
|
"info.readme": "Редактировать readme.txt",
|
||||||
"info.config": "Редактировать конфиг",
|
"info.config": "Редактировать конфиг",
|
||||||
|
|
||||||
"info.telegram_notification": "Неверная попытка авторизации в Hibiscus.txt"
|
"info.telegram_notification": "Неверная попытка авторизации в Hibiscus.txt",
|
||||||
|
"info.telegram.scram": "Активирована функция SCRAM в Hibiscus.txt, сервер выключается"
|
||||||
}
|
}
|
12
info.go
12
info.go
|
@ -13,13 +13,13 @@ type AppInfo struct {
|
||||||
SourceLink string
|
SourceLink string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info contains app information
|
// Info contains app information.
|
||||||
var Info = AppInfo{
|
var Info = AppInfo{
|
||||||
Version: "1.1.2",
|
Version: "1.1.3",
|
||||||
SourceLink: "https://git.a71.su/Andrew71/hibiscus",
|
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) {
|
func GetInfo(w http.ResponseWriter, r *http.Request) {
|
||||||
err := infoTemplate.ExecuteTemplate(w, "base", Info)
|
err := infoTemplate.ExecuteTemplate(w, "base", Info)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -28,3 +28,9 @@ func GetInfo(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetVersionApi returns current app version.
|
||||||
|
func GetVersionApi(w http.ResponseWriter, r *http.Request) {
|
||||||
|
HandleWrite(w.Write([]byte(Info.Version)))
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import (
|
||||||
|
|
||||||
var DebugMode = false
|
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() {
|
func LogInit() {
|
||||||
var w io.Writer
|
var w io.Writer
|
||||||
if Cfg.LogToFile {
|
if Cfg.LogToFile {
|
||||||
|
|
112
routes.go
112
routes.go
|
@ -27,17 +27,17 @@ type Entry struct {
|
||||||
|
|
||||||
type formatEntries func([]string) []Entry
|
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
|
//go:embed public
|
||||||
var Public embed.FS
|
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
|
//go:embed pages
|
||||||
var Pages embed.FS
|
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 {
|
func EmbeddedPage(name string) []byte {
|
||||||
data, err := Pages.ReadFile(name)
|
data, err := Pages.ReadFile(name)
|
||||||
if err != nil {
|
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 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 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) {
|
func NotFound(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(404)
|
w.WriteHeader(404)
|
||||||
HandleWrite(w.Write(EmbeddedPage("pages/error/404.html")))
|
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) {
|
func InternalError(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(500)
|
w.WriteHeader(500)
|
||||||
HandleWrite(w.Write(EmbeddedPage("pages/error/500.html")))
|
HandleWrite(w.Write(EmbeddedPage("pages/error/500.html")))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetToday renders HTML page for today's entry
|
// GetEntries handles showing a list.
|
||||||
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
|
|
||||||
func GetEntries(w http.ResponseWriter, r *http.Request, title string, description template.HTML, dir string, format formatEntries) {
|
func GetEntries(w http.ResponseWriter, r *http.Request, title string, description template.HTML, dir string, format formatEntries) {
|
||||||
filesList, err := ListFiles(dir)
|
filesList, err := ListFiles(dir)
|
||||||
if err != nil {
|
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) {
|
func GetDays(w http.ResponseWriter, r *http.Request) {
|
||||||
description := template.HTML(
|
description := template.HTML(
|
||||||
"<a href=\"#footer\">" + TranslatableText("prompt.days") + "</a>")
|
"<a href=\"#footer\">" + template.HTMLEscapeString(TranslatableText("prompt.days")) + "</a>")
|
||||||
GetEntries(w, r, TranslatableText("title.days"), description, "day", func(files []string) []Entry {
|
GetEntries(w, r, TranslatableText("title.days"), description, "day", func(files []string) []Entry {
|
||||||
var filesFormatted []Entry
|
var filesFormatted []Entry
|
||||||
for i := range files {
|
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) {
|
func GetNotes(w http.ResponseWriter, r *http.Request) {
|
||||||
// This is suboptimal, but will do...
|
// This is suboptimal, but will do...
|
||||||
description := template.HTML(
|
description := template.HTML(
|
||||||
"<a href=\"#\" onclick='newNote(\"" + TranslatableText("prompt.notes") + "\")'>" + TranslatableText("button.notes") + "</a>" +
|
"<a href=\"#\" onclick='newNote(\"" + template.HTMLEscapeString(TranslatableText("prompt.notes")) + "\")'>" + template.HTMLEscapeString(TranslatableText("button.notes")) + "</a>" +
|
||||||
" <noscript>(" + template.HTMLEscapeString(TranslatableText("noscript.notes")) + ")</noscript>")
|
" <noscript>(" + template.HTMLEscapeString(TranslatableText("noscript.notes")) + ")</noscript>")
|
||||||
GetEntries(w, r, TranslatableText("title.notes"), description, "notes", func(files []string) []Entry {
|
GetEntries(w, r, TranslatableText("title.notes"), description, "notes", func(files []string) []Entry {
|
||||||
var filesFormatted []Entry
|
var filesFormatted []Entry
|
||||||
for _, v := range files {
|
for _, v := range files {
|
||||||
titleString := strings.Replace(v, "-", " ", -1) // FIXME: what if I need a hyphen?
|
// titleString := strings.Replace(v, "-", " ", -1) // This would be cool, but what if I need a hyphen?
|
||||||
filesFormatted = append(filesFormatted, Entry{Title: titleString, Link: "notes/" + v})
|
filesFormatted = append(filesFormatted, Entry{Title: v, Link: "notes/" + v})
|
||||||
}
|
}
|
||||||
return filesFormatted
|
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) {
|
func GetEntry(w http.ResponseWriter, r *http.Request, title string, filename string, editable bool) {
|
||||||
entry, err := ReadFile(filename)
|
entry, err := ReadFile(filename)
|
||||||
if err != nil {
|
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) {
|
func GetDay(w http.ResponseWriter, r *http.Request) {
|
||||||
dayString := chi.URLParam(r, "day")
|
dayString := chi.URLParam(r, "day")
|
||||||
if dayString == "" {
|
if dayString == "" {
|
||||||
|
@ -207,7 +189,7 @@ func GetDay(w http.ResponseWriter, r *http.Request) {
|
||||||
GetEntry(w, r, title, DataFile("day/"+dayString), false)
|
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) {
|
func GetNote(w http.ResponseWriter, r *http.Request) {
|
||||||
noteString := chi.URLParam(r, "note")
|
noteString := chi.URLParam(r, "note")
|
||||||
if noteString == "" {
|
if noteString == "" {
|
||||||
|
@ -223,7 +205,7 @@ func GetNote(w http.ResponseWriter, r *http.Request) {
|
||||||
GetEntry(w, r, noteString, DataFile("notes/"+noteString), true)
|
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) {
|
func PostNote(w http.ResponseWriter, r *http.Request) {
|
||||||
noteString := chi.URLParam(r, "note")
|
noteString := chi.URLParam(r, "note")
|
||||||
if noteString == "" {
|
if noteString == "" {
|
||||||
|
@ -231,41 +213,5 @@ func PostNote(w http.ResponseWriter, r *http.Request) {
|
||||||
HandleWrite(w.Write([]byte("note not specified")))
|
HandleWrite(w.Write([]byte("note not specified")))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := SaveFile(DataFile("notes/"+noteString), []byte(r.FormValue("text")))
|
PostEntry(DataFile("notes/"+noteString), w, r)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
22
serve.go
22
serve.go
|
@ -9,7 +9,7 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Serve starts the app's web server
|
// Serve starts the app's web server.
|
||||||
func Serve() {
|
func Serve() {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
r.Use(middleware.RealIP)
|
r.Use(middleware.RealIP)
|
||||||
|
@ -19,32 +19,34 @@ func Serve() {
|
||||||
// Routes ==========
|
// Routes ==========
|
||||||
userRouter := chi.NewRouter()
|
userRouter := chi.NewRouter()
|
||||||
userRouter.Use(BasicAuth)
|
userRouter.Use(BasicAuth)
|
||||||
userRouter.Get("/", GetToday)
|
userRouter.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
userRouter.Post("/", PostToday)
|
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", GetDays)
|
||||||
userRouter.Get("/day/{day}", GetDay)
|
userRouter.Get("/day/{day}", GetDay)
|
||||||
userRouter.Get("/notes", GetNotes)
|
userRouter.Get("/notes", GetNotes)
|
||||||
userRouter.Get("/notes/{note}", GetNote)
|
userRouter.Get("/notes/{note}", GetNote)
|
||||||
userRouter.Post("/notes/{note}", PostNote)
|
userRouter.Post("/notes/{note}", PostNote)
|
||||||
userRouter.Get("/info", GetInfo)
|
userRouter.Get("/info", GetInfo)
|
||||||
userRouter.Get("/readme", GetReadme)
|
userRouter.Get("/readme", func(w http.ResponseWriter, r *http.Request) { GetEntry(w, r, "readme.txt", DataFile("readme"), true) })
|
||||||
userRouter.Post("/readme", PostReadme)
|
userRouter.Post("/readme", func(w http.ResponseWriter, r *http.Request) { PostEntry(DataFile("readme"), w, r) })
|
||||||
userRouter.Get("/config", GetConfig)
|
userRouter.Get("/config", func(w http.ResponseWriter, r *http.Request) { GetEntry(w, r, "config.txt", ConfigFile, true) })
|
||||||
userRouter.Post("/config", PostConfig)
|
userRouter.Post("/config", PostConfig)
|
||||||
r.Mount("/", userRouter)
|
r.Mount("/", userRouter)
|
||||||
|
|
||||||
// API =============
|
// API =============
|
||||||
apiRouter := chi.NewRouter()
|
apiRouter := chi.NewRouter()
|
||||||
apiRouter.Use(BasicAuth)
|
apiRouter.Use(BasicAuth)
|
||||||
apiRouter.Get("/readme", func(w http.ResponseWriter, r *http.Request) { GetFile("readme", w) })
|
apiRouter.Get("/readme", func(w http.ResponseWriter, r *http.Request) { GetFileApi("readme", w) })
|
||||||
apiRouter.Post("/readme", func(w http.ResponseWriter, r *http.Request) { PostFile("readme", w, r) })
|
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", func(w http.ResponseWriter, r *http.Request) { GetFileList("day", w) })
|
||||||
apiRouter.Get("/day/{day}", GetDayApi)
|
apiRouter.Get("/day/{day}", GetDayApi)
|
||||||
apiRouter.Get("/notes", func(w http.ResponseWriter, r *http.Request) { GetFileList("notes", w) })
|
apiRouter.Get("/notes", func(w http.ResponseWriter, r *http.Request) { GetFileList("notes", w) })
|
||||||
apiRouter.Get("/notes/{note}", GetNoteApi)
|
apiRouter.Get("/notes/{note}", GetNoteApi)
|
||||||
apiRouter.Post("/notes/{note}", PostNoteApi)
|
apiRouter.Post("/notes/{note}", PostNoteApi)
|
||||||
apiRouter.Get("/today", GetTodayApi)
|
apiRouter.Get("/today", func(w http.ResponseWriter, r *http.Request) { GetFileApi(DataFile("day/"+TodayDate()), w) })
|
||||||
apiRouter.Post("/today", PostTodayApi)
|
apiRouter.Post("/today", func(w http.ResponseWriter, r *http.Request) { PostEntry(DataFile("day/"+TodayDate()), w, r) })
|
||||||
apiRouter.Get("/export", GetExport)
|
apiRouter.Get("/export", GetExport)
|
||||||
apiRouter.Get("/grace", GraceActiveApi)
|
apiRouter.Get("/grace", GraceActiveApi)
|
||||||
apiRouter.Get("/version", GetVersionApi)
|
apiRouter.Get("/version", GetVersionApi)
|
||||||
|
|
Loading…
Reference in a new issue