Add JsonDB and initial Admin Setup POST handler
This commit is contained in:
parent
456cad7a50
commit
c69dbfebba
2
Makefile
2
Makefile
|
@ -21,7 +21,7 @@ build:
|
||||||
# `make run` to run it in debug mode.
|
# `make run` to run it in debug mode.
|
||||||
.PHONY: run
|
.PHONY: run
|
||||||
run:
|
run:
|
||||||
./go-reload main.go -debug
|
./go-reload main.go -debug root
|
||||||
|
|
||||||
# `make test` to run unit tests.
|
# `make test` to run unit tests.
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
|
|
|
@ -1,10 +1,57 @@
|
||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/kirsle/blog/core/models/users"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SetupHandler is the initial blog setup route.
|
// SetupHandler is the initial blog setup route.
|
||||||
func (b *Blog) SetupHandler(w http.ResponseWriter, r *http.Request) {
|
func (b *Blog) SetupHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
b.RenderTemplate(w, r, "admin/setup", nil)
|
vars := map[string]interface{}{
|
||||||
|
"errors": []error{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Method == "POST" {
|
||||||
|
var errors []error
|
||||||
|
payload := struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
Confirm string
|
||||||
|
}{
|
||||||
|
Username: r.FormValue("username"),
|
||||||
|
Password: r.FormValue("password"),
|
||||||
|
Confirm: r.FormValue("confirm"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate stuff.
|
||||||
|
if len(payload.Username) == 0 {
|
||||||
|
errors = append(errors, fmt.Errorf("Admin Username is required"))
|
||||||
|
}
|
||||||
|
if len(payload.Password) < 3 {
|
||||||
|
errors = append(errors, fmt.Errorf("Admin Password is too short"))
|
||||||
|
}
|
||||||
|
if payload.Password != payload.Confirm {
|
||||||
|
errors = append(errors, fmt.Errorf("Your passwords do not match"))
|
||||||
|
}
|
||||||
|
|
||||||
|
vars["errors"] = errors
|
||||||
|
|
||||||
|
// No problems?
|
||||||
|
if len(errors) == 0 {
|
||||||
|
log.Info("Creating admin account %s", payload.Username)
|
||||||
|
user := &users.User{
|
||||||
|
Username: payload.Username,
|
||||||
|
Password: payload.Password,
|
||||||
|
}
|
||||||
|
err := b.DB.Commit("users/by-name/"+payload.Username, user)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Error: %v", err)
|
||||||
|
b.BadRequest(w, r, "DB error when writing user")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.RenderTemplate(w, r, "admin/setup", vars)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,10 @@ package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/kirsle/blog/core/jsondb"
|
||||||
"github.com/urfave/negroni"
|
"github.com/urfave/negroni"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -16,6 +18,8 @@ type Blog struct {
|
||||||
DocumentRoot string
|
DocumentRoot string
|
||||||
UserRoot string
|
UserRoot string
|
||||||
|
|
||||||
|
DB *jsondb.DB
|
||||||
|
|
||||||
// Web app objects.
|
// Web app objects.
|
||||||
n *negroni.Negroni // Negroni middleware manager
|
n *negroni.Negroni // Negroni middleware manager
|
||||||
r *mux.Router // Router
|
r *mux.Router // Router
|
||||||
|
@ -26,7 +30,9 @@ func New(documentRoot, userRoot string) *Blog {
|
||||||
blog := &Blog{
|
blog := &Blog{
|
||||||
DocumentRoot: documentRoot,
|
DocumentRoot: documentRoot,
|
||||||
UserRoot: userRoot,
|
UserRoot: userRoot,
|
||||||
|
DB: jsondb.New(filepath.Join(userRoot, ".private")),
|
||||||
}
|
}
|
||||||
|
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
blog.r = r
|
blog.r = r
|
||||||
r.HandleFunc("/admin/setup", blog.SetupHandler)
|
r.HandleFunc("/admin/setup", blog.SetupHandler)
|
||||||
|
|
187
core/jsondb/jsondb.go
Normal file
187
core/jsondb/jsondb.go
Normal file
|
@ -0,0 +1,187 @@
|
||||||
|
// Package jsondb implements a flat file JSON database engine.
|
||||||
|
package jsondb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DB is the database manager.
|
||||||
|
type DB struct {
|
||||||
|
Root string // The root directory of the database
|
||||||
|
|
||||||
|
// Use Redis to cache filesystem reads of the database.
|
||||||
|
EnableRedis bool
|
||||||
|
RedisURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error codes returned.
|
||||||
|
var (
|
||||||
|
ErrNotFound = errors.New("document not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
// New initializes the JSON database.
|
||||||
|
func New(root string) *DB {
|
||||||
|
return &DB{
|
||||||
|
Root: root,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a document by path and load it into the object `v`.
|
||||||
|
func (db *DB) Get(document string, v interface{}) error {
|
||||||
|
log.Debug("GET %s", document)
|
||||||
|
if !db.Exists(document) {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the file path and stats.
|
||||||
|
path := db.toPath(document)
|
||||||
|
_, err := os.Stat(path) // TODO: mtime for caching
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the JSON.
|
||||||
|
err = db.readJSON(path, &v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit writes a JSON object to the database.
|
||||||
|
func (db *DB) Commit(document string, v interface{}) error {
|
||||||
|
log.Debug("COMMIT %s", document)
|
||||||
|
path := db.toPath(document)
|
||||||
|
|
||||||
|
// Ensure the directory tree is ready.
|
||||||
|
db.makePath(path)
|
||||||
|
|
||||||
|
// Write the document.
|
||||||
|
err := db.writeJSON(path, v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removes a JSON document from the database.
|
||||||
|
func (db *DB) Delete(document string) error {
|
||||||
|
log.Debug("DELETE %s", document)
|
||||||
|
path := db.toPath(document)
|
||||||
|
|
||||||
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
log.Warn("Delete document %s: did not exist")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.Remove(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exists tells you whether a document exists in the database.
|
||||||
|
func (db *DB) Exists(document string) bool {
|
||||||
|
if _, err := os.Stat(db.toPath(document)); os.IsNotExist(err) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all the documents at the path given.
|
||||||
|
func (db *DB) List(path string) ([]string, error) {
|
||||||
|
return db.list(path, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAll recursively lists the documents at the path prefix given.
|
||||||
|
func (db *DB) ListAll(path string) ([]string, error) {
|
||||||
|
return db.list(path, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// makePath ensures all the directory components in a document path exist.
|
||||||
|
// path: the filesystem path like from toPath().
|
||||||
|
func (db *DB) makePath(path string) error {
|
||||||
|
parts := strings.Split(path, string(filepath.Separator))
|
||||||
|
log.Debug("%v", parts)
|
||||||
|
parts = parts[:len(parts)-1] // pop off the filename
|
||||||
|
log.Debug("%v", parts)
|
||||||
|
directory := filepath.Join(parts...)
|
||||||
|
|
||||||
|
log.Debug("Ensure exists: %s (from orig path %s)", directory, path)
|
||||||
|
|
||||||
|
if _, err := os.Stat(directory); err != nil {
|
||||||
|
log.Debug("Create directory: %s", directory)
|
||||||
|
err = os.MkdirAll(directory, 0755)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// list returns the documents under a path with optional recursion.
|
||||||
|
func (db *DB) list(path string, recursive bool) ([]string, error) {
|
||||||
|
root := db.toPath(path)
|
||||||
|
var docs []string
|
||||||
|
|
||||||
|
files, err := ioutil.ReadDir(root)
|
||||||
|
if err != nil {
|
||||||
|
return docs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
filePath := filepath.Join(root, file.Name())
|
||||||
|
dbPath := filepath.Join(path, file.Name())
|
||||||
|
if file.IsDir() && recursive {
|
||||||
|
subfiles, err := db.list(dbPath, recursive)
|
||||||
|
if err != nil {
|
||||||
|
return docs, err
|
||||||
|
}
|
||||||
|
docs = append(docs, subfiles...)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(filePath, ".json") {
|
||||||
|
name := strings.TrimSuffix(filePath, ".json")
|
||||||
|
docs = append(docs, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return docs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// readJSON reads a JSON file from disk.
|
||||||
|
func (db *DB) readJSON(path string, v interface{}) error {
|
||||||
|
fh, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer fh.Close()
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(fh)
|
||||||
|
err = decoder.Decode(&v)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeJSON writes a JSON document to disk.
|
||||||
|
func (db *DB) writeJSON(path string, v interface{}) error {
|
||||||
|
fh, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer fh.Close()
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(fh)
|
||||||
|
encoder.SetIndent("", "\t")
|
||||||
|
encoder.Encode(v)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// toPath translates a document name into a filesystem path.
|
||||||
|
func (db *DB) toPath(document string) string {
|
||||||
|
return filepath.Join(db.Root, document+".json")
|
||||||
|
}
|
28
core/jsondb/log.go
Normal file
28
core/jsondb/log.go
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
// Package jsondb implements a flat file JSON database engine.
|
||||||
|
package jsondb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/kirsle/golog"
|
||||||
|
)
|
||||||
|
|
||||||
|
var log *golog.Logger
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log = golog.GetLogger("jsondb")
|
||||||
|
log.Configure(&golog.Config{
|
||||||
|
Level: golog.InfoLevel,
|
||||||
|
Colors: golog.ExtendedColor,
|
||||||
|
Theme: golog.DarkTheme,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDebug turns on debug logging.
|
||||||
|
func SetDebug(debug bool) {
|
||||||
|
if debug {
|
||||||
|
log.Config.Level = golog.DebugLevel
|
||||||
|
log.Debug("JsonDB Debug log enabled.")
|
||||||
|
} else {
|
||||||
|
log.Config.Level = golog.InfoLevel
|
||||||
|
log.Info("JsonDB Debug log disabled.")
|
||||||
|
}
|
||||||
|
}
|
19
core/models/base.go
Normal file
19
core/models/base.go
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import "github.com/kirsle/blog/core/jsondb"
|
||||||
|
|
||||||
|
// Model is a generic interface for models.
|
||||||
|
type Model interface {
|
||||||
|
UseDB(*jsondb.DB)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base is an implementation of the Model interface suitable for including in
|
||||||
|
// your actual models.
|
||||||
|
type Base struct {
|
||||||
|
DB *jsondb.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseDB stores a reference to your JSON DB for the model to use.
|
||||||
|
func (b *Base) UseDB(db *jsondb.DB) {
|
||||||
|
b.DB = db
|
||||||
|
}
|
17
core/models/users/users.go
Normal file
17
core/models/users/users.go
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
package users
|
||||||
|
|
||||||
|
import "github.com/kirsle/blog/core/models"
|
||||||
|
|
||||||
|
// User holds information about a user account.
|
||||||
|
type User struct {
|
||||||
|
models.Base
|
||||||
|
ID int `json:"id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) DocumentPath() string {
|
||||||
|
return "users/by-id/%s"
|
||||||
|
}
|
|
@ -22,15 +22,28 @@ func (b *Blog) NotFound(w http.ResponseWriter, r *http.Request, message ...strin
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err.Error())
|
log.Error(err.Error())
|
||||||
http.NotFound(w, r)
|
w.Write([]byte("Unrecoverable template error for NotFound()"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Forbidden sends an HTTP 400 Forbidden response.
|
// Forbidden sends an HTTP 403 Forbidden response.
|
||||||
func (b *Blog) Forbidden(w http.ResponseWriter, r *http.Request, message ...string) {
|
func (b *Blog) Forbidden(w http.ResponseWriter, r *http.Request, message ...string) {
|
||||||
w.WriteHeader(http.StatusForbidden)
|
w.WriteHeader(http.StatusForbidden)
|
||||||
err := b.RenderTemplate(w, r, ".errors/403", nil)
|
err := b.RenderTemplate(w, r, ".errors/403", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.NotFound(w, r)
|
log.Error(err.Error())
|
||||||
|
w.Write([]byte("Unrecoverable template error for Forbidden()"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BadRequest sends an HTTP 400 Bad Request.
|
||||||
|
func (b *Blog) BadRequest(w http.ResponseWriter, r *http.Request, message ...string) {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
err := b.RenderTemplate(w, r, ".errors/400", map[string]interface{}{
|
||||||
|
"message": message[0],
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err.Error())
|
||||||
|
w.Write([]byte("Unrecoverable template error for BadRequest()"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
15
main.go
15
main.go
|
@ -6,8 +6,11 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
"github.com/kirsle/blog/core"
|
"github.com/kirsle/blog/core"
|
||||||
|
"github.com/kirsle/blog/core/jsondb"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Build-time config constants.
|
// Build-time config constants.
|
||||||
|
@ -32,8 +35,16 @@ func init() {
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
userRoot := flag.Arg(0)
|
||||||
|
if userRoot == "" {
|
||||||
|
fmt.Printf("Need user root\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
app := core.New(DocumentRoot, "")
|
app := core.New(DocumentRoot, userRoot)
|
||||||
app.Debug = fDebug
|
if fDebug {
|
||||||
|
app.Debug = true
|
||||||
|
jsondb.SetDebug(true)
|
||||||
|
}
|
||||||
app.ListenAndServe(fAddress)
|
app.ListenAndServe(fAddress)
|
||||||
}
|
}
|
||||||
|
|
6
root/.errors/400.gohtml
Normal file
6
root/.errors/400.gohtml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{{ define "title" }}Bad Request{{ end }}
|
||||||
|
{{ define "content" }}
|
||||||
|
<h1>400 Bad Request</h1>
|
||||||
|
|
||||||
|
{{ .message }}
|
||||||
|
{{ end }}
|
|
@ -2,6 +2,16 @@
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<h1>Initial Setup</h1>
|
<h1>Initial Setup</h1>
|
||||||
|
|
||||||
|
{{ if .errors }}
|
||||||
|
<h2>Please correct the following errors:</h2>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{{ range .errors }}
|
||||||
|
<li>{{ . }}</li>
|
||||||
|
{{ end }}
|
||||||
|
</ul>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Welcome to your new web blog! To get started, you'll need to create a username
|
Welcome to your new web blog! To get started, you'll need to create a username
|
||||||
and password to be your <strong>admin user</strong>. You can create additional
|
and password to be your <strong>admin user</strong>. You can create additional
|
||||||
|
@ -16,11 +26,12 @@
|
||||||
<form method="POST" action="/admin/setup">
|
<form method="POST" action="/admin/setup">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="setup-admin-username">Admin username:</label>
|
<label for="setup-admin-username">Admin username:</label>
|
||||||
<input type="text" class="form-control" id="setup-admin-username" placeholder="Enter username">
|
<input type="text" name="username" class="form-control" id="setup-admin-username" placeholder="Enter username">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="setup-admin-password1">Passphrase:</label>
|
<label for="setup-admin-password1">Passphrase:</label>
|
||||||
<input type="password"
|
<input type="password"
|
||||||
|
name="password"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
id="setup-admin-password1"
|
id="setup-admin-password1"
|
||||||
placeholder="correct horse battery staple"
|
placeholder="correct horse battery staple"
|
||||||
|
@ -31,7 +42,11 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="setup-admin-password2">Confirm:</label>
|
<label for="setup-admin-password2">Confirm:</label>
|
||||||
<input type="password" class="form-control" id="setup-admin-password2" placeholder="correct horse battery staple">
|
<input type="password"
|
||||||
|
name="confirm"
|
||||||
|
class="form-control"
|
||||||
|
id="setup-admin-password2"
|
||||||
|
placeholder="correct horse battery staple">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary">Continue</button>
|
<button type="submit" class="btn btn-primary">Continue</button>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user