all repos — nirvash @ b7e2dc3ab7463cc239093623befde3901dcacec7

modular CMS using the quartzgun library

initial commit - cmd, config, and login working with Adapter interface and skeleton EurekaAdapter
Iris Lightshard nilix@nilfm.cc
PGP Signature
-----BEGIN PGP SIGNATURE-----

iQIzBAABCAAdFiEEkFh6dA+k/6CXFXU4O3+8IhROY5gFAmKS+ngACgkQO3+8IhRO
Y5gsBg/+KNRuZI5BlQhJ3VW+DhF/jJrpriP3WTlwqBlGtOy1MVc+Jk5xx4K1s5Ad
3DZEGK9fuvhxYBthZdpCGoKeo/jlQu2yvWYqrY2G2YvkwRja8ij4LCz6grGf4FHs
tTgc/gnu3toW/vXNaWasBAgUqPxV/paV8A3eE/+OZhwRTyUFcmk0fE1ayfgtirKt
dVihFRa+rL/bKGKXhH59d+m9a3NmDaTOjO31TFWcWAQPleGsm82J8WFiCwryJ0oS
HITq74QSmazKiEm6+/jP+Tc3TAijWU6u9myBStRtvVAkmbRjQHhbLu36HEzADDFK
R5y9NXEMLgtZqgrPT6JgXZwLPJ6o4oX8O9mPKf4eAysRU4uQEtmHV8UJhnkDPrhn
JDhpDASpmEydix6bcPtWD52nUdQUUT4mi6Nv+EYn+QhF0ZPeu7WXM76HzwW8n7n5
VnTuJvhptqZR6cFrPgCVdSkZbjdf9ttfAlHXbV43m09R4PXsX01g43WJE/QX3lBy
wkU5tbALNR0MAQUA6uEb8NErtBU4KNRM0ii4Y7tRZVX7cdEBOR+nLSk3lbcx6/JL
1SDzCeNkn1IySlIahvhr4W03SzvqBa5j7pQnqCl2kDYXwwalGZlB3Cb0Px5RqWuC
lwGFg/0PUZz+Ip9xymz1IZAWC4y77wEVLFX1Tf9veIqSkbxuEBk=
=/Y41
-----END PGP SIGNATURE-----
commit

b7e2dc3ab7463cc239093623befde3901dcacec7

A .gitignore

@@ -0,0 +1,1 @@

+nirvash
A README.md

@@ -0,0 +1,33 @@

+# NIRVASH + +![view within the cockpit of Nirvash](./static/bg.png) + +## about + +`nirvash` is a content management system (CMS) written in Go using the [quartzgun](https://nilfm.cc/git/quartzgun) library, designed to be efficient, modular, and easy to use. It uses an `Adapter` system that in theory allows almost any backend SSG to be used for actual page generation and backend storage. It's inspired by [Joost Van Der Schee](https://usecue.com)'s Usecue CMS. + +## installation and configuration + +Clone this repository and run `go build` to build `nirvash`. Just running `./nirvash` from there, if you haven't run it before, should run the configuration wizard (it runs if the config file found in your user's config directory is missing or incomplete). The configuration file looks like: + +``` +adapter=eureka // one of the supported adapters, currently just eureka +root=/path/to/ssg/root // path to where your SSG content root is +assetRoot=/path/to/asset/root // path to the Nirvash static assets (eg static/ directory in this repo) +staticRoot=/path/to/static/root // path to static file storage on your webserver +plugins=none // list of plugins to use, currently none are implemented +``` + +You can also set the configuration options by running eg `nirvash configure adapter=eureka root=/var/www`. Key-value pairs given on the command line are written to the configuration file, and pairs not listed are unmodified. + +User management is done from the command line as well: + +- `nirvash adduser username password` +- `nirvash rmuser username` +- `nirvash passwd username oldpass newpass` + +## usage + +Running `nirvash` without any arguments starts the webserver on port 8080. + +MORE TO COME
A adapter/adapter.go

@@ -0,0 +1,16 @@

