Noah Petherbridge
6d3de7da69
Since most of the `render.Vars{}` fields were hardcoded/not really editable for the templates, apart from .Data, this struct is now locked away in the render subpackage. End http.HandlerFunc's can then make any arbitrary template data structure they want to, available inside the templates as `.Data`.
235 lines
6.5 KiB
Go
235 lines
6.5 KiB
Go
package core
|
|
|
|
import (
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/kirsle/blog/core/internal/forms"
|
|
"github.com/kirsle/blog/core/internal/middleware/auth"
|
|
"github.com/kirsle/blog/core/internal/models/settings"
|
|
"github.com/kirsle/blog/core/internal/render"
|
|
"github.com/kirsle/blog/core/internal/responses"
|
|
"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(true)
|
|
adminRouter.HandleFunc("/", b.AdminHandler)
|
|
adminRouter.HandleFunc("/settings", b.SettingsHandler)
|
|
adminRouter.HandleFunc("/editor", b.EditorHandler)
|
|
// r.HandleFunc("/admin", b.AdminHandler)
|
|
r.PathPrefix("/admin").Handler(negroni.New(
|
|
negroni.HandlerFunc(auth.LoginRequired(b.MustLogin)),
|
|
negroni.Wrap(adminRouter),
|
|
))
|
|
}
|
|
|
|
// AdminHandler is the admin landing page.
|
|
func (b *Blog) AdminHandler(w http.ResponseWriter, r *http.Request) {
|
|
render.Template(w, r, "admin/index", nil)
|
|
}
|
|
|
|
// FileTree holds information about files in the document roots.
|
|
type FileTree struct {
|
|
UserRoot bool // false = CoreRoot
|
|
Files []render.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") == ActionSave
|
|
deleting = r.FormValue("action") == ActionDelete
|
|
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 {
|
|
responses.Flash(w, r, "Error saving: %s", err)
|
|
} else {
|
|
responses.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 {
|
|
responses.FlashAndRedirect(w, r, "/admin/editor", "Error deleting: %s", err)
|
|
} else {
|
|
responses.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 {
|
|
responses.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 := map[string]interface{}{
|
|
"File": file,
|
|
"Path": fp,
|
|
"Body": string(body),
|
|
"FromCore": fromCore,
|
|
}
|
|
render.Template(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: []render.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, render.Filepath{
|
|
Absolute: abs,
|
|
Relative: rel,
|
|
Basename: filepath.Base(path),
|
|
})
|
|
return nil
|
|
})
|
|
|
|
trees = append(trees, tree)
|
|
}
|
|
v := map[string]interface{}{
|
|
"FileTrees": trees,
|
|
}
|
|
render.Template(w, r, "admin/filelist", v)
|
|
}
|
|
|
|
// SettingsHandler lets you configure the app from the frontend.
|
|
func (b *Blog) SettingsHandler(w http.ResponseWriter, r *http.Request) {
|
|
// Get the current settings.
|
|
settings, _ := settings.Load()
|
|
v := map[string]interface{}{
|
|
"s": settings,
|
|
}
|
|
|
|
if r.Method == http.MethodPost {
|
|
redisPort, _ := strconv.Atoi(r.FormValue("redis-port"))
|
|
redisDB, _ := strconv.Atoi(r.FormValue("redis-db"))
|
|
mailPort, _ := strconv.Atoi(r.FormValue("mail-port"))
|
|
form := &forms.Settings{
|
|
Title: r.FormValue("title"),
|
|
Description: r.FormValue("description"),
|
|
AdminEmail: r.FormValue("admin-email"),
|
|
URL: r.FormValue("url"),
|
|
RedisEnabled: len(r.FormValue("redis-enabled")) > 0,
|
|
RedisHost: r.FormValue("redis-host"),
|
|
RedisPort: redisPort,
|
|
RedisDB: redisDB,
|
|
RedisPrefix: r.FormValue("redis-prefix"),
|
|
MailEnabled: len(r.FormValue("mail-enabled")) > 0,
|
|
MailSender: r.FormValue("mail-sender"),
|
|
MailHost: r.FormValue("mail-host"),
|
|
MailPort: mailPort,
|
|
MailUsername: r.FormValue("mail-username"),
|
|
MailPassword: r.FormValue("mail-password"),
|
|
}
|
|
|
|
// Copy form values into the settings struct for display, in case of
|
|
// any validation errors.
|
|
settings.Site.Title = form.Title
|
|
settings.Site.Description = form.Description
|
|
settings.Site.AdminEmail = form.AdminEmail
|
|
settings.Site.URL = form.URL
|
|
settings.Redis.Enabled = form.RedisEnabled
|
|
settings.Redis.Host = form.RedisHost
|
|
settings.Redis.Port = form.RedisPort
|
|
settings.Redis.DB = form.RedisDB
|
|
settings.Redis.Prefix = form.RedisPrefix
|
|
settings.Mail.Enabled = form.MailEnabled
|
|
settings.Mail.Sender = form.MailSender
|
|
settings.Mail.Host = form.MailHost
|
|
settings.Mail.Port = form.MailPort
|
|
settings.Mail.Username = form.MailUsername
|
|
settings.Mail.Password = form.MailPassword
|
|
err := form.Validate()
|
|
if err != nil {
|
|
v["Error"] = err
|
|
} else {
|
|
// Save the settings.
|
|
settings.Save()
|
|
b.Configure()
|
|
|
|
responses.FlashAndReload(w, r, "Settings have been saved!")
|
|
return
|
|
}
|
|
}
|
|
render.Template(w, r, "admin/settings", v)
|
|
}
|