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 (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"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)
|
||||
r.HandleFunc("/admin", b.AdminHandler) // so as to not be "/admin/"
|
||||
adminRouter.HandleFunc("/settings", b.SettingsHandler)
|
||||
adminRouter.HandleFunc("/editor", b.EditorHandler)
|
||||
adminRouter.PathPrefix("/").HandlerFunc(b.PageHandler)
|
||||
r.PathPrefix("/admin").Handler(negroni.New(
|
||||
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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (b *Blog) SettingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
v := NewVars()
|
||||
|
|
|
@ -70,7 +70,7 @@ func (b *Blog) RenderComments(session *sessions.Session, csrfToken, url, subject
|
|||
// Look up the author username.
|
||||
if c.UserID > 0 {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -220,7 +220,11 @@ func (b *Blog) CommentHandler(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
// 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)
|
||||
|
||||
// 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!")
|
||||
log.Info("t: %v", t.Comments)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
@ -164,7 +164,11 @@ func (db *DB) ListAll(path string) ([]string, error) {
|
|||
func (db *DB) makePath(path string) error {
|
||||
parts := strings.Split(path, string(filepath.Separator))
|
||||
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 {
|
||||
log.Debug("[JsonDB] Create directory: %s", directory)
|
||||
|
|
|
@ -23,6 +23,7 @@ type Vars struct {
|
|||
LoggedIn bool
|
||||
CurrentUser *users.User
|
||||
CSRF string
|
||||
Editable bool // page is editable
|
||||
Request *http.Request
|
||||
RequestTime time.Time
|
||||
RequestDuration time.Duration
|
||||
|
@ -125,6 +126,9 @@ func (b *Blog) RenderPartialTemplate(w io.Writer, path string, v interface{}, wi
|
|||
"RenderIndex": b.RenderIndex,
|
||||
"RenderPost": b.RenderPost,
|
||||
"RenderTags": b.RenderTags,
|
||||
"TemplateName": func() string {
|
||||
return filepath.URI
|
||||
},
|
||||
}
|
||||
if functions != nil {
|
||||
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.CSRF = b.GenerateCSRFToken(w, r, session)
|
||||
vars.Editable = !strings.HasPrefix(path, "admin/")
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; encoding=UTF-8")
|
||||
b.RenderPartialTemplate(w, path, vars, true, template.FuncMap{
|
||||
|
|
|
@ -3,4 +3,15 @@
|
|||
<h1>404 Not Found</h1>
|
||||
|
||||
{{ .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 }}
|
||||
|
|
|
@ -78,6 +78,12 @@
|
|||
{{ end }}
|
||||
|
||||
{{ 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 class="col-3">
|
||||
|
||||
|
@ -95,7 +101,7 @@
|
|||
<h4 class="cart-title">Control Center</h4>
|
||||
|
||||
<p>
|
||||
Logged in as: <a href="/u/{{ .CurrentUser.Username }}">{{ .CurrentUser.Username }}</a>
|
||||
Logged in as: {{ .CurrentUser.Username }}
|
||||
</p>
|
||||
|
||||
<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" }})
|
||||
</span>
|
||||
{{ end }}
|
||||
by <a href="/u/{{ $a.Username }}">{{ or $a.Name $a.Username }}</a>
|
||||
by {{ or $a.Name $a.Username }}
|
||||
</div>
|
||||
|
||||
<div class="markdown mb-4">
|
||||
|
|
|
@ -2,16 +2,16 @@
|
|||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<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 }}"
|
||||
width="96"
|
||||
height="96"
|
||||
alt="Avatar image">
|
||||
</div>
|
||||
<div class="markdown col-12 col-lg-11">
|
||||
<div class="markdown col-12 col-lg-10">
|
||||
<div class="comment-meta">
|
||||
{{ if and .UserID .Username }}
|
||||
<a href="/u/{{ .Username }}"><strong>{{ or .Name "Anonymous" }}</strong></a>
|
||||
<strong>{{ or .Name "Anonymous" }}</strong> (@{{ .Username }})
|
||||
{{ else }}
|
||||
<strong>{{ or .Name "Anonymous" }}</strong>
|
||||
{{ end }}
|
||||
|
|
Loading…
Reference in New Issue
Block a user