commit b7e2dc3ab7463cc239093623befde3901dcacec7 Author: Derek Stevens Date: Sat May 28 22:45:44 2022 -0600 initial commit - cmd, config, and login working with Adapter interface and skeleton EurekaAdapter diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5e2c1bd --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +nirvash \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5653d67 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/adapter/adapter.go b/adapter/adapter.go new file mode 100644 index 0000000..35a030e --- /dev/null +++ b/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() +} \ No newline at end of file diff --git a/adapter/eureka.go b/adapter/eureka.go new file mode 100644 index 0000000..ffbf0c4 --- /dev/null +++ b/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 +} \ No newline at end of file diff --git a/cmd/cmd.go b/cmd/cmd.go new file mode 100644 index 0000000..ee97811 --- /dev/null +++ b/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 +} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..86d1278 --- /dev/null +++ b/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 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7f100e7 --- /dev/null +++ b/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 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1e975db --- /dev/null +++ b/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= diff --git a/nirvash.go b/nirvash.go new file mode 100644 index 0000000..1a46971 --- /dev/null +++ b/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) +} \ No newline at end of file diff --git a/page/page.go b/page/page.go new file mode 100644 index 0000000..b79a661 --- /dev/null +++ b/page/page.go @@ -0,0 +1,11 @@ +package page + +import ( + "time" +) + +type Page struct { + Title string + Content string + Edited time.Time +} \ No newline at end of file diff --git a/static/bg.png b/static/bg.png new file mode 100644 index 0000000..60c902f Binary files /dev/null and b/static/bg.png differ diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..076f56c --- /dev/null +++ b/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; +} \ No newline at end of file diff --git a/templates/cms_list.html b/templates/cms_list.html new file mode 100644 index 0000000..4dfa833 --- /dev/null +++ b/templates/cms_list.html @@ -0,0 +1,3 @@ +{{ template "header" .}} +

It works!

+{{ template "footer" .}} \ No newline at end of file diff --git a/templates/error.html b/templates/error.html new file mode 100644 index 0000000..eed8715 --- /dev/null +++ b/templates/error.html @@ -0,0 +1,17 @@ +{{ $params := (.Context).Value "params" }} + + + + + + + + + test — error + + +

{{ $params.ErrorCode }}

+
+ {{ $params.ErrorMessage }} +
+{{ template "footer" .}} diff --git a/templates/footer.html b/templates/footer.html new file mode 100644 index 0000000..cb0be69 --- /dev/null +++ b/templates/footer.html @@ -0,0 +1,5 @@ +{{define "footer"}} + TEST + + +{{end}} \ No newline at end of file diff --git a/templates/header.html b/templates/header.html new file mode 100644 index 0000000..f3c546f --- /dev/null +++ b/templates/header.html @@ -0,0 +1,11 @@ +{{define "header"}} + + + + + + + Nirvash — Test + + +{{end}} \ No newline at end of file diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..ac9a045 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,26 @@ +{{ $tryagain := .FormValue "tryagain" }} + + + + + + + + Nirvash — Login + + + + +
+

Nirvash

+ {{ if $tryagain }} + Incorrect credentials; please try again. + {{ end }} +
+ + + +
+
+ + diff --git a/templates/paramTest.html b/templates/paramTest.html new file mode 100644 index 0000000..d5e072e --- /dev/null +++ b/templates/paramTest.html @@ -0,0 +1,19 @@ +{{ $params := (.Context).Value "params" }} + + + + + + + + + + test — thing + + +

nilFM

+
+ {{ $params.Thing }} +
+ + diff --git a/templates/test.html b/templates/test.html new file mode 100644 index 0000000..915596d --- /dev/null +++ b/templates/test.html @@ -0,0 +1,17 @@ + + + + + + + + + test — something + + +

nilFM

+
+ {{ .Form.Get "Content" }} +
+ +