initial commit - cmd, config, and login working with Adapter interface and skeleton EurekaAdapter

This commit is contained in:
Iris Lightshard 2022-05-28 22:45:44 -06:00
commit b7e2dc3ab7
Signed by: Iris Lightshard
GPG key ID: 3B7FBC22144E6398
19 changed files with 586 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
nirvash

33
README.md Normal file
View file

@ -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

16
adapter/adapter.go Normal file
View file

@ -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()
}

41
adapter/eureka.go Normal file
View file

@ -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
}

61
cmd/cmd.go Normal file
View file

@ -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
}

173
config/config.go Normal file
View file

@ -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
}

7
go.mod Normal file
View file

@ -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

25
go.sum Normal file
View file

@ -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=

46
nirvash.go Normal file
View file

@ -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)
}

11
page/page.go Normal file
View file

@ -0,0 +1,11 @@
package page
import (
"time"
)
type Page struct {
Title string
Content string
Edited time.Time
}

BIN
static/bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

74
static/style.css Normal file
View file

@ -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;
}

3
templates/cms_list.html Normal file
View file

@ -0,0 +1,3 @@
{{ template "header" .}}
<h1>It works!</h1>
{{ template "footer" .}}

17
templates/error.html Normal file
View file

@ -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" .}}

5
templates/footer.html Normal file
View file

@ -0,0 +1,5 @@
{{define "footer"}}
TEST
</body>
</html>
{{end}}

11
templates/header.html Normal file
View file

@ -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}}

26
templates/login.html Normal file
View file

@ -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>

19
templates/paramTest.html Normal file
View file

@ -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>

17
templates/test.html Normal file
View file

@ -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>