Initial form and model layout, user creating/loading
This commit is contained in:
parent
c69dbfebba
commit
6f330a3e92
|
@ -1,55 +1,45 @@
|
||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/kirsle/blog/core/forms"
|
||||||
"github.com/kirsle/blog/core/models/users"
|
"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) {
|
||||||
vars := map[string]interface{}{
|
vars := &Vars{
|
||||||
"errors": []error{},
|
Form: forms.Setup{},
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.Method == "POST" {
|
if r.Method == "POST" {
|
||||||
var errors []error
|
form := forms.Setup{
|
||||||
payload := struct {
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
Confirm string
|
|
||||||
}{
|
|
||||||
Username: r.FormValue("username"),
|
Username: r.FormValue("username"),
|
||||||
Password: r.FormValue("password"),
|
Password: r.FormValue("password"),
|
||||||
Confirm: r.FormValue("confirm"),
|
Confirm: r.FormValue("confirm"),
|
||||||
}
|
}
|
||||||
|
vars.Form = form
|
||||||
// Validate stuff.
|
err := form.Validate()
|
||||||
if len(payload.Username) == 0 {
|
if err != nil {
|
||||||
errors = append(errors, fmt.Errorf("Admin Username is required"))
|
vars.Error = err
|
||||||
}
|
} else {
|
||||||
if len(payload.Password) < 3 {
|
log.Info("Creating admin account %s", form.Username)
|
||||||
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{
|
user := &users.User{
|
||||||
Username: payload.Username,
|
Username: form.Username,
|
||||||
Password: payload.Password,
|
Password: form.Password,
|
||||||
|
Admin: true,
|
||||||
|
Name: "Administrator",
|
||||||
}
|
}
|
||||||
err := b.DB.Commit("users/by-name/"+payload.Username, user)
|
err := users.Create(user)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Error: %v", err)
|
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/gorilla/mux"
|
||||||
"github.com/kirsle/blog/core/jsondb"
|
"github.com/kirsle/blog/core/jsondb"
|
||||||
|
"github.com/kirsle/blog/core/models/users"
|
||||||
"github.com/urfave/negroni"
|
"github.com/urfave/negroni"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -33,6 +34,9 @@ func New(documentRoot, userRoot string) *Blog {
|
||||||
DB: jsondb.New(filepath.Join(userRoot, ".private")),
|
DB: jsondb.New(filepath.Join(userRoot, ".private")),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize all the models.
|
||||||
|
users.DB = blog.DB
|
||||||
|
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
blog.r = r
|
blog.r = r
|
||||||
r.HandleFunc("/admin/setup", blog.SetupHandler)
|
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.
|
// list returns the documents under a path with optional recursion.
|
||||||
func (db *DB) list(path string, recursive bool) ([]string, error) {
|
func (db *DB) list(path string, recursive bool) ([]string, error) {
|
||||||
root := db.toPath(path)
|
root := filepath.Join(db.Root, path)
|
||||||
var docs []string
|
var docs []string
|
||||||
|
|
||||||
files, err := ioutil.ReadDir(root)
|
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
|
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.
|
// User holds information about a user account.
|
||||||
type User struct {
|
type User struct {
|
||||||
models.Base
|
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
|
Admin bool `json:"admin"`
|
||||||
Name string `json:"name"`
|
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 {
|
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)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
err := b.RenderTemplate(w, r, ".errors/404", map[string]interface{}{
|
err := b.RenderTemplate(w, r, ".errors/404", &Vars{
|
||||||
"message": message[0],
|
Message: message[0],
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err.Error())
|
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.
|
// BadRequest sends an HTTP 400 Bad Request.
|
||||||
func (b *Blog) BadRequest(w http.ResponseWriter, r *http.Request, message ...string) {
|
func (b *Blog) BadRequest(w http.ResponseWriter, r *http.Request, message ...string) {
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
err := b.RenderTemplate(w, r, ".errors/400", map[string]interface{}{
|
err := b.RenderTemplate(w, r, ".errors/400", &Vars{
|
||||||
"message": message[0],
|
Message: message[0],
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err.Error())
|
log.Error(err.Error())
|
||||||
|
|
|
@ -3,26 +3,35 @@ package core
|
||||||
import (
|
import (
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/kirsle/blog/core/forms"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultVars combines template variables with default, globally available vars.
|
// Vars is an interface to implement by the templates to pass their own custom
|
||||||
func (b *Blog) DefaultVars(vars map[string]interface{}) map[string]interface{} {
|
// variables in. It auto-loads global template variables (site name, etc.)
|
||||||
defaults := map[string]interface{}{
|
// when the template is rendered.
|
||||||
"title": "Untitled Blog",
|
type Vars struct {
|
||||||
}
|
// Global template variables.
|
||||||
if vars == nil {
|
Title string
|
||||||
return defaults
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, v := range defaults {
|
// Common template variables.
|
||||||
vars[k] = v
|
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.
|
// 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.
|
// Get the layout template.
|
||||||
layout, err := b.ResolvePath(".layout")
|
layout, err := b.ResolvePath(".layout")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -46,7 +55,10 @@ func (b *Blog) RenderTemplate(w http.ResponseWriter, r *http.Request, path strin
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inject globally available variables.
|
// 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")
|
w.Header().Set("Content-Type", "text/html; encoding=UTF-8")
|
||||||
err = t.ExecuteTemplate(w, "layout", vars)
|
err = t.ExecuteTemplate(w, "layout", vars)
|
||||||
|
|
|
@ -2,5 +2,5 @@
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<h1>400 Bad Request</h1>
|
<h1>400 Bad Request</h1>
|
||||||
|
|
||||||
{{ .message }}
|
{{ .Message }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
{{ define "title" }}Forbidden{{ end }}
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<h1>403 Forbidden</h1>
|
<h1>403 Forbidden</h1>
|
||||||
|
|
||||||
|
{{ .Message }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
@ -2,5 +2,5 @@
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<h1>404 Not Found</h1>
|
<h1>404 Not Found</h1>
|
||||||
|
|
||||||
{{ .message }}
|
{{ .Message }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<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 -->
|
<!-- Bootstrap core CSS -->
|
||||||
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
<link rel="stylesheet" href="/css/bootstrap.min.css">
|
||||||
|
@ -15,7 +15,7 @@
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<nav class="navbar navbar-expand-md fixed-top bluez-navbar">
|
<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">
|
<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>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
|
@ -41,7 +41,7 @@
|
||||||
|
|
||||||
<div class="bluez-header">
|
<div class="bluez-header">
|
||||||
<div class="container">
|
<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>
|
<p class="lead bluez-description">Just another web blog.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -49,6 +49,12 @@
|
||||||
<div class="container mb-5">
|
<div class="container mb-5">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-9">
|
<div class="col-9">
|
||||||
|
{{ if .Error }}
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<strong>Error:</strong> {{ .Error }}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
{{ template "content" . }}
|
{{ template "content" . }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-3">
|
<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" }}
|
{{ 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
|
||||||
|
@ -26,7 +16,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" 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>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="setup-admin-password1">Passphrase:</label>
|
<label for="setup-admin-password1">Passphrase:</label>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user