Add admin page editor UX
This commit is contained in:
parent
3f7b800423
commit
9eaca9c98b
141
core/admin.go
141
core/admin.go
|
@ -2,8 +2,13 @@ package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/kirsle/blog/core/caches/null"
|
"github.com/kirsle/blog/core/caches/null"
|
||||||
|
@ -18,6 +23,7 @@ func (b *Blog) AdminRoutes(r *mux.Router) {
|
||||||
adminRouter := mux.NewRouter().PathPrefix("/admin").Subrouter().StrictSlash(false)
|
adminRouter := mux.NewRouter().PathPrefix("/admin").Subrouter().StrictSlash(false)
|
||||||
r.HandleFunc("/admin", b.AdminHandler) // so as to not be "/admin/"
|
r.HandleFunc("/admin", b.AdminHandler) // so as to not be "/admin/"
|
||||||
adminRouter.HandleFunc("/settings", b.SettingsHandler)
|
adminRouter.HandleFunc("/settings", b.SettingsHandler)
|
||||||
|
adminRouter.HandleFunc("/editor", b.EditorHandler)
|
||||||
adminRouter.PathPrefix("/").HandlerFunc(b.PageHandler)
|
adminRouter.PathPrefix("/").HandlerFunc(b.PageHandler)
|
||||||
r.PathPrefix("/admin").Handler(negroni.New(
|
r.PathPrefix("/admin").Handler(negroni.New(
|
||||||
negroni.HandlerFunc(b.LoginRequired),
|
negroni.HandlerFunc(b.LoginRequired),
|
||||||
|
@ -30,6 +36,141 @@ func (b *Blog) AdminHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
b.RenderTemplate(w, r, "admin/index", nil)
|
b.RenderTemplate(w, r, "admin/index", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FileTree holds information about files in the document roots.
|
||||||
|
type FileTree struct {
|
||||||
|
UserRoot bool // false = CoreRoot
|
||||||
|
Files []Filepath
|
||||||
|
}
|
||||||
|
|
||||||
|
// EditorHandler lets you edit web pages from the frontend.
|
||||||
|
func (b *Blog) EditorHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Editing a page?
|
||||||
|
file := strings.Trim(r.FormValue("file"), "/")
|
||||||
|
if len(file) > 0 {
|
||||||
|
var (
|
||||||
|
fp string
|
||||||
|
fromCore = r.FormValue("from") == "core"
|
||||||
|
saving = r.FormValue("action") == "save"
|
||||||
|
deleting = r.FormValue("action") == "delete"
|
||||||
|
body = []byte{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Are they saving?
|
||||||
|
if saving {
|
||||||
|
fp = filepath.Join(b.UserRoot, file)
|
||||||
|
body = []byte(r.FormValue("body"))
|
||||||
|
err := ioutil.WriteFile(fp, body, 0644)
|
||||||
|
if err != nil {
|
||||||
|
b.Flash(w, r, "Error saving: %s", err)
|
||||||
|
} else {
|
||||||
|
b.FlashAndRedirect(w, r, "/admin/editor?file="+url.QueryEscape(file), "Page saved successfully!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if deleting {
|
||||||
|
fp = filepath.Join(b.UserRoot, file)
|
||||||
|
err := os.Remove(fp)
|
||||||
|
if err != nil {
|
||||||
|
b.FlashAndRedirect(w, r, "/admin/editor", "Error deleting: %s", err)
|
||||||
|
} else {
|
||||||
|
b.FlashAndRedirect(w, r, "/admin/editor", "Page deleted!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Where is the file from?
|
||||||
|
if fromCore {
|
||||||
|
fp = filepath.Join(b.DocumentRoot, file)
|
||||||
|
} else {
|
||||||
|
fp = filepath.Join(b.UserRoot, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the file. If not found, check from the core root.
|
||||||
|
f, err := os.Stat(fp)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
fp = filepath.Join(b.DocumentRoot, file)
|
||||||
|
fromCore = true
|
||||||
|
f, err = os.Stat(fp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it exists, load it.
|
||||||
|
if !os.IsNotExist(err) && !f.IsDir() {
|
||||||
|
body, err = ioutil.ReadFile(fp)
|
||||||
|
if err != nil {
|
||||||
|
b.Flash(w, r, "Error reading %s: %s", fp, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default HTML boilerplate for .gohtml templates.
|
||||||
|
if len(body) == 0 && strings.HasSuffix(fp, ".gohtml") {
|
||||||
|
body = []byte("{{ define \"title\" }}Untitled Page{{ end }}\n" +
|
||||||
|
"{{ define \"content\" }}\n<h1>Untitled Page</h1>\n\n{{ end }}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
v := NewVars(map[interface{}]interface{}{
|
||||||
|
"File": file,
|
||||||
|
"Path": fp,
|
||||||
|
"Body": string(body),
|
||||||
|
"FromCore": fromCore,
|
||||||
|
})
|
||||||
|
b.RenderTemplate(w, r, "admin/editor", v)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise listing the index view.
|
||||||
|
b.editorFileList(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// editorFileList handles the index view of /admin/editor.
|
||||||
|
func (b *Blog) editorFileList(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Listing the file tree?
|
||||||
|
trees := []FileTree{}
|
||||||
|
for i, root := range []string{b.UserRoot, b.DocumentRoot} {
|
||||||
|
tree := FileTree{
|
||||||
|
UserRoot: i == 0,
|
||||||
|
Files: []Filepath{},
|
||||||
|
}
|
||||||
|
|
||||||
|
filepath.Walk(root, func(path string, f os.FileInfo, err error) error {
|
||||||
|
abs, _ := filepath.Abs(path)
|
||||||
|
rel, _ := filepath.Rel(root, path)
|
||||||
|
|
||||||
|
// Skip hidden files and directories.
|
||||||
|
if f.IsDir() || rel == "." || strings.HasPrefix(rel, ".private") || strings.HasPrefix(rel, "admin/") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only text files.
|
||||||
|
ext := strings.ToLower(filepath.Ext(path))
|
||||||
|
okTypes := []string{
|
||||||
|
".html", ".gohtml", ".md", ".markdown", ".js", ".css", ".jsx",
|
||||||
|
}
|
||||||
|
ok := false
|
||||||
|
for _, ft := range okTypes {
|
||||||
|
if ext == ft {
|
||||||
|
ok = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tree.Files = append(tree.Files, Filepath{
|
||||||
|
Absolute: abs,
|
||||||
|
Relative: rel,
|
||||||
|
Basename: filepath.Base(path),
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
trees = append(trees, tree)
|
||||||
|
}
|
||||||
|
v := NewVars(map[interface{}]interface{}{
|
||||||
|
"FileTrees": trees,
|
||||||
|
})
|
||||||
|
b.RenderTemplate(w, r, "admin/filelist", v)
|
||||||
|
}
|
||||||
|
|
||||||
// SettingsHandler lets you configure the app from the frontend.
|
// SettingsHandler lets you configure the app from the frontend.
|
||||||
func (b *Blog) SettingsHandler(w http.ResponseWriter, r *http.Request) {
|
func (b *Blog) SettingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
v := NewVars()
|
v := NewVars()
|
||||||
|
|
|
@ -70,7 +70,7 @@ func (b *Blog) RenderComments(session *sessions.Session, csrfToken, url, subject
|
||||||
// Look up the author username.
|
// Look up the author username.
|
||||||
if c.UserID > 0 {
|
if c.UserID > 0 {
|
||||||
if _, ok := userMap[c.UserID]; !ok {
|
if _, ok := userMap[c.UserID]; !ok {
|
||||||
if user, err := users.Load(c.UserID); err == nil {
|
if user, err2 := users.Load(c.UserID); err2 == nil {
|
||||||
userMap[c.UserID] = user
|
userMap[c.UserID] = user
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -220,7 +220,11 @@ func (b *Blog) CommentHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append their comment.
|
// Append their comment.
|
||||||
t.Post(c)
|
err := t.Post(c)
|
||||||
|
if err != nil {
|
||||||
|
b.FlashAndRedirect(w, r, c.OriginURL, "Error posting comment: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
b.NotifyComment(c)
|
b.NotifyComment(c)
|
||||||
|
|
||||||
// Are they subscribing to future comments?
|
// Are they subscribing to future comments?
|
||||||
|
@ -236,6 +240,7 @@ func (b *Blog) CommentHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b.FlashAndRedirect(w, r, c.OriginURL, "Comment posted!")
|
b.FlashAndRedirect(w, r, c.OriginURL, "Comment posted!")
|
||||||
|
log.Info("t: %v", t.Comments)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -164,7 +164,11 @@ func (db *DB) ListAll(path string) ([]string, error) {
|
||||||
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))
|
||||||
parts = parts[:len(parts)-1] // pop off the filename
|
parts = parts[:len(parts)-1] // pop off the filename
|
||||||
directory := "/" + filepath.Join(parts...)
|
directory, err := filepath.Abs(filepath.Join(parts...))
|
||||||
|
if err != nil {
|
||||||
|
log.Error("[JsonDB] Couldn't get abs path to %s", path)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := os.Stat(directory); err != nil {
|
if _, err := os.Stat(directory); err != nil {
|
||||||
log.Debug("[JsonDB] Create directory: %s", directory)
|
log.Debug("[JsonDB] Create directory: %s", directory)
|
||||||
|
|
|
@ -23,6 +23,7 @@ type Vars struct {
|
||||||
LoggedIn bool
|
LoggedIn bool
|
||||||
CurrentUser *users.User
|
CurrentUser *users.User
|
||||||
CSRF string
|
CSRF string
|
||||||
|
Editable bool // page is editable
|
||||||
Request *http.Request
|
Request *http.Request
|
||||||
RequestTime time.Time
|
RequestTime time.Time
|
||||||
RequestDuration time.Duration
|
RequestDuration time.Duration
|
||||||
|
@ -125,6 +126,9 @@ func (b *Blog) RenderPartialTemplate(w io.Writer, path string, v interface{}, wi
|
||||||
"RenderIndex": b.RenderIndex,
|
"RenderIndex": b.RenderIndex,
|
||||||
"RenderPost": b.RenderPost,
|
"RenderPost": b.RenderPost,
|
||||||
"RenderTags": b.RenderTags,
|
"RenderTags": b.RenderTags,
|
||||||
|
"TemplateName": func() string {
|
||||||
|
return filepath.URI
|
||||||
|
},
|
||||||
}
|
}
|
||||||
if functions != nil {
|
if functions != nil {
|
||||||
for name, fn := range functions {
|
for name, fn := range functions {
|
||||||
|
@ -181,6 +185,7 @@ func (b *Blog) RenderTemplate(w http.ResponseWriter, r *http.Request, path strin
|
||||||
|
|
||||||
vars.RequestDuration = time.Now().Sub(vars.RequestTime)
|
vars.RequestDuration = time.Now().Sub(vars.RequestTime)
|
||||||
vars.CSRF = b.GenerateCSRFToken(w, r, session)
|
vars.CSRF = b.GenerateCSRFToken(w, r, session)
|
||||||
|
vars.Editable = !strings.HasPrefix(path, "admin/")
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html; encoding=UTF-8")
|
w.Header().Set("Content-Type", "text/html; encoding=UTF-8")
|
||||||
b.RenderPartialTemplate(w, path, vars, true, template.FuncMap{
|
b.RenderPartialTemplate(w, path, vars, true, template.FuncMap{
|
||||||
|
|
|
@ -3,4 +3,15 @@
|
||||||
<h1>404 Not Found</h1>
|
<h1>404 Not Found</h1>
|
||||||
|
|
||||||
{{ .Message }}
|
{{ .Message }}
|
||||||
|
|
||||||
|
{{ if .CurrentUser.Admin }}
|
||||||
|
<p>
|
||||||
|
<strong>Admin:</strong> create a
|
||||||
|
<a href="/admin/editor?file={{ .Request.URL.Path }}.md">Markdown</a>
|
||||||
|
or
|
||||||
|
<a href="/admin/editor?file={{ .Request.URL.Path }}.gohtml">HTML</a>
|
||||||
|
page here.
|
||||||
|
</p>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
@ -78,6 +78,12 @@
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
{{ template "content" . }}
|
{{ template "content" . }}
|
||||||
|
|
||||||
|
{{ if and .CurrentUser.Admin .Editable }}
|
||||||
|
<p class="mt-4">
|
||||||
|
<strong>Admin:</strong> [<a href="/admin/editor?file={{ TemplateName }}">edit this page</a>]
|
||||||
|
</p>
|
||||||
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-3">
|
<div class="col-3">
|
||||||
|
|
||||||
|
@ -95,7 +101,7 @@
|
||||||
<h4 class="cart-title">Control Center</h4>
|
<h4 class="cart-title">Control Center</h4>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Logged in as: <a href="/u/{{ .CurrentUser.Username }}">{{ .CurrentUser.Username }}</a>
|
Logged in as: {{ .CurrentUser.Username }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled">
|
||||||
|
|
10
root/about.md
Normal file
10
root/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.
|
48
root/admin/editor.gohtml
Normal file
48
root/admin/editor.gohtml
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
{{ define "title" }}Page Editor{{ end }}
|
||||||
|
{{ define "content" }}
|
||||||
|
<h1>Edit: {{ .Data.File }}</h1>
|
||||||
|
|
||||||
|
{{ if .Data.FromCore }}
|
||||||
|
<p>
|
||||||
|
<strong>Note:</strong> this page is from the blog core root, so changes
|
||||||
|
made will be saved to your user root instead.
|
||||||
|
</p>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<form action="/admin/editor" method="POST">
|
||||||
|
<input type="hidden" name="_csrf" value="{{ .CSRF }}">
|
||||||
|
<input type="hidden" name="save" value="true">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="file">
|
||||||
|
Filepath:
|
||||||
|
</label>
|
||||||
|
<input type="text"
|
||||||
|
size="40"
|
||||||
|
id="file"
|
||||||
|
name="file"
|
||||||
|
value="{{ .Data.File }}"
|
||||||
|
placeholder="path/to/file.gohtml"
|
||||||
|
class="form-control"
|
||||||
|
required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="body">
|
||||||
|
Content:
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
cols="40"
|
||||||
|
rows="12"
|
||||||
|
id="body"
|
||||||
|
name="body"
|
||||||
|
class="form-control"
|
||||||
|
required>{{ .Data.Body }}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<button type="submit" name="action" value="save" class="btn btn-primary">Save Page</button>
|
||||||
|
<button type="submit" name="action" value="delete" class="btn btn-danger" onClick="return window.confirm('Are you sure?')">Delete Page</button>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
{{ end }}
|
34
root/admin/filelist.gohtml
Normal file
34
root/admin/filelist.gohtml
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
{{ define "title" }}Page Editor{{ end }}
|
||||||
|
{{ define "content" }}
|
||||||
|
<h1>Page Editor</h1>
|
||||||
|
|
||||||
|
{{ range .Data.FileTrees }}
|
||||||
|
{{ if .UserRoot }}
|
||||||
|
<h2>User Root</h2>
|
||||||
|
<p>
|
||||||
|
These are your custom web files that override those in the CoreRoot.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{{ range .Files }}
|
||||||
|
<li><a href="/admin/editor?file={{ .Relative }}">{{ .Relative }}</a></li>
|
||||||
|
{{ end }}
|
||||||
|
</ul>
|
||||||
|
{{ else }}
|
||||||
|
<h2>Core Root</h2>
|
||||||
|
<p>
|
||||||
|
These are the blog's built-in web files. If you edit them, your
|
||||||
|
changes will be saved into your User Root to override the file
|
||||||
|
from the Core Root.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{{ range .Files }}
|
||||||
|
<li><a href="/admin/editor?file={{ .Relative }}&from=core">{{ .Relative }}</a></li>
|
||||||
|
{{ end }}
|
||||||
|
</ul>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
</form>
|
||||||
|
{{ end }}
|
|
@ -23,7 +23,7 @@
|
||||||
(updated {{ $p.Updated.Format "January 2, 2006" }})
|
(updated {{ $p.Updated.Format "January 2, 2006" }})
|
||||||
</span>
|
</span>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
by <a href="/u/{{ $a.Username }}">{{ or $a.Name $a.Username }}</a>
|
by {{ or $a.Name $a.Username }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="markdown mb-4">
|
<div class="markdown mb-4">
|
||||||
|
|
|
@ -2,16 +2,16 @@
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="markdown col-12 col-lg-1 mb-1">
|
<div class="markdown col-12 col-lg-2 mb-1">
|
||||||
<img src="{{ .Avatar }}"
|
<img src="{{ .Avatar }}"
|
||||||
width="96"
|
width="96"
|
||||||
height="96"
|
height="96"
|
||||||
alt="Avatar image">
|
alt="Avatar image">
|
||||||
</div>
|
</div>
|
||||||
<div class="markdown col-12 col-lg-11">
|
<div class="markdown col-12 col-lg-10">
|
||||||
<div class="comment-meta">
|
<div class="comment-meta">
|
||||||
{{ if and .UserID .Username }}
|
{{ if and .UserID .Username }}
|
||||||
<a href="/u/{{ .Username }}"><strong>{{ or .Name "Anonymous" }}</strong></a>
|
<strong>{{ or .Name "Anonymous" }}</strong> (@{{ .Username }})
|
||||||
{{ else }}
|
{{ else }}
|
||||||
<strong>{{ or .Name "Anonymous" }}</strong>
|
<strong>{{ or .Name "Anonymous" }}</strong>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user