From 37d184b3e228e0b43aa54fb45275131f46540229 Mon Sep 17 00:00:00 2001 From: Derek Stevens Date: Tue, 7 Jun 2022 23:44:54 -0600 Subject: [PATCH] fix style, implement config load/save --- archetype/adapter.go | 9 ++- archetype/eureka.go | 149 +++++++++++++++++++++++++++++++++++--- archetype/page.go | 1 - lfo/middleware.go | 29 ++++++++ nirvash.go | 97 +++++++++++++++++-------- static/style.css | 57 ++++++++++++--- templates/config.html | 25 +++++++ templates/config_set.html | 14 ++++ templates/login.html | 6 +- 9 files changed, 333 insertions(+), 54 deletions(-) create mode 100644 templates/config.html create mode 100644 templates/config_set.html diff --git a/archetype/adapter.go b/archetype/adapter.go index e2b493e..467d1c0 100644 --- a/archetype/adapter.go +++ b/archetype/adapter.go @@ -5,13 +5,18 @@ type BuildStatus struct { Message string } +type ConfigOption struct { + Name string + Type string +} + type Adapter interface { Init(cfg *Config) Name() string EditableSlugs() bool BuildOptions() []string - GetConfig(key string) (interface{}, error) - SetConfig(key string, value interface{}) error + GetConfig() map[ConfigOption]string + SetConfig(map[ConfigOption]string) error ListPages() map[string]string GetPage(string) (Page, error) FormatPage(string) string diff --git a/archetype/eureka.go b/archetype/eureka.go index 47f5255..d0d481b 100644 --- a/archetype/eureka.go +++ b/archetype/eureka.go @@ -6,12 +6,13 @@ import ( "os" "os/exec" "path/filepath" + "strconv" "strings" ) type EurekaAdapter struct { Root string - Config map[string]interface{} + Config map[ConfigOption]string } func (self *EurekaAdapter) Init(cfg *Config) { @@ -23,8 +24,12 @@ func (self *EurekaAdapter) Init(cfg *Config) { } self.Root = cfg.Root - + self.Config = make(map[ConfigOption]string) // TODO: read config.h and build self.Config + err = self.readCfg() + if err != nil { + panic("config.h is malformed!") + } } func (self *EurekaAdapter) Name() string { @@ -39,12 +44,13 @@ func (self *EurekaAdapter) BuildOptions() []string { return []string{"twtxt"} } -func (self *EurekaAdapter) GetConfig(key string) (interface{}, error) { - return nil, nil +func (self *EurekaAdapter) GetConfig() map[ConfigOption]string { + return self.Config } -func (self *EurekaAdapter) SetConfig(key string, value interface{}) error { - return nil +func (self *EurekaAdapter) SetConfig(cfg map[ConfigOption]string) error { + self.Config = cfg + return self.writeCfg() } func (self *EurekaAdapter) ListPages() map[string]string { @@ -85,7 +91,6 @@ func (self *EurekaAdapter) GetPage(filename string) (Page, error) { return Page{ Title: title, - Slug: filename, Content: content, Edited: fileInfo.ModTime(), }, nil @@ -129,7 +134,15 @@ func (self *EurekaAdapter) SavePage(oldSlug, newSlug, title, content string) err defer f.Close() if oldSlug != newSlug { - // TODO: delete old html as well + siteRoot := self.Config[ConfigOption{ + Name: "SITEROOT", + Type: "string", + }] + htmlFile := filepath.Join(self.Root, siteRoot, oldSlug+"l") + _, err := os.Stat(htmlFile) + if !os.IsNotExist(err) { + os.Remove(htmlFile) + } os.Remove(filepath.Join(self.Root, "inc", oldSlug)) } @@ -138,7 +151,15 @@ func (self *EurekaAdapter) SavePage(oldSlug, newSlug, title, content string) err } func (self *EurekaAdapter) DeletePage(slug string) error { - // TODO: delete old html as well + siteRoot := self.Config[ConfigOption{ + Name: "SITEROOT", + Type: "string", + }] + htmlFile := filepath.Join(self.Root, siteRoot, slug+"l") + _, err := os.Stat(htmlFile) + if !os.IsNotExist(err) { + os.Remove(htmlFile) + } return os.Remove(filepath.Join(self.Root, "inc", slug)) } @@ -147,7 +168,7 @@ func (self *EurekaAdapter) Build(buildOptions map[string][]string) BuildStatus { twtxt := buildOptions["twtxt"][0] cmdArgs := "" if twtxt != "" { - cmdArgs += " -t " + twtxt + cmdArgs += "-t " + twtxt } cmd := exec.Command("./build.sh", cmdArgs) @@ -159,3 +180,111 @@ func (self *EurekaAdapter) Build(buildOptions map[string][]string) BuildStatus { Message: string(out), } } + +func (self *EurekaAdapter) readCfg() error { + configPath := filepath.Join(self.Root, "config.h") + _, err := os.Stat(filepath.Join(self.Root, "config.h")) + if os.IsNotExist(err) { + configPath = filepath.Join(self.Root, "config.def.h") + } + f, err := os.ReadFile(configPath) + + if err != nil { + return err + } + + fileData := string(f[:]) + + macros := strings.Split(fileData, "#define ")[1:] + for _, macro := range macros { + tokens := strings.Split(strings.TrimSpace(macro), " ") + k := tokens[0] + v := strings.TrimSpace(strings.Join(tokens[1:], " ")) + + if strings.Contains(v, "\"") { + if strings.Contains(v, "\\\r\n") || strings.Contains(v, "\\\n") { + // process multiline string + lines := strings.Split(v, "\n") + cleanedString := "" + for _, l := range lines { + l = strings.TrimSuffix(l, "\r") + l = strings.TrimSuffix(l, "\\") + l = strings.TrimSpace(l) + l = strings.TrimPrefix(l, "\"") + l = strings.TrimSuffix(l, "\"") + l = strings.ReplaceAll(l, "\\\"", "\"") + l = strings.ReplaceAll(l, "\\n", "\n") + cleanedString += l + } + self.Config[ConfigOption{ + Name: k, + Type: "multilinestring", + }] = cleanedString + } else { + cleanedString := strings.TrimSuffix(strings.TrimPrefix(v, "\""), "\"") + cleanedString = strings.ReplaceAll(cleanedString, "\\n", "\n") + cleanedString = strings.ReplaceAll(cleanedString, "\r", "") + cleanedString = strings.ReplaceAll(cleanedString, "\\\"", "\"") + self.Config[ConfigOption{ + Name: k, + Type: "string", + }] = cleanedString + } + } else if strings.Contains(v, ".") { + _, err := strconv.ParseFloat(v, 64) + if err != nil { + return err + } + self.Config[ConfigOption{ + Name: k, + Type: "float", + }] = v + } else { + _, err := strconv.ParseInt(v, 10, 64) + if err != nil { + return err + } + self.Config[ConfigOption{ + Name: k, + Type: "int", + }] = v + } + } + return nil +} + +func (self *EurekaAdapter) writeCfg() error { + f, err := os.Create(filepath.Join(self.Root, "config.h")) + if err != nil { + return err + } + + defer f.Close() + + for k, v := range self.Config { + switch k.Type { + case "int": + _, err := strconv.ParseInt(v, 10, 64) + if err != nil { + return err + } + f.WriteString("#define " + k.Name + " " + v + "\n") + case "float": + _, err := strconv.ParseFloat(v, 64) + if err != nil { + return err + } + f.WriteString("#define " + k.Name + " " + v + "\n") + case "string": + fallthrough + case "multilinestring": + v = strings.ReplaceAll(v, "\"", "\\\"") + v = strings.ReplaceAll(v, "\n", "\\n\" \\\n\"") + v = strings.ReplaceAll(v, "\r", "") + f.WriteString("#define " + k.Name + " \"" + v + "\"\n") + default: + return errors.New("Unsupported config value type: " + k.Type) + } + } + return nil +} diff --git a/archetype/page.go b/archetype/page.go index fa5695c..6054646 100644 --- a/archetype/page.go +++ b/archetype/page.go @@ -6,7 +6,6 @@ import ( type Page struct { Title string - Slug string Content string Edited time.Time } diff --git a/lfo/middleware.go b/lfo/middleware.go index 9ae2192..0958d4d 100644 --- a/lfo/middleware.go +++ b/lfo/middleware.go @@ -36,3 +36,32 @@ func EnsurePageData(next http.Handler, adapter core.Adapter) http.Handler { return http.HandlerFunc(handlerFunc) } + +func SanitizeFormMap(next http.Handler) http.Handler { + handlerFunc := func(w http.ResponseWriter, req *http.Request) { + delete(req.PostForm, "csrfToken") + next.ServeHTTP(w, req) + } + + return http.HandlerFunc(handlerFunc) +} + +func FormMapToAdapterConfig(next http.Handler, adapter core.Adapter) http.Handler { + handlerFunc := func(w http.ResponseWriter, req *http.Request) { + cfg := make(map[core.ConfigOption]string) + for k, arr := range req.PostForm { + v := strings.Join(arr, "") + optNameAndType := strings.Split(k, ":") + optName := optNameAndType[0] + optType := optNameAndType[1] + cfg[core.ConfigOption{ + Name: optName, + Type: optType, + }] = v + } + *req = *req.WithContext(context.WithValue(req.Context(), "config", cfg)) + next.ServeHTTP(w, req) + } + + return http.HandlerFunc(handlerFunc) +} diff --git a/nirvash.go b/nirvash.go index 14023f8..2b83ca5 100644 --- a/nirvash.go +++ b/nirvash.go @@ -27,14 +27,18 @@ func main() { cfg.Adapter.Init(cfg) + pathConcat := filepath.Join + rtr := &router.Router{ StaticPaths: map[string]string{ - "/static": cfg.AssetRoot, + "/static": filepath.Join(cfg.AssetRoot, "static"), }, } + templateRoot := pathConcat(cfg.AssetRoot, "templates") + rtr.Get("/login", renderer.Template( - "templates/login.html")) + pathConcat(templateRoot, "login.html"))) rtr.Post("/login", middleware.Authorize("/", udb, "/login?tryagain=1")) @@ -45,9 +49,9 @@ func main() { middleware.Protected( shell.WithAdapter( renderer.Template( - "templates/cms_list.html", - "templates/header.html", - "templates/footer.html"), + pathConcat(templateRoot, "cms_list.html"), + pathConcat(templateRoot, "header.html"), + pathConcat(templateRoot, "footer.html")), cfg.Adapter), http.MethodGet, udb, @@ -59,9 +63,9 @@ func main() { middleware.Protected( shell.WithAdapter( renderer.Template( - "templates/cms_edit.html", - "templates/header.html", - "templates/footer.html"), + pathConcat(templateRoot, "cms_edit.html"), + pathConcat(templateRoot, "header.html"), + pathConcat(templateRoot, "footer.html")), cfg.Adapter), http.MethodGet, udb, @@ -74,9 +78,9 @@ func main() { shell.WithAdapter( shell.EnsurePageData( renderer.Template( - "templates/cms_save.html", - "templates/header.html", - "templates/footer.html"), + pathConcat(templateRoot, "cms_save.html"), + pathConcat(templateRoot, "header.html"), + pathConcat(templateRoot, "footer.html")), cfg.Adapter), cfg.Adapter), http.MethodGet, @@ -91,9 +95,9 @@ func main() { middleware.Protected( shell.WithAdapter( renderer.Template( - "templates/cms_new.html", - "templates/header.html", - "templates/footer.html"), + pathConcat(templateRoot, "cms_new.html"), + pathConcat(templateRoot, "header.html"), + pathConcat(templateRoot, "footer.html")), cfg.Adapter), http.MethodGet, udb, @@ -106,9 +110,9 @@ func main() { shell.WithAdapter( shell.EnsurePageData( renderer.Template( - "templates/cms_create.html", - "templates/header.html", - "templates/footer.html"), + pathConcat(templateRoot, "cms_create.html"), + pathConcat(templateRoot, "header.html"), + pathConcat(templateRoot, "footer.html")), cfg.Adapter), cfg.Adapter), http.MethodGet, @@ -123,9 +127,9 @@ func main() { middleware.Protected( shell.WithAdapter( renderer.Template( - "templates/build.html", - "templates/header.html", - "templates/footer.html"), + pathConcat(templateRoot, "build.html"), + pathConcat(templateRoot, "header.html"), + pathConcat(templateRoot, "footer.html")), cfg.Adapter), http.MethodGet, udb, @@ -135,12 +139,13 @@ func main() { `/build-run`, middleware.Defend( middleware.Protected( - shell.WithAdapter( - renderer.Template( - "templates/build_run.html", - "templates/header.html", - "templates/footer.html"), - cfg.Adapter), + shell.SanitizeFormMap( + shell.WithAdapter( + renderer.Template( + pathConcat(templateRoot, "build_run.html"), + pathConcat(templateRoot, "header.html"), + pathConcat(templateRoot, "footer.html")), + cfg.Adapter)), http.MethodGet, udb, "/login"), @@ -153,14 +158,48 @@ func main() { middleware.Protected( shell.WithAdapter( renderer.Template( - "templates/delete.html", - "templates/header.html", - "templates/footer.html"), + pathConcat(templateRoot, "delete.html"), + pathConcat(templateRoot, "header.html"), + pathConcat(templateRoot, "footer.html")), cfg.Adapter), http.MethodGet, udb, "/login"), udb, "/")) + + rtr.Get( + `/config`, + middleware.Fortify( + middleware.Protected( + shell.WithAdapter( + renderer.Template( + pathConcat(templateRoot, "config.html"), + pathConcat(templateRoot, "header.html"), + pathConcat(templateRoot, "footer.html")), + cfg.Adapter), + http.MethodGet, + udb, + "/login"))) + + rtr.Post( + `/config-set`, + middleware.Defend( + middleware.Protected( + shell.SanitizeFormMap( + shell.FormMapToAdapterConfig( + shell.WithAdapter( + renderer.Template( + pathConcat(templateRoot, "config_set.html"), + pathConcat(templateRoot, "header.html"), + pathConcat(templateRoot, "footer.html")), + cfg.Adapter), + cfg.Adapter)), + http.MethodGet, + udb, + "/login"), + udb, + "/")) + http.ListenAndServe(":8080", rtr) } diff --git a/static/style.css b/static/style.css index 6d02248..47b8b68 100644 --- a/static/style.css +++ b/static/style.css @@ -1,7 +1,7 @@ body { padding: 0; margin: 0; - font-family: sans; + font-family: sans-serif; font-size: 16px; height: 100vh; background: black; @@ -37,7 +37,18 @@ body { color: lightgray; } -.login form input { +.login label { + padding: 0; + margin: 0; + height: 0; + width: 0; + font-size: 0; + display: inline; + position: absolute; +} + + +.login form input, .login form input:-internal-autofill-selected { display: block; margin: 1em; margin-left: auto; @@ -52,6 +63,7 @@ body { .login form input[type="text"], login form input[type="password"] { transition: border 1s; outline: none; + margin-bottom: -17px; } .login form input:focus { @@ -61,6 +73,7 @@ body { .login form input[type="submit"] { + margin-top: -17px; text-transform: uppercase; transition: background 1s, color 1s; } @@ -155,7 +168,9 @@ h2 { top: 3em; } -.page-list, form.editor, form.build, span.adapter-error, span.adapter-success, .danger-zone { +.page-list, form.editor, form.build, form.configurator, span.adapter-error, span.adapter-success, .danger-zone { + display: block; + overflow-x: hidden; width: 80%; max-width: 500px; background: rgba(0,0,0,0.8); @@ -166,13 +181,17 @@ h2 { overflow-y: auto; } -form.editor label, form.build label { +span.adapter-error { + border-bottom: 2px solid crimson; +} + +form.editor label, form.build label, .danger-zone label, form.configurator label { font-size: 80%; color: lightgray; text-transform: uppercase; } -form.editor input, form.build input, form.editor textarea { +form.editor input, form.build input, form.editor textarea, form.configurator input, form.configurator textarea, .danger-zone input[type="submit"] { display: block; margin: 0; margin-top: 0.2em; @@ -185,24 +204,42 @@ form.editor input, form.build input, form.editor textarea { outline: none; } -form.editor input.title-input { +form.editor input[type="text"], form.configurator input[type="text"], form.configurator input[type="number"] { margin: 0; width: 100%; + +} + +form.editor input.title-input { font-size: 150%; } +form input:focus, form textarea:focus { + border: 2px solid cyan; +} + form.editor, .danger-zone { max-width: 80em; } form.editor textarea { margin: 0; - width: 80em; + width: 100%; font-size: 16px; height: 25em; } -form.editor input[type="submit"], form.build input[type="submit"] { +form.configurator textarea { + marign: 0; + width: 100%; + height: 5em; +} + +form.configurator input, form.configurator textarea { + font-size: 125%; +} + +form.editor input[type="submit"], form.build input[type="submit"], .danger-zone input[type="submit"], form.configurator input[type="submit"] { margin-left: auto; margin-right: 0; font-size: 150%; @@ -210,7 +247,7 @@ form.editor input[type="submit"], form.build input[type="submit"] { transition: background 1s, color 1s; } -form.editor input[type="submit"]:hover { +form.editor input[type="submit"]:hover,form.build input[type="submit"]:hover, .danger-zone input[type="submit"]:hover, form.configurator input[type="submit"]:hover { background: lightgray; color: black; } @@ -234,4 +271,4 @@ form.editor input[type="submit"]:hover { form input[hidden] { display: none; -} \ No newline at end of file +} diff --git a/templates/config.html b/templates/config.html new file mode 100644 index 0000000..8ef55d8 --- /dev/null +++ b/templates/config.html @@ -0,0 +1,25 @@ +{{ $config := ((.Context).Value "adapter").GetConfig }} +{{ $csrfToken := (.Context).Value "csrfToken" }} + +{{ template "header" . }} + +

Configuration

+ +
+ + {{ range $opt, $val := $config }} + {{ if eq ($opt).Type "int" }} +
+ {{ else if eq ($opt).Type "float" }} +
+ {{ else if eq ($opt).Type "string" }} +
+ {{ else if eq ($opt).Type "multilinestring" }} +
+ {{ end}} + + {{ end }} + +
+ +{{ template "footer" . }} \ No newline at end of file diff --git a/templates/config_set.html b/templates/config_set.html new file mode 100644 index 0000000..5652f04 --- /dev/null +++ b/templates/config_set.html @@ -0,0 +1,14 @@ +{{ $config := (.Context).Value "config" }} +{{ $cfgError := ((.Context).Value "adapter").SetConfig $config }} + +{{ template "header" . }} + +{{ if $cfgError }} +

Configuration Error

+ {{($cfgError).Error}} +{{ else }} +

Configuration Saved

+ The adapter configuration has been saved +{{ end }} + +{{ template "footer" . }} \ No newline at end of file diff --git a/templates/login.html b/templates/login.html index ac9a045..9e9b985 100644 --- a/templates/login.html +++ b/templates/login.html @@ -17,8 +17,10 @@ Incorrect credentials; please try again. {{ end }}
- - + +
+ +