Add config editing to UI
This commit is contained in:
parent
20ca509a2b
commit
fa2ecacce5
15 changed files with 74 additions and 55 deletions
|
@ -1,6 +1,13 @@
|
|||
# Changelog
|
||||
This file keeps track of changes in more human-readable fashion
|
||||
|
||||
## v0.6.0
|
||||
* Replaced config reload with edit in info (api method still available, config reloads on save)
|
||||
* Bug fixes
|
||||
* Filenames are now sanitized when writing files
|
||||
* "Tomorrow" in days list is now also displayed if Timezone is changed and grace period is off
|
||||
* Frontend date display now uses configured timezone
|
||||
|
||||
## v0.5.0
|
||||
* Added a JS prompt to create new note
|
||||
* "Sanitization" for this method is basic and assumes a well-meaning user
|
||||
|
|
|
@ -52,6 +52,7 @@ timezone=Local # IANA Time zone database identifier ("UTC", Local", "Europe/Mos
|
|||
grace_period=0s # Time after a new day begins, but before the switch to next day's file. e.g. 2h30m - files will change at 2:30am
|
||||
language=en # ISO-639 language code (pre-installed - en, ru)
|
||||
theme=default # Picked theme (pre-installed - default, gruvbox, high-contrast)
|
||||
title=🌺 Hibiscus.txt # The text in the header
|
||||
log_to_file=false # Whether to write logs to a file
|
||||
log_file=config/log.txt # Where to store the log file if it is enabled
|
||||
enable_scram=false # Whether the app should shut down if there are 3 or more failed login attempts within 100 seconds
|
||||
|
|
11
TODO.md
11
TODO.md
|
@ -3,22 +3,13 @@ List of things to add to this project
|
|||
|
||||
## v1.0.0
|
||||
* a logo so I can enable PWA (and look cool)
|
||||
* Themes
|
||||
* Theme in config [X]
|
||||
* Themes
|
||||
* Default [X]
|
||||
* Gruvbox [X]
|
||||
* High contrast [ ]
|
||||
* Seasons?
|
||||
* Check for bugs
|
||||
* Versioned containers via `ghcr.io` or `dockerhub`,
|
||||
with automatic CI/CD build on release
|
||||
* Test test test !!!!
|
||||
* ...QA? And polishing.
|
||||
|
||||
## Brainstorming
|
||||
Don't expect any of this, these are ideas floating inside my head
|
||||
* Further info page functionality
|
||||
* Edit config
|
||||
* Statistics e.g. mb taken, number of day pages/notes
|
||||
* Test telegram link
|
||||
* Multi-user support through several username-pass keys
|
||||
|
|
10
api.go
10
api.go
|
@ -72,17 +72,17 @@ func GetDayApi(w http.ResponseWriter, r *http.Request) {
|
|||
HandleWrite(w.Write([]byte("day not specified")))
|
||||
return
|
||||
}
|
||||
GetFile("day/"+dayString, w)
|
||||
GetFile(DataFile("day/"+dayString), w)
|
||||
}
|
||||
|
||||
// GetTodayApi runs GetFile with today's date as filename
|
||||
func GetTodayApi(w http.ResponseWriter, _ *http.Request) {
|
||||
GetFile("day/"+TodayDate(), w)
|
||||
GetFile(DataFile("day/"+TodayDate()), w)
|
||||
}
|
||||
|
||||
// PostTodayApi runs PostFile with today's date as filename
|
||||
func PostTodayApi(w http.ResponseWriter, r *http.Request) {
|
||||
PostFile("day/"+TodayDate(), w, r)
|
||||
PostFile(DataFile("day/"+TodayDate()), w, r)
|
||||
}
|
||||
|
||||
// GetNoteApi returns contents of a note specified in URL
|
||||
|
@ -93,7 +93,7 @@ func GetNoteApi(w http.ResponseWriter, r *http.Request) {
|
|||
HandleWrite(w.Write([]byte("note not specified")))
|
||||
return
|
||||
}
|
||||
GetFile("notes/"+noteString, w)
|
||||
GetFile(DataFile("notes/"+noteString), w)
|
||||
}
|
||||
|
||||
// PostNoteApi writes request's body contents to a note specified in URL
|
||||
|
@ -104,7 +104,7 @@ func PostNoteApi(w http.ResponseWriter, r *http.Request) {
|
|||
HandleWrite(w.Write([]byte("note not specified")))
|
||||
return
|
||||
}
|
||||
PostFile("notes/"+noteString, w, r)
|
||||
PostFile(DataFile("notes/"+noteString), w, r)
|
||||
}
|
||||
|
||||
// GraceActiveApi returns "true" if grace period is active, and "false" otherwise
|
||||
|
|
27
config.go
27
config.go
|
@ -49,10 +49,9 @@ var DefaultConfig = Config{
|
|||
TelegramChat: "",
|
||||
}
|
||||
|
||||
// Save puts modified and mandatory config options into the config.txt file
|
||||
func (c *Config) Save() error {
|
||||
// String returns text version of modified and mandatory config options
|
||||
func (c *Config) String() string {
|
||||
output := ""
|
||||
|
||||
v := reflect.ValueOf(*c)
|
||||
vDefault := reflect.ValueOf(DefaultConfig)
|
||||
typeOfS := v.Type()
|
||||
|
@ -64,22 +63,14 @@ func (c *Config) Save() error {
|
|||
output += fmt.Sprintf("%s=%v\n", key, value)
|
||||
}
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(ConfigFile, os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := f.Write([]byte(output)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return output
|
||||
}
|
||||
|
||||
func (c *Config) Reload() error {
|
||||
*c = DefaultConfig // Reset config
|
||||
|
||||
if _, err := os.Stat(ConfigFile); errors.Is(err, os.ErrNotExist) {
|
||||
err := c.Save()
|
||||
err := c.Save([]byte(c.String()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -156,6 +147,16 @@ func (c *Config) Reload() error {
|
|||
return LoadLanguage(c.Language) // Load selected language
|
||||
}
|
||||
|
||||
// Read gets raw contents from ConfigFile
|
||||
func (c *Config) Read() ([]byte, error) {
|
||||
return ReadFile(ConfigFile)
|
||||
}
|
||||
|
||||
// Save writes to ConfigFile
|
||||
func (c *Config) Save(contents []byte) error {
|
||||
return SaveFile(ConfigFile, contents)
|
||||
}
|
||||
|
||||
// ConfigInit loads config on startup
|
||||
func ConfigInit() Config {
|
||||
cfg := Config{}
|
||||
|
|
|
@ -2,9 +2,4 @@ username=admin
|
|||
password=admin
|
||||
port=7101
|
||||
timezone=Local
|
||||
grace_period=0s
|
||||
language=en
|
||||
theme=default
|
||||
log_to_file=false
|
||||
log_file=config/log.txt
|
||||
enable_scram=false
|
||||
|
|
20
files.go
20
files.go
|
@ -11,14 +11,16 @@ import (
|
|||
"time"
|
||||
)
|
||||
|
||||
// DataFile modifies file path to ensure it's a .txt inside the data folder
|
||||
func DataFile(filename string) string {
|
||||
return "data/" + path.Clean(filename) + ".txt"
|
||||
}
|
||||
|
||||
// ReadFile returns raw contents of a file
|
||||
func ReadFile(filename string) ([]byte, error) {
|
||||
filename = "data/" + path.Clean(filename) + ".txt" // Does this sanitize the path?
|
||||
|
||||
if _, err := os.Stat(filename); errors.Is(err, os.ErrNotExist) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fileContents, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
slog.Error("error reading file",
|
||||
|
@ -26,14 +28,12 @@ func ReadFile(filename string) ([]byte, error) {
|
|||
"file", filename)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return fileContents, nil
|
||||
}
|
||||
|
||||
// SaveFile Writes request's contents to a file
|
||||
// SaveFile Writes contents to a file
|
||||
func SaveFile(filename string, contents []byte) error {
|
||||
contents = bytes.TrimSpace(contents)
|
||||
filename = "data/" + filename + ".txt"
|
||||
if len(contents) == 0 { // Delete empty files
|
||||
err := os.Remove(filename)
|
||||
slog.Error("error deleting empty file",
|
||||
|
@ -63,8 +63,9 @@ func SaveFile(filename string, contents []byte) error {
|
|||
}
|
||||
|
||||
// ListFiles returns slice of filenames in a directory without extensions or path
|
||||
// NOTE: What if I ever want to list non-text files or those outside data directory?
|
||||
func ListFiles(directory string) ([]string, error) {
|
||||
filenames, err := filepath.Glob("data/" + directory + "/*.txt")
|
||||
filenames, err := filepath.Glob("data/" + path.Clean(directory) + "/*.txt")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -93,15 +94,16 @@ func TodayDate() string {
|
|||
if GraceActive() {
|
||||
dateFormatted = time.Now().In(Cfg.Timezone).AddDate(0, 0, -1).Format(time.DateOnly)
|
||||
}
|
||||
slog.Debug("today", "time", time.Now().In(Cfg.Timezone).Format(time.DateTime))
|
||||
return dateFormatted
|
||||
}
|
||||
|
||||
// ReadToday runs ReadFile with today's date as filename
|
||||
func ReadToday() ([]byte, error) {
|
||||
return ReadFile("day/" + TodayDate())
|
||||
return ReadFile(DataFile("day/" + TodayDate()))
|
||||
}
|
||||
|
||||
// SaveToday runs SaveFile with today's date as filename
|
||||
func SaveToday(contents []byte) error {
|
||||
return SaveFile("day/"+TodayDate(), contents)
|
||||
return SaveFile(DataFile("day/"+TodayDate()), contents)
|
||||
}
|
||||
|
|
|
@ -23,5 +23,5 @@
|
|||
"info.version.link": "source and changelog",
|
||||
"info.export": "Export data",
|
||||
"info.readme": "Edit readme.txt",
|
||||
"info.reload": "Reload config"
|
||||
"info.config": "Edit config"
|
||||
}
|
|
@ -23,5 +23,5 @@
|
|||
"info.version.link": "исходный код",
|
||||
"info.export": "Экспорт данных",
|
||||
"info.readme": "Редактировать readme.txt",
|
||||
"info.reload": "Перезагрузить конфиг"
|
||||
"info.config": "Редактировать конфиг"
|
||||
}
|
2
info.go
2
info.go
|
@ -15,7 +15,7 @@ type AppInfo struct {
|
|||
|
||||
// Info contains app information
|
||||
var Info = AppInfo{
|
||||
Version: "0.5.0",
|
||||
Version: "0.6.0",
|
||||
SourceLink: "https://git.a71.su/Andrew71/hibiscus",
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,8 @@
|
|||
</main>
|
||||
{{template "footer" .}}
|
||||
<script defer>
|
||||
const langName="{{ translatableText "lang" }}";
|
||||
const langName="{{ config.Language }}";
|
||||
const timeZone="{{ config.Timezone }}";
|
||||
beginTimeUpdater()
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
<h2>{{ translatableText "title.info" }}</h2>
|
||||
<ul>
|
||||
<li>{{ translatableText "info.version" }} - {{ info.Version }} (<a href="{{ .SourceLink }}">{{ translatableText "info.version.link" }}</a>)</li>
|
||||
<li><a href="/api/export" download="hibiscus">{{ translatableText "info.export" }}</a></li>
|
||||
<li><a href="/config">{{ translatableText "info.config" }}</a></li>
|
||||
<li><a href="/readme">{{ translatableText "info.readme" }}</a></li>
|
||||
<li><a href="/api/reload">{{ translatableText "info.reload" }}</a></li>
|
||||
<li><a href="/api/export" download="hibiscus">{{ translatableText "info.export" }}</a></li>
|
||||
</ul>
|
||||
{{end}}
|
|
@ -3,7 +3,8 @@ function formatDate(date) {
|
|||
let dateFormat = new Intl.DateTimeFormat([langName, "en"], {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
day: 'numeric',
|
||||
timeZone: timeZone
|
||||
})
|
||||
return dateFormat.format(date)
|
||||
}
|
||||
|
|
30
routes.go
30
routes.go
|
@ -113,7 +113,7 @@ func GetDays(w http.ResponseWriter, r *http.Request) {
|
|||
if v == TodayDate() {
|
||||
dayString = TranslatableText("link.today")
|
||||
dayString = strings.ToTitle(string([]rune(dayString)[0])) + string([]rune(dayString)[1:])
|
||||
} else if GraceActive() && v > TodayDate() {
|
||||
} else if v > TodayDate() {
|
||||
dayString = TranslatableText("link.tomorrow")
|
||||
dayString = strings.ToTitle(string([]rune(dayString)[0])) + string([]rune(dayString)[1:])
|
||||
}
|
||||
|
@ -189,7 +189,7 @@ func GetDay(w http.ResponseWriter, r *http.Request) {
|
|||
title = t.Format("02 Jan 2006")
|
||||
}
|
||||
|
||||
GetEntry(w, r, title, "day/"+dayString, false)
|
||||
GetEntry(w, r, title, DataFile("day/"+dayString), false)
|
||||
}
|
||||
|
||||
// GetNote renders HTML page for a note
|
||||
|
@ -205,7 +205,7 @@ func GetNote(w http.ResponseWriter, r *http.Request) {
|
|||
noteString = decodedNote
|
||||
}
|
||||
|
||||
GetEntry(w, r, noteString, "notes/"+noteString, true)
|
||||
GetEntry(w, r, noteString, DataFile("notes/"+noteString), true)
|
||||
}
|
||||
|
||||
// PostNote saves a note form and redirects back to GET
|
||||
|
@ -216,7 +216,7 @@ func PostNote(w http.ResponseWriter, r *http.Request) {
|
|||
HandleWrite(w.Write([]byte("note not specified")))
|
||||
return
|
||||
}
|
||||
err := SaveFile("notes/"+noteString, []byte(r.FormValue("text")))
|
||||
err := SaveFile(DataFile("notes/"+noteString), []byte(r.FormValue("text")))
|
||||
if err != nil {
|
||||
slog.Error("error saving a note", "note", noteString, "error", err)
|
||||
}
|
||||
|
@ -225,14 +225,32 @@ func PostNote(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
// GetReadme calls GetEntry for readme.txt
|
||||
func GetReadme(w http.ResponseWriter, r *http.Request) {
|
||||
GetEntry(w, r, "readme.txt", "readme", true)
|
||||
GetEntry(w, r, "readme.txt", DataFile("readme"), true)
|
||||
}
|
||||
|
||||
// PostReadme saves contents of readme.txt file
|
||||
func PostReadme(w http.ResponseWriter, r *http.Request) {
|
||||
err := SaveFile("readme", []byte(r.FormValue("text")))
|
||||
err := SaveFile(DataFile("readme"), []byte(r.FormValue("text")))
|
||||
if err != nil {
|
||||
slog.Error("error saving readme", "error", err)
|
||||
}
|
||||
http.Redirect(w, r, r.Header.Get("Referer"), 302)
|
||||
}
|
||||
|
||||
// GetConfig calls GetEntry for Cfg
|
||||
func GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
GetEntry(w, r, "config.txt", ConfigFile, true)
|
||||
}
|
||||
|
||||
// PostConfig saves new Cfg
|
||||
func PostConfig(w http.ResponseWriter, r *http.Request) {
|
||||
err := SaveFile(ConfigFile, []byte(r.FormValue("text")))
|
||||
if err != nil {
|
||||
slog.Error("error saving config", "error", err)
|
||||
}
|
||||
err = Cfg.Reload()
|
||||
if err != nil {
|
||||
slog.Error("error reloading config", "error", err)
|
||||
}
|
||||
http.Redirect(w, r, r.Header.Get("Referer"), 302)
|
||||
}
|
||||
|
|
2
serve.go
2
serve.go
|
@ -28,6 +28,8 @@ func Serve() {
|
|||
userRouter.Get("/info", GetInfo)
|
||||
userRouter.Get("/readme", GetReadme)
|
||||
userRouter.Post("/readme", PostReadme)
|
||||
userRouter.Get("/config", GetConfig)
|
||||
userRouter.Post("/config", PostConfig)
|
||||
r.Mount("/", userRouter)
|
||||
|
||||
// API =============
|
||||
|
|
Loading…
Reference in a new issue