Initial form and model layout, user creating/loading
This commit is contained in:
parent
c69dbfebba
commit
6f330a3e92
|
@ -1,55 +1,45 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/kirsle/blog/core/forms"
|
||||
"github.com/kirsle/blog/core/models/users"
|
||||
)
|
||||
|
||||
// SetupHandler is the initial blog setup route.
|
||||
func (b *Blog) SetupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := map[string]interface{}{
|
||||
"errors": []error{},
|
||||
vars := &Vars{
|
||||
Form: forms.Setup{},
|
||||
}
|
||||
|
||||
if r.Method == "POST" {
|
||||
var errors []error
|
||||
payload := struct {
|
||||
Username string
|
||||
Password string
|
||||
Confirm string
|
||||
}{
|
||||
form := forms.Setup{
|
||||
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)
|
||||
vars.Form = form
|
||||
err := form.Validate()
|
||||
if err != nil {
|
||||
vars.Error = err
|
||||
} else {
|
||||
log.Info("Creating admin account %s", form.Username)
|
||||
user := &users.User{
|
||||
Username: payload.Username,
|
||||
Password: payload.Password,
|
||||
Username: form.Username,
|
||||
Password: form.Password,
|
||||
Admin: true,
|
||||
Name: "Administrator",
|
||||
}
|
||||
err := b.DB.Commit("users/by-name/"+payload.Username, user)
|
||||
err := users.Create(user)
|
||||
if err != nil {
|
||||
log.Error("Error: %v", err)
|
||||
b.BadRequest(w, r, "DB error when writing user")
|
||||
vars.Error = err
|
||||
}
|
||||
|
||||
// All set!
|
||||
b.Redirect(w, "/admin")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/kirsle/blog/core/jsondb"
|
||||
"github.com/kirsle/blog/core/models/users"
|
||||
"github.com/urfave/negroni"
|
||||
)
|
||||
|
||||
|
@ -33,6 +34,9 @@ func New(documentRoot, userRoot string) *Blog {
|
|||
DB: jsondb.New(filepath.Join(userRoot, ".private")),
|
||||
}
|
||||
|
||||
// Initialize all the models.
|
||||
users.DB = blog.DB
|
||||
|
||||
r := mux.NewRouter()
|
||||
blog.r = r
|
||||
r.HandleFunc("/admin/setup", blog.SetupHandler)
|
||||
|
|
9
core/forms/forms.go
Normal file
9
core/forms/forms.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package forms
|
||||
|
||||
import "net/http"
|
||||
|
||||
// Form is an interface for forms that can validate themselves.
|
||||
type Form interface {
|
||||
Parse(r *http.Request)
|
||||
Validate() error
|
||||
}
|
32
core/forms/setup.go
Normal file
32
core/forms/setup.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
package forms
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Setup is for the initial blog setup page at /admin/setup.
|
||||
type Setup struct {
|
||||
Username string
|
||||
Password string
|
||||
Confirm string
|
||||
}
|
||||
|
||||
// Parse the form.
|
||||
func (f Setup) Parse(r *http.Request) {
|
||||
f.Username = r.FormValue("username")
|
||||
f.Password = r.FormValue("password")
|
||||
f.Confirm = r.FormValue("confirm")
|
||||
}
|
||||
|
||||
// Validate the form.
|
||||
func (f Setup) Validate() error {
|
||||
if len(f.Username) == 0 {
|
||||
return errors.New("admin username is required")
|
||||
} else if len(f.Password) == 0 {
|
||||
return errors.New("admin password is required")
|
||||
} else if f.Password != f.Confirm {
|
||||
return errors.New("your passwords do not match")
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -124,7 +124,7 @@ func (db *DB) makePath(path string) error {
|
|||
|
||||
// list returns the documents under a path with optional recursion.
|
||||
func (db *DB) list(path string, recursive bool) ([]string, error) {
|
||||
root := db.toPath(path)
|
||||
root := filepath.Join(db.Root, path)
|
||||
var docs []string
|
||||
|
||||
files, err := ioutil.ReadDir(root)
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,15 +1,160 @@
|
|||
package users
|
||||
|
||||
import "github.com/kirsle/blog/core/models"
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/kirsle/blog/core/jsondb"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// DB is a reference to the parent app's JsonDB object.
|
||||
var DB *jsondb.DB
|
||||
|
||||
// HashCost is the cost value given to bcrypt to hash passwords.
|
||||
// TODO: make configurable from main package
|
||||
var HashCost = 14
|
||||
|
||||
// User holds information about a user account.
|
||||
type User struct {
|
||||
models.Base
|
||||
ID int `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Admin bool `json:"admin"`
|
||||
Name string `json:"name"`
|
||||
Role string `json:"role"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
// ByName model maps usernames to their IDs.
|
||||
type ByName struct {
|
||||
ID int `json:"id"`
|
||||
}
|
||||
|
||||
// Create a new user.
|
||||
func Create(u *User) error {
|
||||
// Sanity checks.
|
||||
u.Username = Normalize(u.Username)
|
||||
if len(u.Username) == 0 {
|
||||
return errors.New("username is required")
|
||||
} else if len(u.Password) == 0 {
|
||||
return errors.New("password is required")
|
||||
}
|
||||
|
||||
// Make sure the username is available.
|
||||
if UsernameExists(u.Username) {
|
||||
return errors.New("that username already exists")
|
||||
}
|
||||
|
||||
// Assign the next ID.
|
||||
u.ID = nextID()
|
||||
|
||||
// Hash the password.
|
||||
u.SetPassword(u.Password)
|
||||
|
||||
// TODO: check existing
|
||||
|
||||
return u.Save()
|
||||
}
|
||||
|
||||
// SetPassword sets a user's password by bcrypt hashing it. After this function,
|
||||
// u.Password will contain the bcrypt hash.
|
||||
func (u *User) SetPassword(password string) error {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), HashCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u.Password = string(hash)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UsernameExists checks if a username is taken.
|
||||
func UsernameExists(username string) bool {
|
||||
username = Normalize(username)
|
||||
return DB.Exists("users/by-name/" + username)
|
||||
}
|
||||
|
||||
// LoadUsername loads a user by username.
|
||||
func LoadUsername(username string) (*User, error) {
|
||||
username = Normalize(username)
|
||||
u := &User{}
|
||||
|
||||
// Look up the user ID by name.
|
||||
name := ByName{}
|
||||
err := DB.Get("users/by-name/"+username, &name)
|
||||
if err != nil {
|
||||
return u, fmt.Errorf("failed to look up user ID for username %s: %v", username, err)
|
||||
}
|
||||
|
||||
// And load that user.
|
||||
return Load(name.ID)
|
||||
}
|
||||
|
||||
// Load a user by their ID number.
|
||||
func Load(id int) (*User, error) {
|
||||
u := &User{}
|
||||
err := DB.Get(fmt.Sprintf("users/by-id/%d", id), &u)
|
||||
return u, err
|
||||
}
|
||||
|
||||
// Save the user.
|
||||
func (u *User) Save() error {
|
||||
// Sanity check that we have an ID.
|
||||
if u.ID == 0 {
|
||||
return errors.New("can't save: user does not have an ID!")
|
||||
}
|
||||
|
||||
// Save the main DB file.
|
||||
err := DB.Commit(u.key(), u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The username to ID mapping.
|
||||
err = DB.Commit(u.nameKey(), ByName{u.ID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get the next user ID number.
|
||||
func nextID() int {
|
||||
// Highest ID seen so far.
|
||||
var highest int
|
||||
|
||||
users, err := DB.List("users/by-id")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, doc := range users {
|
||||
fields := strings.Split(doc, "/")
|
||||
id, err := strconv.Atoi(fields[len(fields)-1])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if id > highest {
|
||||
highest = id
|
||||
}
|
||||
}
|
||||
|
||||
// Return the highest +1
|
||||
return highest + 1
|
||||
}
|
||||
|
||||
// DB key for users by ID number.
|
||||
func (u *User) key() string {
|
||||
return fmt.Sprintf("users/by-id/%d", u.ID)
|
||||
}
|
||||
|
||||
// DB key for users by username.
|
||||
func (u *User) nameKey() string {
|
||||
return "users/by-name/" + u.Username
|
||||
}
|
||||
|
||||
func (u *User) DocumentPath() string {
|
||||
|
|
15
core/models/users/utils.go
Normal file
15
core/models/users/utils.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package users
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Normalize lowercases and safens a username.
|
||||
func Normalize(username string) string {
|
||||
username = strings.ToLower(username)
|
||||
|
||||
// Strip special characters.
|
||||
re := regexp.MustCompile(`[./\\]+`)
|
||||
return re.ReplaceAllString(username, "")
|
||||
}
|
|
@ -17,8 +17,8 @@ func (b *Blog) NotFound(w http.ResponseWriter, r *http.Request, message ...strin
|
|||
}
|
||||
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
err := b.RenderTemplate(w, r, ".errors/404", map[string]interface{}{
|
||||
"message": message[0],
|
||||
err := b.RenderTemplate(w, r, ".errors/404", &Vars{
|
||||
Message: message[0],
|
||||
})
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
|
@ -39,8 +39,8 @@ func (b *Blog) Forbidden(w http.ResponseWriter, r *http.Request, message ...stri
|
|||
// 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],
|
||||
err := b.RenderTemplate(w, r, ".errors/400", &Vars{
|
||||
Message: message[0],
|
||||
})
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
|
|
|
@ -3,26 +3,35 @@ package core
|
|||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"github.com/kirsle/blog/core/forms"
|
||||
)
|
||||
|
||||
// DefaultVars combines template variables with default, globally available vars.
|
||||
func (b *Blog) DefaultVars(vars map[string]interface{}) map[string]interface{} {
|
||||
defaults := map[string]interface{}{
|
||||
"title": "Untitled Blog",
|
||||
}
|
||||
if vars == nil {
|
||||
return defaults
|
||||
}
|
||||
// Vars is an interface to implement by the templates to pass their own custom
|
||||
// variables in. It auto-loads global template variables (site name, etc.)
|
||||
// when the template is rendered.
|
||||
type Vars struct {
|
||||
// Global template variables.
|
||||
Title string
|
||||
|
||||
for k, v := range defaults {
|
||||
vars[k] = v
|
||||
}
|
||||
// Common template variables.
|
||||
Message string
|
||||
Error error
|
||||
Form forms.Form
|
||||
}
|
||||
|
||||
return vars
|
||||
// LoadDefaults combines template variables with default, globally available vars.
|
||||
func (v *Vars) LoadDefaults() {
|
||||
v.Title = "Untitled Blog"
|
||||
}
|
||||
|
||||
// TemplateVars is an interface that describes the template variable struct.
|
||||
type TemplateVars interface {
|
||||
LoadDefaults()
|
||||
}
|
||||
|
||||
// RenderTemplate responds with an HTML template.
|
||||
func (b *Blog) RenderTemplate(w http.ResponseWriter, r *http.Request, path string, vars map[string]interface{}) error {
|
||||
func (b *Blog) RenderTemplate(w http.ResponseWriter, r *http.Request, path string, vars TemplateVars) error {
|
||||
// Get the layout template.
|
||||
layout, err := b.ResolvePath(".layout")
|
||||
if err != nil {
|
||||
|
@ -46,7 +55,10 @@ func (b *Blog) RenderTemplate(w http.ResponseWriter, r *http.Request, path strin
|
|||
}
|
||||
|
||||
// Inject globally available variables.
|
||||
vars = b.DefaultVars(vars)
|
||||
if vars == nil {
|
||||
vars = &Vars{}
|
||||
}
|
||||
vars.LoadDefaults()
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; encoding=UTF-8")
|
||||
err = t.ExecuteTemplate(w, "layout", vars)
|
||||
|
|
|
@ -2,5 +2,5 @@
|
|||
{{ define "content" }}
|
||||
<h1>400 Bad Request</h1>
|
||||
|
||||
{{ .message }}
|
||||
{{ .Message }}
|
||||
{{ end }}
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
{{ define "title" }}Forbidden{{ end }}
|
||||
{{ define "content" }}
|
||||
<h1>403 Forbidden</h1>
|
||||
|
||||
{{ .Message }}
|
||||
{{ end }}
|
||||
|
|
|
@ -2,5 +2,5 @@
|
|||
{{ define "content" }}
|
||||
<h1>404 Not Found</h1>
|
||||
|
||||
{{ .message }}
|
||||
{{ .Message }}
|
||||
{{ end }}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
|
||||
<title>{{ template "title" or "Untitled" }} - {{ .title }}</title>
|
||||
<title>{{ template "title" or "Untitled" }} - {{ .Title }}</title>
|
||||
|
||||
<!-- Bootstrap core CSS -->
|
||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||
|
@ -15,7 +15,7 @@
|
|||
<body>
|
||||
|
||||
<nav class="navbar navbar-expand-md fixed-top bluez-navbar">
|
||||
<a href="#" class="navbar-brand">{{ .title }}</a>
|
||||
<a href="#" class="navbar-brand">{{ .Title }}</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
@ -41,7 +41,7 @@
|
|||
|
||||
<div class="bluez-header">
|
||||
<div class="container">
|
||||
<h1 class="bluez-title">{{ .title }}</h1>
|
||||
<h1 class="bluez-title">{{ .Title }}</h1>
|
||||
<p class="lead bluez-description">Just another web blog.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -49,6 +49,12 @@
|
|||
<div class="container mb-5">
|
||||
<div class="row">
|
||||
<div class="col-9">
|
||||
{{ if .Error }}
|
||||
<div class="alert alert-danger">
|
||||
<strong>Error:</strong> {{ .Error }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ template "content" . }}
|
||||
</div>
|
||||
<div class="col-3">
|
||||
|
|
11
root/admin/index.gohtml
Normal file
11
root/admin/index.gohtml
Normal file
|
@ -0,0 +1,11 @@
|
|||
{{ define "title" }}Admin Center{{ end }}
|
||||
{{ define "content" }}
|
||||
<h1>Admin Center</h1>
|
||||
|
||||
<ul>
|
||||
<li><a href="/admin/settings">App Settings</a></li>
|
||||
<li><a href="/blog/edit">Post Blog Entry</a></li>
|
||||
<li><a href="/admin/editor">Page Editor</a></li>
|
||||
<li><a href="/admin/users">User Management</a></li>
|
||||
</ul>
|
||||
{{ end }}
|
|
@ -2,16 +2,6 @@
|
|||
{{ define "content" }}
|
||||
<h1>Initial Setup</h1>
|
||||
|
||||
{{ if .errors }}
|
||||
<h2>Please correct the following errors:</h2>
|
||||
|
||||
<ul>
|
||||
{{ range .errors }}
|
||||
<li>{{ . }}</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ end }}
|
||||
|
||||
<p>
|
||||
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
|
||||
|
@ -26,7 +16,12 @@
|
|||
<form method="POST" action="/admin/setup">
|
||||
<div class="form-group">
|
||||
<label for="setup-admin-username">Admin username:</label>
|
||||
<input type="text" name="username" 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"
|
||||
value="{{ .Form.Username }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="setup-admin-password1">Passphrase:</label>
|
||||
|
|
Loading…
Reference in New Issue
Block a user