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
|
||||
clean:
|
||||
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 (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/kirsle/blog/core/forms"
|
||||
"github.com/gorilla/mux"
|
||||
"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.
|
||||
func (b *Blog) AdminHandler(w http.ResponseWriter, r *http.Request) {
|
||||
b.RenderTemplate(w, r, "admin/index", nil)
|
||||
}
|
||||
|
||||
// SetupHandler is the initial blog setup route.
|
||||
func (b *Blog) SetupHandler(w http.ResponseWriter, r *http.Request) {
|
||||
vars := &Vars{
|
||||
Form: forms.Setup{},
|
||||
}
|
||||
// SettingsHandler lets you configure the app from the frontend.
|
||||
func (b *Blog) SettingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
v := NewVars()
|
||||
|
||||
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, "admin/setup", vars)
|
||||
// Get the current settings.
|
||||
settings, _ := settings.Load()
|
||||
v.Data["s"] = settings
|
||||
b.RenderTemplate(w, r, "admin/settings", v)
|
||||
}
|
||||
|
|
25
core/app.go
25
core/app.go
|
@ -53,31 +53,26 @@ func New(documentRoot, userRoot string) *Blog {
|
|||
|
||||
// Initialize the router.
|
||||
r := mux.NewRouter()
|
||||
blog.r = r
|
||||
|
||||
// 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("/initial-setup", blog.SetupHandler)
|
||||
r.HandleFunc("/login", blog.LoginHandler)
|
||||
r.HandleFunc("/logout", blog.LogoutHandler)
|
||||
r.HandleFunc("/", blog.PageHandler)
|
||||
blog.AdminRoutes(r)
|
||||
|
||||
r.PathPrefix("/").HandlerFunc(blog.PageHandler)
|
||||
r.NotFoundHandler = http.HandlerFunc(blog.PageHandler)
|
||||
|
||||
n := negroni.New(
|
||||
negroni.NewRecovery(),
|
||||
negroni.NewLogger(),
|
||||
negroni.HandlerFunc(blog.SessionLoader),
|
||||
negroni.HandlerFunc(blog.AuthMiddleware),
|
||||
)
|
||||
blog.n = n
|
||||
n.Use(negroni.HandlerFunc(blog.AuthMiddleware))
|
||||
n.UseHandler(r)
|
||||
|
||||
// Keep references handy elsewhere in the app.
|
||||
blog.n = n
|
||||
blog.r = r
|
||||
|
||||
return blog
|
||||
}
|
||||
|
||||
|
|
|
@ -8,12 +8,6 @@ import (
|
|||
"github.com/kirsle/blog/core/models/users"
|
||||
)
|
||||
|
||||
type key int
|
||||
|
||||
const (
|
||||
userKey key = iota
|
||||
)
|
||||
|
||||
// Login logs the browser in as the given user.
|
||||
func (b *Blog) Login(w http.ResponseWriter, r *http.Request, u *users.User) error {
|
||||
session, err := b.store.Get(r, "session") // TODO session name
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"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 {
|
||||
Username 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`.
|
||||
func (db *DB) Get(document string, v interface{}) error {
|
||||
log.Debug("GET %s", document)
|
||||
log.Debug("[JsonDB] GET %s", document)
|
||||
if !db.Exists(document) {
|
||||
return ErrNotFound
|
||||
}
|
||||
|
@ -56,7 +56,7 @@ func (db *DB) Get(document string, v interface{}) error {
|
|||
|
||||
// Commit writes a JSON object to the database.
|
||||
func (db *DB) Commit(document string, v interface{}) error {
|
||||
log.Debug("COMMIT %s", document)
|
||||
log.Debug("[JsonDB] COMMIT %s", document)
|
||||
path := db.toPath(document)
|
||||
|
||||
// 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.
|
||||
func (db *DB) Delete(document string) error {
|
||||
log.Debug("DELETE %s", document)
|
||||
log.Debug("[JsonDB] DELETE %s", document)
|
||||
path := db.toPath(document)
|
||||
|
||||
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().
|
||||
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)
|
||||
log.Debug("[JsonDB] Create directory: %s", directory)
|
||||
err = os.MkdirAll(directory, 0755)
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -4,13 +4,46 @@ import (
|
|||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
"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.
|
||||
func (b *Blog) AuthMiddleware(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||
session, _ := b.store.Get(r, "session")
|
||||
log.Info("Session: %v", session.Values)
|
||||
session := b.Session(r)
|
||||
log.Debug("AuthMiddleware() -- session values: %v", session.Values)
|
||||
if loggedIn, ok := session.Values["logged-in"].(bool); ok && loggedIn {
|
||||
// They seem to be logged in. Get their user object.
|
||||
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)
|
||||
next(w, r.WithContext(ctx))
|
||||
return
|
||||
}
|
||||
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.ID > 0 {
|
||||
next(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("Redirect away!")
|
||||
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.
|
||||
func (b *Blog) PageHandler(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
log.Debug("Catch-all page handler invoked for request URI: %s", path)
|
||||
|
||||
// Remove trailing slashes by redirecting them away.
|
||||
if len(path) > 1 && path[len(path)-1] == '/' {
|
||||
|
@ -65,7 +66,14 @@ func (b *Blog) ResolvePath(path string) (Filepath, error) {
|
|||
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} {
|
||||
if len(root) == 0 {
|
||||
continue
|
||||
|
@ -78,11 +86,11 @@ func (b *Blog) ResolvePath(path string) (Filepath, error) {
|
|||
log.Error("%v", err)
|
||||
}
|
||||
|
||||
log.Debug("Expected filepath: %s", absPath)
|
||||
debug("Expected filepath: %s", absPath)
|
||||
|
||||
// Found an exact hit?
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -98,7 +106,7 @@ func (b *Blog) ResolvePath(path string) (Filepath, error) {
|
|||
for _, suffix := range suffixes {
|
||||
test := absPath + suffix
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
|
||||
// Redirect sends an HTTP redirect response.
|
||||
func (b *Blog) Redirect(w http.ResponseWriter, location string) {
|
||||
log.Error("Redirect: %s", location)
|
||||
w.Header().Set("Location", location)
|
||||
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."}
|
||||
}
|
||||
|
||||
log.Error("HERE 2")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
err := b.RenderTemplate(w, r, ".errors/404", &Vars{
|
||||
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.
|
||||
func (b *Blog) Forbidden(w http.ResponseWriter, r *http.Request, message ...string) {
|
||||
log.Error("HERE 3")
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
err := b.RenderTemplate(w, r, ".errors/403", 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.
|
||||
func (b *Blog) BadRequest(w http.ResponseWriter, r *http.Request, message ...string) {
|
||||
log.Error("HERE 4")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
err := b.RenderTemplate(w, r, ".errors/400", &Vars{
|
||||
Message: message[0],
|
||||
|
|
|
@ -25,9 +25,24 @@ type Vars struct {
|
|||
Message string
|
||||
Flash string
|
||||
Error error
|
||||
Data map[interface{}]interface{}
|
||||
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.
|
||||
func (v *Vars) LoadDefaults(r *http.Request) {
|
||||
// Get the site settings.
|
||||
|
@ -36,7 +51,7 @@ func (v *Vars) LoadDefaults(r *http.Request) {
|
|||
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.Title = s.Site.Title
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
{{ define "title" }}Untitled{{ end }}
|
||||
{{ define "scripts" }}Default Scripts{{ end }}
|
||||
|
||||
{{ define "layout" }}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
@ -52,7 +54,7 @@
|
|||
{{ if .SetupNeeded }}
|
||||
<div class="alert alert-success">
|
||||
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.
|
||||
</div>
|
||||
{{ end }}
|
||||
|
@ -157,6 +159,9 @@
|
|||
</div>
|
||||
</footer>
|
||||
|
||||
<script type="text/javascript" src="/js/vue.min.js"></script>
|
||||
{{ template "scripts" or "" }}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
{{ 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.
|
||||
</p>
|
||||
|
||||
<form method="POST" action="/admin/setup">
|
||||
<form method="POST" action="/initial-setup">
|
||||
<div class="form-group">
|
||||
<label for="setup-admin-username">Admin username:</label>
|
||||
<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