diff --git a/Makefile b/Makefile index 705f758..e7cbf79 100644 --- a/Makefile +++ b/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 diff --git a/core/admin.go b/core/admin.go index 26ff2e2..f3874ae 100644 --- a/core/admin.go +++ b/core/admin.go @@ -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) } diff --git a/core/app.go b/core/app.go index 058c4aa..956cb08 100644 --- a/core/app.go +++ b/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 } diff --git a/core/auth.go b/core/auth.go index c92a28f..aa7d175 100644 --- a/core/auth.go +++ b/core/auth.go @@ -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 diff --git a/core/forms/setup.go b/core/forms/setup.go index d88a7e6..dd76a08 100644 --- a/core/forms/setup.go +++ b/core/forms/setup.go @@ -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 diff --git a/core/initial-setup.go b/core/initial-setup.go new file mode 100644 index 0000000..86ef3c7 --- /dev/null +++ b/core/initial-setup.go @@ -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) +} diff --git a/core/jsondb/jsondb.go b/core/jsondb/jsondb.go index eab1d61..6e61b38 100644 --- a/core/jsondb/jsondb.go +++ b/core/jsondb/jsondb.go @@ -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 } diff --git a/core/middleware.go b/core/middleware.go index ddbfd9f..71be2c5 100644 --- a/core/middleware.go +++ b/core/middleware.go @@ -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) } diff --git a/core/pages.go b/core/pages.go index 1afd8ab..b39844f 100644 --- a/core/pages.go +++ b/core/pages.go @@ -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 } } diff --git a/core/responses.go b/core/responses.go index ca748ed..c282741 100644 --- a/core/responses.go +++ b/core/responses.go @@ -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], diff --git a/core/templates.go b/core/templates.go index 8829f5c..722b4fe 100644 --- a/core/templates.go +++ b/core/templates.go @@ -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 diff --git a/root/.layout.gohtml b/root/.layout.gohtml index c21f740..4e99c0e 100644 --- a/root/.layout.gohtml +++ b/root/.layout.gohtml @@ -1,4 +1,6 @@ {{ define "title" }}Untitled{{ end }} +{{ define "scripts" }}Default Scripts{{ end }} + {{ define "layout" }} @@ -52,7 +54,7 @@ {{ if .SetupNeeded }}