CSRF, Initial Setup and Admin User Creation
This commit is contained in:
parent
c4cc4ba854
commit
9348050b4c
2
go.mod
2
go.mod
|
@ -6,5 +6,7 @@ require (
|
|||
github.com/gorilla/mux v1.7.3
|
||||
github.com/jinzhu/gorm v1.9.11
|
||||
github.com/kirsle/blog v0.0.0-20191022175051-d78814b9c99b
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
github.com/urfave/negroni v1.0.0
|
||||
golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443
|
||||
)
|
||||
|
|
6
go.sum
6
go.sum
|
@ -35,15 +35,19 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z
|
|||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
|
||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
|
||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
||||
github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU=
|
||||
github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
|
@ -88,6 +92,8 @@ github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R
|
|||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
|
||||
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0=
|
||||
github.com/shurcooL/highlight_diff v0.0.0-20181222201841-111da2e7d480/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU=
|
||||
|
|
8
pkg/constants/constants.go
Normal file
8
pkg/constants/constants.go
Normal file
|
@ -0,0 +1,8 @@
|
|||
package constants
|
||||
|
||||
// Misc constants.
|
||||
const (
|
||||
// Password values
|
||||
PasswordMinLength = 8
|
||||
BcryptCost = 14
|
||||
)
|
7
pkg/constants/csrf.go
Normal file
7
pkg/constants/csrf.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package constants
|
||||
|
||||
// CSRF protection constants.
|
||||
const (
|
||||
CSRFCookieName = "csrf_token"
|
||||
CSRFFormName = "_csrf"
|
||||
)
|
|
@ -9,16 +9,6 @@ import (
|
|||
)
|
||||
|
||||
func init() {
|
||||
glue.Register(glue.Endpoint{
|
||||
Path: "/about",
|
||||
Middleware: []mux.MiddlewareFunc{
|
||||
middleware.ExampleMiddleware,
|
||||
},
|
||||
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("About Site"))
|
||||
},
|
||||
})
|
||||
|
||||
glue.Register(glue.Endpoint{
|
||||
Path: "/admin",
|
||||
Middleware: []mux.MiddlewareFunc{
|
||||
|
|
|
@ -1,22 +1,73 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"git.kirsle.net/apps/gophertype/pkg/constants"
|
||||
"git.kirsle.net/apps/gophertype/pkg/glue"
|
||||
"git.kirsle.net/apps/gophertype/pkg/middleware"
|
||||
"git.kirsle.net/apps/gophertype/pkg/models"
|
||||
"git.kirsle.net/apps/gophertype/pkg/responses"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func init() {
|
||||
glue.Register(glue.Endpoint{
|
||||
Path: "/admin/setup",
|
||||
Path: "/admin/setup",
|
||||
Methods: []string{"GET", "POST"},
|
||||
Middleware: []mux.MiddlewareFunc{
|
||||
middleware.ExampleMiddleware,
|
||||
},
|
||||
Handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
responses.RenderTemplate(w, "_builtin/initial_setup.gohtml", nil)
|
||||
// See if we already have an admin account.
|
||||
if _, err := models.FirstAdmin(); err == nil {
|
||||
responses.Panic(w, http.StatusForbidden, "This site is already initialized.")
|
||||
return
|
||||
}
|
||||
|
||||
// Template variables.
|
||||
v := map[string]interface{}{}
|
||||
|
||||
// POST handler: create the admin account.
|
||||
if r.Method == http.MethodPost {
|
||||
var (
|
||||
username = r.FormValue("username")
|
||||
displayName = r.FormValue("name")
|
||||
password = r.FormValue("password")
|
||||
password2 = r.FormValue("password2")
|
||||
)
|
||||
|
||||
// Username and display name validation happens in CreateUser.
|
||||
// Validate the passwords match here.
|
||||
if len(password) < constants.PasswordMinLength {
|
||||
v["Error"] = fmt.Errorf("your password is too short (must be %d+ characters)", constants.PasswordMinLength)
|
||||
}
|
||||
if password != password2 {
|
||||
v["Error"] = errors.New("your passwords don't match")
|
||||
} else {
|
||||
admin := models.User{
|
||||
Username: username,
|
||||
Name: displayName,
|
||||
IsAdmin: true,
|
||||
}
|
||||
admin.SetPassword(password)
|
||||
|
||||
if err := models.CreateUser(admin); err != nil {
|
||||
v["Error"] = err
|
||||
} else {
|
||||
// Admin created! Make the default config.
|
||||
cfg := models.GetSettings()
|
||||
cfg.Save()
|
||||
|
||||
w.Write([]byte("Success"))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
responses.RenderTemplate(w, r, "_builtin/initial_setup.gohtml", v)
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ func CatchAllHandler(w http.ResponseWriter, r *http.Request) {
|
|||
// Resolve the target path.
|
||||
filepath, err := responses.ResolveFile(path)
|
||||
if err != nil {
|
||||
responses.Panic(w, http.StatusNotFound, "ResolveFile: "+err.Error())
|
||||
responses.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -31,7 +31,7 @@ func CatchAllHandler(w http.ResponseWriter, r *http.Request) {
|
|||
// Is it a Go template?
|
||||
if strings.HasSuffix(filepath, ".gohtml") {
|
||||
log.Printf("Resolved to Go Template path %s", filepath)
|
||||
responses.RenderTemplate(w, filepath, nil)
|
||||
responses.RenderTemplate(w, r, filepath, nil)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
48
pkg/middleware/csrf.go
Normal file
48
pkg/middleware/csrf.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.kirsle.net/apps/gophertype/pkg/constants"
|
||||
"git.kirsle.net/apps/gophertype/pkg/responses"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
// CSRF prevents Cross-Site Request Forgery.
|
||||
// All "POST" requests are required to have an "_csrf" variable passed in which
|
||||
// matches the "csrf_token" HTTP cookie with their request.
|
||||
func CSRF(next http.Handler) http.Handler {
|
||||
middleware := func(w http.ResponseWriter, r *http.Request) {
|
||||
// All requests: verify they have a CSRF cookie, create one if not.
|
||||
var token string
|
||||
cookie, err := r.Cookie(constants.CSRFCookieName)
|
||||
if err == nil {
|
||||
token = cookie.Value
|
||||
}
|
||||
|
||||
// Generate a token cookie if not found.
|
||||
if len(token) < 8 || err != nil {
|
||||
token = uuid.NewV4().String()
|
||||
cookie = &http.Cookie{
|
||||
Name: constants.CSRFCookieName,
|
||||
Value: token,
|
||||
Expires: time.Now().Add(24 * time.Hour),
|
||||
}
|
||||
http.SetCookie(w, cookie)
|
||||
}
|
||||
|
||||
// POST requests: verify token from form parameter.
|
||||
if r.Method == http.MethodPost {
|
||||
compare := r.FormValue(constants.CSRFFormName)
|
||||
if compare != token {
|
||||
responses.Panic(w, http.StatusForbidden, "CSRF token failure.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(middleware)
|
||||
}
|
72
pkg/models/app_settings.go
Normal file
72
pkg/models/app_settings.go
Normal file
|
@ -0,0 +1,72 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
// AppSetting singleton holds the app configuration.
|
||||
type AppSetting struct {
|
||||
gorm.Model
|
||||
|
||||
// Site information
|
||||
Title string
|
||||
Description string
|
||||
Email string // primary email for notifications
|
||||
NSFW bool
|
||||
BaseURL string
|
||||
|
||||
// Blog settings
|
||||
PostsPerPage int
|
||||
|
||||
// Mail settings
|
||||
MailEnabled bool
|
||||
MailSender string
|
||||
MailHost string
|
||||
MailPort int
|
||||
MailUsername string
|
||||
MailPassword string
|
||||
|
||||
// Security
|
||||
SecretKey string `json:"-"`
|
||||
}
|
||||
|
||||
// GetSettings gets or creates the App Settings.
|
||||
func GetSettings() AppSetting {
|
||||
var s AppSetting
|
||||
r := DB.First(&s)
|
||||
if r.Error != nil {
|
||||
s = AppSetting{
|
||||
Title: "Untitled Site",
|
||||
Description: "Just another web blog.",
|
||||
SecretKey: MakeSecretKey(),
|
||||
}
|
||||
}
|
||||
|
||||
if s.SecretKey == "" {
|
||||
s.SecretKey = MakeSecretKey()
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// Save the settings to DB.
|
||||
func (s AppSetting) Save() error {
|
||||
if DB.NewRecord(s) {
|
||||
r := DB.Create(&s)
|
||||
return r.Error
|
||||
}
|
||||
r := DB.Save(&s)
|
||||
return r.Error
|
||||
}
|
||||
|
||||
// MakeSecretKey generates a secret key for signing HTTP cookies.
|
||||
func MakeSecretKey() string {
|
||||
keyLength := 32
|
||||
|
||||
b := make([]byte, keyLength)
|
||||
rand.Read(b)
|
||||
return base64.URLEncoding.EncodeToString(b)
|
||||
}
|
|
@ -8,5 +8,6 @@ var DB *gorm.DB
|
|||
// UseDB registers a database driver.
|
||||
func UseDB(db *gorm.DB) {
|
||||
DB = db
|
||||
DB.AutoMigrate(&AppSetting{})
|
||||
DB.AutoMigrate(&User{})
|
||||
}
|
||||
|
|
|
@ -1,11 +1,67 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.kirsle.net/apps/gophertype/pkg/constants"
|
||||
"github.com/jinzhu/gorm"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// User account for the site.
|
||||
type User struct {
|
||||
ID int `json:"id"`
|
||||
Username string `json:"username" gorm:"unique"`
|
||||
Password string `json:"-"`
|
||||
IsAdmin bool `json:"isAdmin"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
gorm.Model
|
||||
Username string `json:"username" gorm:"unique_index"`
|
||||
HashedPassword string `json:"-"`
|
||||
IsAdmin bool `json:"isAdmin" gorm:"index"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email" gorm:"index"`
|
||||
}
|
||||
|
||||
// Validate the User object has everything filled in. Fixes what it can,
|
||||
// returns an error if something is wrong. Ensures the HashedPassword is hashed.
|
||||
func (u *User) Validate() error {
|
||||
u.Username = strings.TrimSpace(strings.ToLower(u.Username))
|
||||
u.Name = strings.TrimSpace(strings.ToLower(u.Name))
|
||||
|
||||
// Defaults
|
||||
if len(u.Name) == 0 {
|
||||
u.Name = u.Username
|
||||
}
|
||||
|
||||
if len(u.Username) == 0 {
|
||||
return errors.New("username is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetPassword stores the hashed password for a user.
|
||||
func (u *User) SetPassword(password string) error {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), constants.BcryptCost)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SetPassword: %s", err)
|
||||
}
|
||||
|
||||
u.HashedPassword = string(hash)
|
||||
fmt.Printf("Set hashed password: %s", u.HashedPassword)
|
||||
return nil
|
||||
}
|
||||
|
||||
// FirstAdmin returns the admin user with the lowest ID number.
|
||||
func FirstAdmin() (User, error) {
|
||||
var user User
|
||||
r := DB.First(&user, "is_admin", true)
|
||||
return user, r.Error
|
||||
}
|
||||
|
||||
// CreateUser adds a new user to the database.
|
||||
func CreateUser(u User) error {
|
||||
if err := u.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r := DB.Create(&u)
|
||||
return r.Error
|
||||
}
|
||||
|
|
|
@ -1,9 +1,33 @@
|
|||
package responses
|
||||
|
||||
import "net/http"
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Panic gives a simple error with no template or anything fancy.
|
||||
func Panic(w http.ResponseWriter, code int, message string) {
|
||||
w.WriteHeader(code)
|
||||
w.Write([]byte(message))
|
||||
}
|
||||
|
||||
// Error returns an error page.
|
||||
func Error(w http.ResponseWriter, r *http.Request, code int, message string) {
|
||||
v := map[string]interface{}{
|
||||
"Message": message,
|
||||
}
|
||||
w.WriteHeader(code)
|
||||
|
||||
if err := RenderTemplate(w, r, "_builtin/errors/generic.gohtml", v); err != nil {
|
||||
log.Printf("responses.Error: failed to render a pretty error template: %s", err)
|
||||
w.Write([]byte(message))
|
||||
}
|
||||
}
|
||||
|
||||
// NotFound returns an HTML 404 page.
|
||||
func NotFound(w http.ResponseWriter, r *http.Request) {
|
||||
if err := RenderTemplate(w, r, "_builtin/errors/404.gohtml", nil); err != nil {
|
||||
log.Printf("responses.NotFound: failed to render a pretty error template: %s", err)
|
||||
w.Write([]byte("Not Found"))
|
||||
}
|
||||
}
|
||||
|
|
27
pkg/responses/template_functions.go
Normal file
27
pkg/responses/template_functions.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package responses
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"git.kirsle.net/apps/gophertype/pkg/constants"
|
||||
)
|
||||
|
||||
// TemplateFuncs available to all templates.
|
||||
func TemplateFuncs(r *http.Request) template.FuncMap {
|
||||
return template.FuncMap{
|
||||
"CSRF": func() template.HTML {
|
||||
fmt.Println("CSRF() func called")
|
||||
token, _ := r.Cookie(constants.CSRFCookieName)
|
||||
return template.HTML(fmt.Sprintf(
|
||||
`<input type="hidden" name="%s" value="%s">`,
|
||||
constants.CSRFFormName,
|
||||
token.Value,
|
||||
))
|
||||
},
|
||||
"TestFunction": func() template.HTML {
|
||||
return template.HTML("Testing")
|
||||
},
|
||||
}
|
||||
}
|
|
@ -87,21 +87,22 @@ func ResolveFile(path string) (string, error) {
|
|||
|
||||
// RenderTemplate renders a Go HTML template.
|
||||
// The io.Writer can be an http.ResponseWriter.
|
||||
func RenderTemplate(w http.ResponseWriter, tmpl string, vars interface{}) error {
|
||||
func RenderTemplate(w http.ResponseWriter, r *http.Request, tmpl string, vars interface{}) error {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
|
||||
// Look for the built-in template.
|
||||
if b, err := GetFile(tmpl); err == nil {
|
||||
t, err := template.New(tmpl).Parse(string(b))
|
||||
t, err := template.New(tmpl).Funcs(TemplateFuncs(r)).Parse(string(b))
|
||||
if err != nil {
|
||||
return fmt.Errorf("bundled template '%s': %s", tmpl, err)
|
||||
log.Printf("bundled template '%s': %s", tmpl, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// We found the template. Can we find the layout html?
|
||||
if layout, err := GetFile(".layout.gohtml"); err == nil {
|
||||
_, err := t.New("layout").Parse(string(layout))
|
||||
if err != nil {
|
||||
fmt.Errorf("RenderTemplate(.layout.gohtml): %s", err)
|
||||
log.Printf("RenderTemplate(.layout.gohtml): %s", err)
|
||||
}
|
||||
} else {
|
||||
log.Printf("RenderTemplate: .layout.gohtml not found to wrap %s", tmpl)
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
"git.kirsle.net/apps/gophertype/pkg/controllers"
|
||||
"git.kirsle.net/apps/gophertype/pkg/glue"
|
||||
"git.kirsle.net/apps/gophertype/pkg/middleware"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
|
@ -14,6 +15,8 @@ import (
|
|||
func (s *Site) SetupRouter() error {
|
||||
router := mux.NewRouter()
|
||||
|
||||
router.Use(middleware.CSRF)
|
||||
|
||||
for _, route := range glue.GetControllers() {
|
||||
log.Printf("Register: %+v", route)
|
||||
if len(route.Methods) == 0 {
|
||||
|
|
11
pvt-www/_builtin/error.gohtml
Normal file
11
pvt-www/_builtin/error.gohtml
Normal file
|
@ -0,0 +1,11 @@
|
|||
{{ define "title" }}An error has occurred{{ end }}
|
||||
{{ define "content" }}
|
||||
<h1>An error has occurred</h1>
|
||||
|
||||
{{ if .Message }}
|
||||
<div class="banner banner-danger">
|
||||
{{ .Message }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ end }}
|
6
pvt-www/_builtin/errors/404.gohtml
Normal file
6
pvt-www/_builtin/errors/404.gohtml
Normal file
|
@ -0,0 +1,6 @@
|
|||
{{ define "title" }}Not Found{{ end }}
|
||||
{{ define "content" }}
|
||||
<h1>Not Found</h1>
|
||||
|
||||
The page you were looking for was not found.
|
||||
{{ end }}
|
|
@ -1,5 +1,66 @@
|
|||
{{ define "title" }}Initial Setup{{ end }}
|
||||
|
||||
{{ define "content" }}
|
||||
<h1>Initial Setup</h1>
|
||||
|
||||
{{ if .Error }}
|
||||
<div class="banner banner-danger mb-4">
|
||||
<strong>Error:</strong> {{ .Error }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<p>
|
||||
Welcome to Gophertype! Fill out the basic configuration below to set up the app.
|
||||
</p>
|
||||
|
||||
<form method="POST" action="/admin/setup">
|
||||
{{ CSRF }}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
Create Administrator Login
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-6">
|
||||
<label for="username">Username<span class="text-danger">*</span>:</label>
|
||||
<input type="text" class="form-control"
|
||||
name="username"
|
||||
id="username"
|
||||
placeholder="Admin"
|
||||
required>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="username">Display Name:</label>
|
||||
<input type="text" class="form-control"
|
||||
name="name"
|
||||
id="name"
|
||||
placeholder="Soandso">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-6">
|
||||
<label for="password">Password<span class="text-danger">*</span>:</label>
|
||||
<input type="password" class="form-control"
|
||||
name="password"
|
||||
id="password"
|
||||
required>
|
||||
</div>
|
||||
<div class="form-group col-md-6">
|
||||
<label for="confirm">Confirm<span class="text-danger">*</span>:</label>
|
||||
<input type="password" class="form-control"
|
||||
name="password2"
|
||||
id="confirm"
|
||||
required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="col">
|
||||
<button type="submit"
|
||||
class="btn btn-primary">Continue</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{{ end }}
|
||||
|
|
10
pvt-www/about.md
Normal file
10
pvt-www/about.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
# About Blog
|
||||
|
||||
This is a simple web blog and content management system written in Go.
|
||||
|
||||
## Features
|
||||
|
||||
* Web blog
|
||||
* Draft, Private Posts
|
||||
* Page editor
|
||||
* You can edit any page from the front-end.
|
Loading…
Reference in New Issue
Block a user