+package adapter + +import ( + "nilfm.cc/git/nirvash/page" +) + +type Adapter interface { + Name() string + GetConfig(key string) (interface{}, error) + SetConfig(key string, value interface{}) error + ListPages() map[string]string + GetPage(string) page.Page + FormatPage(string) string + FormattingHelp() string + Build() +}
A adapter/eureka.go

@@ -0,0 +1,41 @@

+package adapter + +import ( + "nilfm.cc/git/nirvash/page" +) + +type EurekaAdapter struct { + Root string +} + +func (self *EurekaAdapter) Name() string { + return "eureka" +} + +func (self *EurekaAdapter) GetConfig(key string) (interface{}, error) { + return nil, nil +} + +func (self *EurekaAdapter) SetConfig(key string, value interface{}) error { + return nil +} + +func (self *EurekaAdapter) ListPages() map[string]string { + return map[string]string{} +} + +func (self *EurekaAdapter) GetPage(path string) page.Page { + return page.Page{} +} + +func (self *EurekaAdapter) FormatPage(raw string) string { + return raw +} + +func (self *EurekaAdapter) FormattingHelp() string { + return "help!" +} + +func (self *EurekaAdapter) Build() { + return +}
A cmd/cmd.go

@@ -0,0 +1,61 @@

+package cmd + +import ( + "fmt" + "strings" + "nilfm.cc/git/quartzgun/auth" + "nilfm.cc/git/nirvash/config" +) + +func Process(args []string, userStore auth.UserStore, cfg *config.Config) bool { + if len(args) == 1 { + return false + } + switch args[1] { + case "adduser": + if len(args) < 4 { + return help() + } + userStore.AddUser(args[2], args[3]) + case "rmuser": + if len(args) < 3 { + return help() + } + userStore.DeleteUser(args[2]) + case "passwd": + if len(args) < 5 { + return help() + } + userStore.ChangePassword(args[2], args[3], args[4]) + case "configure": + fmt.Printf("configuring\n") + for _, token := range args[2:] { + kvp := strings.Split(token, "=") + k := kvp[0] + v := kvp[1] + fmt.Printf("%s = %s\n", k, v) + switch k { + case "adapter": + config.SetAdapter(cfg, v) + case "root": + cfg.Root = v + case "assetRoot": + cfg.AssetRoot = v + case "staticRoot": + cfg.StaticRoot = v + case "plugins": + // handle plugins later + default: + panic("unknown configuration option: " + v) + } + } + config.Write(cfg) + default: + help() + } + return true +} + +func help() bool { + return true +}
A config/config.go

@@ -0,0 +1,173 @@

+package config + +import ( + "fmt" + "runtime" + "os" + "strings" + "path/filepath" + "nilfm.cc/git/nirvash/adapter" +) + +type Config struct { + Adapter adapter.Adapter // adapter for this instance + Root string // root of the site data + StaticRoot string // root of static files for StaticFileManager + AssetRoot string // root of Nirvash dist files (CSS, images) + Plugins map[string]interface{} +} + +func GetConfigLocation() string { + home := os.Getenv("HOME") + appdata := os.Getenv("APPDATA") + switch (runtime.GOOS) { + case "windows": + return filepath.Join(appdata, "nirvash") + case "darwin": + return filepath.Join(home, "Library", "Application Support", "nirvash") + case "plan9": + return filepath.Join(home, "lib", "nirvash") + default: + return filepath.Join(home, ".config", "nirvash") + } +} + +func ensureConfigLocationExists() { + _, err := os.Stat(GetConfigLocation()) + + if os.IsNotExist(err) { + os.MkdirAll(GetConfigLocation(), os.ModePerm) + } +} + +func Read() *Config { + ensureConfigLocationExists() + return parseConfig(filepath.Join(GetConfigLocation(), "nirvash.conf")) +} + +func Write(cfg *Config) error { + ensureConfigLocationExists() + return writeConfig(cfg, filepath.Join(GetConfigLocation(), "nirvash.conf")) +} + + +func SetAdapter(cfg *Config, adptr string) { + switch adptr { + case "eureka": + cfg.Adapter = &adapter.EurekaAdapter{} + default: + panic("Unsupported adapter! Try one of [ eureka ]") + } +} + + + +func IsNull(cfg *Config) bool { + return cfg.Adapter == nil || len(cfg.Root) == 0 || len(cfg.StaticRoot) == 0 || len(cfg.AssetRoot) == 0 +} + +func RunWizard(cfg *Config) { + fmt.Printf("All options are required.\n") + defer func(cfg *Config) { + if r := recover(); r != nil { + fmt.Printf("Invalid selection, starting over...") + RunWizard(cfg) + } + }(cfg) + inputBuf := "" + fmt.Printf("adapter? (eureka) [eureka] ") + fmt.Scanln(&inputBuf) + if len(strings.TrimSpace(inputBuf)) == 0 { + inputBuf = "eureka" + } + SetAdapter(cfg, inputBuf) + + inputBuf = "" + fmt.Printf("site data root? ") + ensureNonEmptyOption(&inputBuf) + cfg.Root = inputBuf + + inputBuf = "" + + fmt.Printf("static file root? ") + ensureNonEmptyOption(&inputBuf) + cfg.StaticRoot = inputBuf + + inputBuf = "" + fmt.Printf("nirvash asset root? ") + ensureNonEmptyOption(&inputBuf) + cfg.AssetRoot = inputBuf + + inputBuf = "" + fmt.Printf("plugins? (not implemented yet) ") + ensureNonEmptyOption(&inputBuf) + //cfg.Plugins = processPlugins(inputBuf) + + fmt.Printf("Configuration complete!\n") + Write(cfg) +} + +func ensureNonEmptyOption(buffer *string) { + for ;; { + fmt.Scanln(buffer) + if len(strings.TrimSpace(*buffer)) != 0 { + break + } + } +} + +func writeConfig(cfg *Config, configFile string) error { + f, err := os.Create(configFile) + if err != nil { + return err + } + + defer f.Close() + + f.WriteString("root=" + cfg.Root + "\n") + f.WriteString("staticRoot=" + cfg.StaticRoot + "\n") + f.WriteString("assetRoot=" + cfg.AssetRoot + "\n") + f.WriteString("adapter=" + cfg.Adapter.Name() + "\n") + f.WriteString("plugins=\n") + return nil +} + +func parseConfig(configFile string) *Config { + f, err := os.ReadFile(configFile) + cfg := &Config{} + if err != nil { + return cfg + } + + fileData := string(f[:]) + + lines := strings.Split(fileData, "\n") + + for _, l := range lines { + if len(l) == 0 { + continue + } + if !strings.Contains(l, "=") { + panic("Malformed config not in INI format") + } + + kvp := strings.Split(l, "=") + k := strings.TrimSpace(kvp[0]) + v := strings.TrimSpace(kvp[1]) + switch k { + case "root": + cfg.Root = v + case "staticRoot": + cfg.StaticRoot = v + case "assetRoot": + cfg.AssetRoot = v + case "plugins": + // not implemented + case "adapter": + SetAdapter(cfg, v) + default: + panic("Unrecognized config option: " + k) + } + } + return cfg +}
A go.mod

@@ -0,0 +1,7 @@

+module nilfm.cc/git/nirvash + +go 1.17 + +require nilfm.cc/git/quartzgun v0.1.0 + +require golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898 // indirect
A go.sum

@@ -0,0 +1,25 @@

+golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M= +golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898 h1:SLP7Q4Di66FONjDJbCYrCRrh97focO6sLogHO7/g8F0= +golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +nilfm.cc/git/quartzgun v0.0.0-20220516042416-1dbca325d20a h1:NtR/vUiY7nhEARMOXgabxwd4Z2kbC/z0AJgtpQ04ai0= +nilfm.cc/git/quartzgun v0.0.0-20220516042416-1dbca325d20a/go.mod h1:YqXoEQkRNOU1fZXeq5r2kTzvNbaH2VmULRP9an/sBX4= +nilfm.cc/git/quartzgun v0.0.0-20220516045132-9bf93d5c7575 h1:68aITeSQJ2EMuyWVNPsQvYw9W/sUsbzt2CNyg6Jg7bs= +nilfm.cc/git/quartzgun v0.0.0-20220516045132-9bf93d5c7575/go.mod h1:/DDvt1DtzNuUf3HHaP29WMei/kkdaRW+ySmEzybvVto= +nilfm.cc/git/quartzgun v0.0.0-20220516045804-ac526a0d7890 h1:R+jc5HoSg88gUlj5tVsm9ZsEkaNw0i+4e9xzeCJE9ig= +nilfm.cc/git/quartzgun v0.0.0-20220516045804-ac526a0d7890/go.mod h1:/DDvt1DtzNuUf3HHaP29WMei/kkdaRW+ySmEzybvVto= +nilfm.cc/git/quartzgun v0.0.0-20220516052922-27b61b7e68a2 h1:xufV1FtykeEITJegz7qSqQOnsESTt1mIBJ09zAAzpgg= +nilfm.cc/git/quartzgun v0.0.0-20220516052922-27b61b7e68a2/go.mod h1:/DDvt1DtzNuUf3HHaP29WMei/kkdaRW+ySmEzybvVto= +nilfm.cc/git/quartzgun v0.0.0-20220516055202-14a8c12fd440 h1:R1b9Jl6vDVAaCs+MaYI4LMVVajwQ2jGZcqDL8L33SA0= +nilfm.cc/git/quartzgun v0.0.0-20220516055202-14a8c12fd440/go.mod h1:/DDvt1DtzNuUf3HHaP29WMei/kkdaRW+ySmEzybvVto= +nilfm.cc/git/quartzgun v0.0.0-20220516061509-0e5a81f27b63 h1:HlIWrDDJjOFLrxPQzldzDz78K8Z5NDtTCoYkmmI8/JA= +nilfm.cc/git/quartzgun v0.0.0-20220516061509-0e5a81f27b63/go.mod h1:/DDvt1DtzNuUf3HHaP29WMei/kkdaRW+ySmEzybvVto= +nilfm.cc/git/quartzgun v0.1.0 h1:G+f/UnGpm5FAEqaY3Lj5UHvq0eB5sytM5s4FLesLC3E= +nilfm.cc/git/quartzgun v0.1.0/go.mod h1:/DDvt1DtzNuUf3HHaP29WMei/kkdaRW+ySmEzybvVto=
A nirvash.go

@@ -0,0 +1,46 @@

+package main + +import ( + "os" + "path/filepath" + "net/http" + "nilfm.cc/git/quartzgun/indentalUserDB" + "nilfm.cc/git/quartzgun/router" + "nilfm.cc/git/quartzgun/renderer" + "nilfm.cc/git/quartzgun/middleware" + "nilfm.cc/git/nirvash/cmd" + "nilfm.cc/git/nirvash/config" +) + +func main() { + cfg := config.Read() + udb := indentalUserDB.CreateIndentalUserDB( + filepath.Join( + config.GetConfigLocation(), + "user.db")) + if cmd.Process(os.Args, udb, cfg) { + os.Exit(0) + } + if config.IsNull(cfg) { + config.RunWizard(cfg) + } + + rtr := &router.Router{ + StaticPaths: map[string]string{ + "/static": cfg.AssetRoot, + }, + } + + rtr.Get("/login", renderer.Template( + "templates/login.html")) + + rtr.Post("/login", middleware.Authorize("/", udb, "/login?tryagain=1")) + + rtr.Get("/", middleware.Protected( + renderer.Template( + "templates/cms_list.html", + "templates/header.html", + "templates/footer.html"), http.MethodGet, udb, "/login")) + + http.ListenAndServe(":8080", rtr) +}
A page/page.go

@@ -0,0 +1,11 @@

+package page + +import ( + "time" +) + +type Page struct { + Title string + Content string + Edited time.Time +}
A static/style.css

@@ -0,0 +1,74 @@

+body { + padding: 0; + margin: 0; + font-family: sans; +} + +.login-body { + background: url('/static/bg.png'); + background-size: cover; + background-position: center center; + height: 100vh; +} + +.login { + text-align: center; + position: relative; + width: 100%; + box-sizing: border-box; + height: auto; + top: 50%; + transform: translateY(-50%); + -webkit-transform: translateY(-50%); + background-color: rgba(0,0,0,0.8); + padding: 1em; +} + +.login h1 { + font-size: 225%; + text-transform: uppercase; + color: lightgray; +} + +.login form input { + display: block; + margin: 1em; + margin-left: auto; + margin-right: auto; + background: transparent; + border: solid 2px lightgray; + font-size: 200%; + color: lightgray; + padding: 0.2em; +} + +.login form input[type="text"], login form input[type="password"] { + transition: border 1s; + outline: none; +} + +.login form input:focus { + border: solid 2px cyan; + outline: none; +} + + +.login form input[type="submit"] { + text-transform: uppercase; + transition: background 1s, color 1s; +} + +.login form input[type="submit"]:hover { + background: lightgray; + color: black; +} + +.login .error { + positon: relative; + text-align: center; + margin-left: auto; + margin-right: auto; + color: lightgray; + border-bottom: 2px solid crimson; + width: auto; +}
A templates/cms_list.html

@@ -0,0 +1,3 @@

+{{ template "header" .}} +<h1>It works!</h1> +{{ template "footer" .}}
A templates/error.html

@@ -0,0 +1,17 @@

+{{ $params := (.Context).Value "params" }} + +<!DOCTYPE html> +<html lang='en'> + <head> + <meta charset='utf-8'> + + <meta name='viewport' content='width=device-width,initial-scale=1'> + <link rel='shortcut icon' href='/favicon.ico'> + <title>test &mdash; error</title> + </head> + <body> + <header><h1>{{ $params.ErrorCode }}</h1></header> + <main> + {{ $params.ErrorMessage }} + </main> +{{ template "footer" .}}
A templates/footer.html

@@ -0,0 +1,5 @@

+{{define "footer"}} + TEST + </body> +</html> +{{end}}
A templates/header.html

@@ -0,0 +1,11 @@

+{{define "header"}} +<!DOCTYPE html> +<html lang='en'> + <head> + <meta charset='utf-8'> + <meta name='description' content='Nirvash CMS'/> + <meta name='viewport' content='width=device-width,initial-scale=1'> + <title>Nirvash &mdash; Test</title> + </head> + <body> +{{end}}
A templates/login.html

@@ -0,0 +1,26 @@

+{{ $tryagain := .FormValue "tryagain" }} + +<!DOCTYPE html> +<html lang='en'> + <head> + <meta charset='utf-8'> + <meta name='description' content='Nirvash CMS'/> + <meta name='viewport' content='width=device-width,initial-scale=1'> + <title>Nirvash &mdash; Login</title> + <link rel='stylesheet' type='text/css' href='/static/style.css'> + <link rel='shortcut icon' href='/static/favicon.png'> + </head> + <body class="login-body"> + <div class="login"> + <h1>Nirvash</h1> + {{ if $tryagain }} + <span class="error">Incorrect credentials; please try again.</span> + {{ end }} + <form action='/login' method='post'> + <input type="text" name="user" placeholder="user"> + <input type="password" name="password" placeholder="password"> + <input type="submit" value="Login"> + </form> + </div> + </body> +</html>
A templates/paramTest.html

@@ -0,0 +1,19 @@

+{{ $params := (.Context).Value "params" }} + +<!DOCTYPE html> +<html lang='en'> + <head> + <meta charset='utf-8'> + + <meta name='viewport' content='width=device-width,initial-scale=1'> + <link rel='stylesheet' type='text/css' href='/style.css'> + <link rel='shortcut icon' href='/favicon.ico'> + <title>test &mdash; thing</title> + </head> + <body> + <header><h1>nilFM</h1></header> + <main> + {{ $params.Thing }} + </main> + </body> +</html>
A templates/test.html

@@ -0,0 +1,17 @@

+<!DOCTYPE html> +<html lang='en'> + <head> + <meta charset='utf-8'> + + <meta name='viewport' content='width=device-width,initial-scale=1'> + <link rel='stylesheet' type='text/css' href='/style.css'> + <link rel='shortcut icon' href='/favicon.ico'> + <title>test &mdash; something</title> + </head> + <body> + <header><h1>nilFM</h1></header> + <main> + {{ .Form.Get "Content" }} + </main> + </body> +</html>