Sessions, log in-out, app settings GUI
This commit is contained in:
parent
fe84b0c4f1
commit
3d4d69decc
6
Makefile
6
Makefile
|
@ -32,3 +32,9 @@ test:
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
clean:
|
clean:
|
||||||
rm -rf bin dist
|
rm -rf bin dist
|
||||||
|
|
||||||
|
# `make hardclean` cleans EVERY THING, including root/.private, resetting
|
||||||
|
# your database in the local dev environment. Be careful!
|
||||||
|
.PHONY: hardclean
|
||||||
|
hardclean: clean
|
||||||
|
rm -rf root/.private
|
||||||
|
|
|
@ -3,61 +3,34 @@ package core
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/mux"
|
||||||
"github.com/kirsle/blog/core/forms"
|
|
||||||
"github.com/kirsle/blog/core/models/settings"
|
"github.com/kirsle/blog/core/models/settings"
|
||||||
"github.com/kirsle/blog/core/models/users"
|
"github.com/urfave/negroni"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// AdminRoutes attaches the admin routes to the app.
|
||||||
|
func (b *Blog) AdminRoutes(r *mux.Router) {
|
||||||
|
adminRouter := mux.NewRouter().PathPrefix("/admin").Subrouter().StrictSlash(false)
|
||||||
|
r.HandleFunc("/admin", b.AdminHandler) // so as to not be "/admin/"
|
||||||
|
adminRouter.HandleFunc("/settings", b.SettingsHandler)
|
||||||
|
adminRouter.PathPrefix("/").HandlerFunc(b.PageHandler)
|
||||||
|
r.PathPrefix("/admin").Handler(negroni.New(
|
||||||
|
negroni.HandlerFunc(b.LoginRequired),
|
||||||
|
negroni.Wrap(adminRouter),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
// AdminHandler is the admin landing page.
|
// AdminHandler is the admin landing page.
|
||||||
func (b *Blog) AdminHandler(w http.ResponseWriter, r *http.Request) {
|
func (b *Blog) AdminHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
b.RenderTemplate(w, r, "admin/index", nil)
|
b.RenderTemplate(w, r, "admin/index", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetupHandler is the initial blog setup route.
|
// SettingsHandler lets you configure the app from the frontend.
|
||||||
func (b *Blog) SetupHandler(w http.ResponseWriter, r *http.Request) {
|
func (b *Blog) SettingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := &Vars{
|
v := NewVars()
|
||||||
Form: forms.Setup{},
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.Method == "POST" {
|
// Get the current settings.
|
||||||
form := forms.Setup{
|
settings, _ := settings.Load()
|
||||||
Username: r.FormValue("username"),
|
v.Data["s"] = settings
|
||||||
Password: r.FormValue("password"),
|
b.RenderTemplate(w, r, "admin/settings", v)
|
||||||
Confirm: r.FormValue("confirm"),
|
|
||||||
}
|
|
||||||
vars.Form = form
|
|
||||||
err := form.Validate()
|
|
||||||
if err != nil {
|
|
||||||
vars.Error = err
|
|
||||||
} else {
|
|
||||||
// Save the site config.
|
|
||||||
log.Info("Creating default website config file")
|
|
||||||
s := settings.Defaults()
|
|
||||||
s.Save()
|
|
||||||
|
|
||||||
// Re-initialize the cookie store with the new secret key.
|
|
||||||
b.store = sessions.NewCookieStore([]byte(s.Security.SecretKey))
|
|
||||||
|
|
||||||
log.Info("Creating admin account %s", form.Username)
|
|
||||||
user := &users.User{
|
|
||||||
Username: form.Username,
|
|
||||||
Password: form.Password,
|
|
||||||
Admin: true,
|
|
||||||
Name: "Administrator",
|
|
||||||
}
|
|
||||||
err := users.Create(user)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Error: %v", err)
|
|
||||||
vars.Error = err
|
|
||||||
}
|
|
||||||
|
|
||||||
// All set!
|
|
||||||
b.Login(w, r, user)
|
|
||||||
b.Redirect(w, "/admin")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
b.RenderTemplate(w, r, "admin/setup", vars)
|
|
||||||
}
|
}
|
||||||
|
|
25
core/app.go
25
core/app.go
|
@ -53,31 +53,26 @@ func New(documentRoot, userRoot string) *Blog {
|
||||||
|
|
||||||
// Initialize the router.
|
// Initialize the router.
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
blog.r = r
|
r.HandleFunc("/initial-setup", blog.SetupHandler)
|
||||||
|
|
||||||
// Blog setup.
|
|
||||||
r.HandleFunc("/admin/setup", blog.SetupHandler)
|
|
||||||
|
|
||||||
// Admin pages that require a logged-in user.
|
|
||||||
admin := mux.NewRouter()
|
|
||||||
admin.HandleFunc("/admin", blog.AdminHandler)
|
|
||||||
r.PathPrefix("/admin").Handler(negroni.New(
|
|
||||||
negroni.HandlerFunc(blog.LoginRequired),
|
|
||||||
negroni.Wrap(admin),
|
|
||||||
))
|
|
||||||
r.HandleFunc("/login", blog.LoginHandler)
|
r.HandleFunc("/login", blog.LoginHandler)
|
||||||
r.HandleFunc("/logout", blog.LogoutHandler)
|
r.HandleFunc("/logout", blog.LogoutHandler)
|
||||||
r.HandleFunc("/", blog.PageHandler)
|
blog.AdminRoutes(r)
|
||||||
|
|
||||||
|
r.PathPrefix("/").HandlerFunc(blog.PageHandler)
|
||||||
r.NotFoundHandler = http.HandlerFunc(blog.PageHandler)
|
r.NotFoundHandler = http.HandlerFunc(blog.PageHandler)
|
||||||
|
|
||||||
n := negroni.New(
|
n := negroni.New(
|
||||||
negroni.NewRecovery(),
|
negroni.NewRecovery(),
|
||||||
negroni.NewLogger(),
|
negroni.NewLogger(),
|
||||||
|
negroni.HandlerFunc(blog.SessionLoader),
|
||||||
|
negroni.HandlerFunc(blog.AuthMiddleware),
|
||||||
)
|
)
|
||||||
blog.n = n
|
|
||||||
n.Use(negroni.HandlerFunc(blog.AuthMiddleware))
|
|
||||||
n.UseHandler(r)
|
n.UseHandler(r)
|
||||||
|
|
||||||
|
// Keep references handy elsewhere in the app.
|
||||||
|
blog.n = n
|
||||||
|
blog.r = r
|
||||||
|
|
||||||
return blog
|
return blog
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,12 +8,6 @@ import (
|
||||||
"github.com/kirsle/blog/core/models/users"
|
"github.com/kirsle/blog/core/models/users"
|
||||||
)
|
)
|
||||||
|
|
||||||
type key int
|
|
||||||
|
|
||||||
const (
|
|
||||||
userKey key = iota
|
|
||||||
)
|
|
||||||
|
|
||||||
// Login logs the browser in as the given user.
|
// Login logs the browser in as the given user.
|
||||||
func (b *Blog) Login(w http.ResponseWriter, r *http.Request, u *users.User) error {
|
func (b *Blog) Login(w http.ResponseWriter, r *http.Request, u *users.User) error {
|
||||||
session, err := b.store.Get(r, "session") // TODO session name
|
session, err := b.store.Get(r, "session") // TODO session name
|
||||||
|
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Setup is for the initial blog setup page at /admin/setup.
|
// Setup is for the initial blog setup page at /initial-setup.
|
||||||
type Setup struct {
|
type Setup struct {
|
||||||
Username string
|
Username string
|
||||||
Password string
|
Password string
|
||||||
|
|
58
core/initial-setup.go
Normal file
58
core/initial-setup.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
"github.com/kirsle/blog/core/forms"
|
||||||
|
"github.com/kirsle/blog/core/models/settings"
|
||||||
|
"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 := &Vars{
|
||||||
|
Form: forms.Setup{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Method == "POST" {
|
||||||
|
form := forms.Setup{
|
||||||
|
Username: r.FormValue("username"),
|
||||||
|
Password: r.FormValue("password"),
|
||||||
|
Confirm: r.FormValue("confirm"),
|
||||||
|
}
|
||||||
|
vars.Form = form
|
||||||
|
err := form.Validate()
|
||||||
|
if err != nil {
|
||||||
|
vars.Error = err
|
||||||
|
} else {
|
||||||
|
// Save the site config.
|
||||||
|
log.Info("Creating default website config file")
|
||||||
|
s := settings.Defaults()
|
||||||
|
s.Save()
|
||||||
|
|
||||||
|
// Re-initialize the cookie store with the new secret key.
|
||||||
|
b.store = sessions.NewCookieStore([]byte(s.Security.SecretKey))
|
||||||
|
|
||||||
|
log.Info("Creating admin account %s", form.Username)
|
||||||
|
user := &users.User{
|
||||||
|
Username: form.Username,
|
||||||
|
Password: form.Password,
|
||||||
|
Admin: true,
|
||||||
|
Name: "Administrator",
|
||||||
|
}
|
||||||
|
err := users.Create(user)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Error: %v", err)
|
||||||
|
vars.Error = err
|
||||||
|
}
|
||||||
|
|
||||||
|
// All set!
|
||||||
|
b.Login(w, r, user)
|
||||||
|
b.Redirect(w, "/admin")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.RenderTemplate(w, r, "initial-setup", vars)
|
||||||
|
}
|
|
@ -33,7 +33,7 @@ func New(root string) *DB {
|
||||||
|
|
||||||
// Get a document by path and load it into the object `v`.
|
// Get a document by path and load it into the object `v`.
|
||||||
func (db *DB) Get(document string, v interface{}) error {
|
func (db *DB) Get(document string, v interface{}) error {
|
||||||
log.Debug("GET %s", document)
|
log.Debug("[JsonDB] GET %s", document)
|
||||||
if !db.Exists(document) {
|
if !db.Exists(document) {
|
||||||
return ErrNotFound
|
return ErrNotFound
|
||||||
}
|
}
|
||||||
|
@ -56,7 +56,7 @@ func (db *DB) Get(document string, v interface{}) error {
|
||||||
|
|
||||||
// Commit writes a JSON object to the database.
|
// Commit writes a JSON object to the database.
|
||||||
func (db *DB) Commit(document string, v interface{}) error {
|
func (db *DB) Commit(document string, v interface{}) error {
|
||||||
log.Debug("COMMIT %s", document)
|
log.Debug("[JsonDB] COMMIT %s", document)
|
||||||
path := db.toPath(document)
|
path := db.toPath(document)
|
||||||
|
|
||||||
// Ensure the directory tree is ready.
|
// Ensure the directory tree is ready.
|
||||||
|
@ -73,7 +73,7 @@ func (db *DB) Commit(document string, v interface{}) error {
|
||||||
|
|
||||||
// Delete removes a JSON document from the database.
|
// Delete removes a JSON document from the database.
|
||||||
func (db *DB) Delete(document string) error {
|
func (db *DB) Delete(document string) error {
|
||||||
log.Debug("DELETE %s", document)
|
log.Debug("[JsonDB] DELETE %s", document)
|
||||||
path := db.toPath(document)
|
path := db.toPath(document)
|
||||||
|
|
||||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
|
@ -106,15 +106,11 @@ func (db *DB) ListAll(path string) ([]string, error) {
|
||||||
// path: the filesystem path like from toPath().
|
// path: the filesystem path like from toPath().
|
||||||
func (db *DB) makePath(path string) error {
|
func (db *DB) makePath(path string) error {
|
||||||
parts := strings.Split(path, string(filepath.Separator))
|
parts := strings.Split(path, string(filepath.Separator))
|
||||||
log.Debug("%v", parts)
|
|
||||||
parts = parts[:len(parts)-1] // pop off the filename
|
parts = parts[:len(parts)-1] // pop off the filename
|
||||||
log.Debug("%v", parts)
|
|
||||||
directory := filepath.Join(parts...)
|
directory := filepath.Join(parts...)
|
||||||
|
|
||||||
log.Debug("Ensure exists: %s (from orig path %s)", directory, path)
|
|
||||||
|
|
||||||
if _, err := os.Stat(directory); err != nil {
|
if _, err := os.Stat(directory); err != nil {
|
||||||
log.Debug("Create directory: %s", directory)
|
log.Debug("[JsonDB] Create directory: %s", directory)
|
||||||
err = os.MkdirAll(directory, 0755)
|
err = os.MkdirAll(directory, 0755)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,13 +4,46 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
"github.com/kirsle/blog/core/models/users"
|
"github.com/kirsle/blog/core/models/users"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type key int
|
||||||
|
|
||||||
|
const (
|
||||||
|
sessionKey key = iota
|
||||||
|
userKey
|
||||||
|
)
|
||||||
|
|
||||||
|
// SessionLoader gets the Gorilla session store and makes it available on the
|
||||||
|
// Request context.
|
||||||
|
func (b *Blog) SessionLoader(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||||
|
session, _ := b.store.Get(r, "session")
|
||||||
|
|
||||||
|
log.Debug("REQUEST START: %s %s", r.Method, r.URL.Path)
|
||||||
|
ctx := context.WithValue(r.Context(), sessionKey, session)
|
||||||
|
next(w, r.WithContext(ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session returns the current request's session.
|
||||||
|
func (b *Blog) Session(r *http.Request) *sessions.Session {
|
||||||
|
ctx := r.Context()
|
||||||
|
if session, ok := ctx.Value(sessionKey).(*sessions.Session); ok {
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Error(
|
||||||
|
"Session(): didn't find session in request context! Getting it " +
|
||||||
|
"from the session store instead.",
|
||||||
|
)
|
||||||
|
session, _ := b.store.Get(r, "session")
|
||||||
|
return session
|
||||||
|
}
|
||||||
|
|
||||||
// AuthMiddleware loads the user's authentication state.
|
// AuthMiddleware loads the user's authentication state.
|
||||||
func (b *Blog) AuthMiddleware(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
func (b *Blog) AuthMiddleware(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||||
session, _ := b.store.Get(r, "session")
|
session := b.Session(r)
|
||||||
log.Info("Session: %v", session.Values)
|
log.Debug("AuthMiddleware() -- session values: %v", session.Values)
|
||||||
if loggedIn, ok := session.Values["logged-in"].(bool); ok && loggedIn {
|
if loggedIn, ok := session.Values["logged-in"].(bool); ok && loggedIn {
|
||||||
// They seem to be logged in. Get their user object.
|
// They seem to be logged in. Get their user object.
|
||||||
id := session.Values["user-id"].(int)
|
id := session.Values["user-id"].(int)
|
||||||
|
@ -23,6 +56,7 @@ func (b *Blog) AuthMiddleware(w http.ResponseWriter, r *http.Request, next http.
|
||||||
|
|
||||||
ctx := context.WithValue(r.Context(), userKey, u)
|
ctx := context.WithValue(r.Context(), userKey, u)
|
||||||
next(w, r.WithContext(ctx))
|
next(w, r.WithContext(ctx))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
next(w, r)
|
next(w, r)
|
||||||
}
|
}
|
||||||
|
@ -33,8 +67,10 @@ func (b *Blog) LoginRequired(w http.ResponseWriter, r *http.Request, next http.H
|
||||||
if user, ok := ctx.Value(userKey).(*users.User); ok {
|
if user, ok := ctx.Value(userKey).(*users.User); ok {
|
||||||
if user.ID > 0 {
|
if user.ID > 0 {
|
||||||
next(w, r)
|
next(w, r)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Info("Redirect away!")
|
||||||
b.Redirect(w, "/login?next="+r.URL.Path)
|
b.Redirect(w, "/login?next="+r.URL.Path)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
// PageHandler is the catch-all route handler, for serving static web pages.
|
// PageHandler is the catch-all route handler, for serving static web pages.
|
||||||
func (b *Blog) PageHandler(w http.ResponseWriter, r *http.Request) {
|
func (b *Blog) PageHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
path := r.URL.Path
|
path := r.URL.Path
|
||||||
|
log.Debug("Catch-all page handler invoked for request URI: %s", path)
|
||||||
|
|
||||||
// Remove trailing slashes by redirecting them away.
|
// Remove trailing slashes by redirecting them away.
|
||||||
if len(path) > 1 && path[len(path)-1] == '/' {
|
if len(path) > 1 && path[len(path)-1] == '/' {
|
||||||
|
@ -65,7 +66,14 @@ func (b *Blog) ResolvePath(path string) (Filepath, error) {
|
||||||
path = strings.TrimPrefix(path, "/")
|
path = strings.TrimPrefix(path, "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("Resolving filepath for URI: %s", path)
|
// If you need to debug this function, edit this block.
|
||||||
|
debug := func(tmpl string, args ...interface{}) {
|
||||||
|
if false {
|
||||||
|
log.Debug(tmpl, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debug("Resolving filepath for URI: %s", path)
|
||||||
for _, root := range []string{b.DocumentRoot, b.UserRoot} {
|
for _, root := range []string{b.DocumentRoot, b.UserRoot} {
|
||||||
if len(root) == 0 {
|
if len(root) == 0 {
|
||||||
continue
|
continue
|
||||||
|
@ -78,11 +86,11 @@ func (b *Blog) ResolvePath(path string) (Filepath, error) {
|
||||||
log.Error("%v", err)
|
log.Error("%v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debug("Expected filepath: %s", absPath)
|
debug("Expected filepath: %s", absPath)
|
||||||
|
|
||||||
// Found an exact hit?
|
// Found an exact hit?
|
||||||
if stat, err := os.Stat(absPath); !os.IsNotExist(err) && !stat.IsDir() {
|
if stat, err := os.Stat(absPath); !os.IsNotExist(err) && !stat.IsDir() {
|
||||||
log.Debug("Exact filepath found: %s", absPath)
|
debug("Exact filepath found: %s", absPath)
|
||||||
return Filepath{path, relPath, absPath}, nil
|
return Filepath{path, relPath, absPath}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,7 +106,7 @@ func (b *Blog) ResolvePath(path string) (Filepath, error) {
|
||||||
for _, suffix := range suffixes {
|
for _, suffix := range suffixes {
|
||||||
test := absPath + suffix
|
test := absPath + suffix
|
||||||
if stat, err := os.Stat(test); !os.IsNotExist(err) && !stat.IsDir() {
|
if stat, err := os.Stat(test); !os.IsNotExist(err) && !stat.IsDir() {
|
||||||
log.Debug("Filepath found via suffix %s: %s", suffix, test)
|
debug("Filepath found via suffix %s: %s", suffix, test)
|
||||||
return Filepath{path + suffix, relPath + suffix, test}, nil
|
return Filepath{path + suffix, relPath + suffix, test}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
|
|
||||||
// Redirect sends an HTTP redirect response.
|
// Redirect sends an HTTP redirect response.
|
||||||
func (b *Blog) Redirect(w http.ResponseWriter, location string) {
|
func (b *Blog) Redirect(w http.ResponseWriter, location string) {
|
||||||
|
log.Error("Redirect: %s", location)
|
||||||
w.Header().Set("Location", location)
|
w.Header().Set("Location", location)
|
||||||
w.WriteHeader(http.StatusFound)
|
w.WriteHeader(http.StatusFound)
|
||||||
}
|
}
|
||||||
|
@ -16,6 +17,7 @@ func (b *Blog) NotFound(w http.ResponseWriter, r *http.Request, message ...strin
|
||||||
message = []string{"The page you were looking for was not found."}
|
message = []string{"The page you were looking for was not found."}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Error("HERE 2")
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
err := b.RenderTemplate(w, r, ".errors/404", &Vars{
|
err := b.RenderTemplate(w, r, ".errors/404", &Vars{
|
||||||
Message: message[0],
|
Message: message[0],
|
||||||
|
@ -28,6 +30,7 @@ func (b *Blog) NotFound(w http.ResponseWriter, r *http.Request, message ...strin
|
||||||
|
|
||||||
// Forbidden sends an HTTP 403 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) {
|
||||||
|
log.Error("HERE 3")
|
||||||
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 {
|
||||||
|
@ -38,6 +41,7 @@ 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) {
|
||||||
|
log.Error("HERE 4")
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
err := b.RenderTemplate(w, r, ".errors/400", &Vars{
|
err := b.RenderTemplate(w, r, ".errors/400", &Vars{
|
||||||
Message: message[0],
|
Message: message[0],
|
||||||
|
|
|
@ -25,9 +25,24 @@ type Vars struct {
|
||||||
Message string
|
Message string
|
||||||
Flash string
|
Flash string
|
||||||
Error error
|
Error error
|
||||||
|
Data map[interface{}]interface{}
|
||||||
Form forms.Form
|
Form forms.Form
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewVars initializes a Vars struct with the custom Data map initialized.
|
||||||
|
// You may pass in an initial value for this map if you want.
|
||||||
|
func NewVars(data ...map[interface{}]interface{}) *Vars {
|
||||||
|
var value map[interface{}]interface{}
|
||||||
|
if len(data) > 0 {
|
||||||
|
value = data[0]
|
||||||
|
} else {
|
||||||
|
value = make(map[interface{}]interface{})
|
||||||
|
}
|
||||||
|
return &Vars{
|
||||||
|
Data: value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// LoadDefaults combines template variables with default, globally available vars.
|
// LoadDefaults combines template variables with default, globally available vars.
|
||||||
func (v *Vars) LoadDefaults(r *http.Request) {
|
func (v *Vars) LoadDefaults(r *http.Request) {
|
||||||
// Get the site settings.
|
// Get the site settings.
|
||||||
|
@ -36,7 +51,7 @@ func (v *Vars) LoadDefaults(r *http.Request) {
|
||||||
s = settings.Defaults()
|
s = settings.Defaults()
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.Initialized == false && !strings.HasPrefix(r.URL.Path, "/admin/setup") {
|
if s.Initialized == false && !strings.HasPrefix(r.URL.Path, "/initial-setup") {
|
||||||
v.SetupNeeded = true
|
v.SetupNeeded = true
|
||||||
}
|
}
|
||||||
v.Title = s.Site.Title
|
v.Title = s.Site.Title
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
{{ define "title" }}Untitled{{ end }}
|
{{ define "title" }}Untitled{{ end }}
|
||||||
|
{{ define "scripts" }}Default Scripts{{ end }}
|
||||||
|
|
||||||
{{ define "layout" }}
|
{{ define "layout" }}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
@ -52,7 +54,7 @@
|
||||||
{{ if .SetupNeeded }}
|
{{ if .SetupNeeded }}
|
||||||
<div class="alert alert-success">
|
<div class="alert alert-success">
|
||||||
Your web blog needs to be set up!
|
Your web blog needs to be set up!
|
||||||
Please <a href="/admin/setup">click here</a> to
|
Please <a href="/initial-setup">click here</a> to
|
||||||
configure your blog.
|
configure your blog.
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
@ -157,6 +159,9 @@
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<script type="text/javascript" src="/js/vue.min.js"></script>
|
||||||
|
{{ template "scripts" or "" }}
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
154
root/admin/settings.gohtml
Normal file
154
root/admin/settings.gohtml
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
{{ define "title" }}Website Settings{{ end }}
|
||||||
|
{{ define "content" }}
|
||||||
|
<form action="/admin/settings" method="POST">
|
||||||
|
<div id="settings-app" class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<ul class="nav nav-tabs card-header-tabs" role="tablist">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#site"
|
||||||
|
:class="{ active: currentTab === 'site'}"
|
||||||
|
v-on:click="currentTab = 'site'">
|
||||||
|
Settings
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<!-- <li class="nav-item">
|
||||||
|
<a class="nav-link" href="#db"
|
||||||
|
:class="{ active: currentTab === 'db'}"
|
||||||
|
v-on:click="currentTab = 'db'">
|
||||||
|
Database
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#security"
|
||||||
|
:class="{ active: currentTab === 'security'}"
|
||||||
|
v-on:click="currentTab = 'security'">
|
||||||
|
Security
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#">Hello</a>
|
||||||
|
</li> -->
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{ with .Data.s }}
|
||||||
|
<div class="card-body" v-if="currentTab === 'site'">
|
||||||
|
<h3>The Basics</h3>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="title">Title</label>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control"
|
||||||
|
name="title" id="title"
|
||||||
|
value="{{ .Site.Title }}"
|
||||||
|
placeholder="Website Title">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="admin-email">Admin Email</label>
|
||||||
|
<small class="text-muted">For getting notifications about comments, etc.</small>
|
||||||
|
<input type="email"
|
||||||
|
class="form-control"
|
||||||
|
name="admin-email" id="admin-email"
|
||||||
|
value="{{ .Site.AdminEmail }}"
|
||||||
|
placeholder="name@domain.com">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Redis Cache</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Using a <a href="https://redis.io/" target="_blank">Redis</a> cache can
|
||||||
|
boost the performance of the JSON database by caching documents in
|
||||||
|
memory instead of always reading from disk.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="form-check">
|
||||||
|
<label class="form-check-label">
|
||||||
|
<input type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
name="redis-enabled"
|
||||||
|
value="true"
|
||||||
|
{{ if .Redis.Enabled }}checked{{ end }}>
|
||||||
|
Enable Redis
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="redis-prefix">Key Prefix</label>
|
||||||
|
<small class="text-muted">(optional)</small>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control"
|
||||||
|
name="redis-prefix" id="redis-prefix"
|
||||||
|
value="{{ .Redis.Prefix }}"
|
||||||
|
placeholder="blog:">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="redis-host">Redis Host</label>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control"
|
||||||
|
name="redis-host" id="redis-host"
|
||||||
|
value="{{ .Redis.Host }}"
|
||||||
|
placeholder="localhost">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="redis-port">Port</label>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control"
|
||||||
|
name="redis-port" id="redis-port"
|
||||||
|
value="{{ .Redis.Port }}"
|
||||||
|
placeholder="6379">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="redis-db">DB Number</label>
|
||||||
|
<small class="text-muted">0-15</small>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control"
|
||||||
|
name="redis-db" id="redis-db"
|
||||||
|
value="{{ .Redis.DB }}"
|
||||||
|
placeholder="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="redis-prefix">Key Prefix</label>
|
||||||
|
<small class="text-muted">(optional)</small>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control"
|
||||||
|
name="redis-prefix" id="redis-prefix"
|
||||||
|
value="{{ .Redis.Prefix }}"
|
||||||
|
placeholder="blog:">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body" v-if="currentTab === 'db'">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body" v-if="currentTab === 'security'">
|
||||||
|
<div class="form-check">
|
||||||
|
<label class="form-check-label">
|
||||||
|
<input type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
name="redis-enabled"
|
||||||
|
value="true"
|
||||||
|
{{ if .Redis.Enabled }}checked{{ end }}>
|
||||||
|
Enable Redis
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="redis-prefix">Key Prefix</label>
|
||||||
|
<small class="text-muted">(optional)</small>
|
||||||
|
<input type="text"
|
||||||
|
class="form-control"
|
||||||
|
name="redis-prefix" id="redis-prefix"
|
||||||
|
value="{{ .Redis.Prefix }}"
|
||||||
|
placeholder="blog:">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{{ end }}
|
||||||
|
{{ define "scripts" }}
|
||||||
|
<script type="text/javascript" src="/admin/settings.js"></script>
|
||||||
|
{{ end }}
|
19
root/admin/settings.js
Normal file
19
root/admin/settings.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
var app = new Vue({
|
||||||
|
el: "#settings-app",
|
||||||
|
data: {
|
||||||
|
currentTab: "site",
|
||||||
|
},
|
||||||
|
mounted: function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var m = window.location.hash.match(/^#(\w+?)$/)
|
||||||
|
if (m) {
|
||||||
|
self.currentTab = m[1];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
load: function() {
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
|
@ -13,7 +13,7 @@
|
||||||
predictable for an attacker to guess.
|
predictable for an attacker to guess.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form method="POST" action="/admin/setup">
|
<form method="POST" action="/initial-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"
|
<input type="text"
|
6
root/js/vue.min.js
vendored
Normal file
6
root/js/vue.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user