diff --git a/core/admin.go b/core/admin.go
deleted file mode 100644
index 8af40ef..0000000
--- a/core/admin.go
+++ /dev/null
@@ -1,234 +0,0 @@
-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
Untitled Page
\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)
-}
diff --git a/core/comments.go b/core/comments.go
index f6a985b..ba5164a 100644
--- a/core/comments.go
+++ b/core/comments.go
@@ -2,15 +2,14 @@ package core
import (
"bytes"
- "errors"
"html/template"
"net/http"
- "net/mail"
"strings"
"github.com/google/uuid"
"github.com/gorilla/mux"
"github.com/kirsle/blog/core/internal/log"
+ "github.com/kirsle/blog/core/internal/mail"
"github.com/kirsle/blog/core/internal/markdown"
"github.com/kirsle/blog/core/internal/middleware/auth"
"github.com/kirsle/blog/core/internal/models/comments"
@@ -236,7 +235,7 @@ func (b *Blog) CommentHandler(w http.ResponseWriter, r *http.Request) {
responses.FlashAndRedirect(w, r, c.OriginURL, "Error posting comment: %s", err)
return
}
- b.NotifyComment(c)
+ mail.NotifyComment(c)
// Are they subscribing to future comments?
if c.Subscribe && len(c.Email) > 0 {
@@ -266,14 +265,13 @@ func (b *Blog) CommentHandler(w http.ResponseWriter, r *http.Request) {
// SubscriptionHandler to opt out of subscriptions.
func (b *Blog) SubscriptionHandler(w http.ResponseWriter, r *http.Request) {
- var err error
// POST to unsubscribe from all threads.
if r.Method == http.MethodPost {
email := r.FormValue("email")
if email == "" {
- err = errors.New("email address is required to unsubscribe from comment threads")
+ b.BadRequest(w, r, "email address is required to unsubscribe from comment threads")
} else if _, err := mail.ParseAddress(email); err != nil {
- err = errors.New("invalid email address")
+ b.BadRequest(w, r, "invalid email address")
}
m := comments.LoadMailingList()
@@ -294,9 +292,7 @@ func (b *Blog) SubscriptionHandler(w http.ResponseWriter, r *http.Request) {
return
}
- render.Template(w, r, "comments/subscription.gohtml", map[string]error{
- "Error": err,
- })
+ render.Template(w, r, "comments/subscription.gohtml", nil)
}
// QuickDeleteHandler allows the admin to quickly delete spam without logging in.
diff --git a/core/core.go b/core/core.go
index 3324f16..1f96907 100644
--- a/core/core.go
+++ b/core/core.go
@@ -7,6 +7,10 @@ import (
"path/filepath"
"github.com/gorilla/mux"
+ "github.com/kirsle/blog/core/internal/controllers/admin"
+ "github.com/kirsle/blog/core/internal/controllers/authctl"
+ "github.com/kirsle/blog/core/internal/controllers/contact"
+ "github.com/kirsle/blog/core/internal/controllers/setup"
"github.com/kirsle/blog/core/internal/log"
"github.com/kirsle/blog/core/internal/markdown"
"github.com/kirsle/blog/core/internal/middleware"
@@ -16,6 +20,7 @@ import (
"github.com/kirsle/blog/core/internal/models/settings"
"github.com/kirsle/blog/core/internal/models/users"
"github.com/kirsle/blog/core/internal/render"
+ "github.com/kirsle/blog/core/internal/responses"
"github.com/kirsle/blog/core/internal/sessions"
"github.com/kirsle/blog/jsondb"
"github.com/kirsle/blog/jsondb/caches"
@@ -105,10 +110,10 @@ func (b *Blog) Configure() {
func (b *Blog) SetupHTTP() {
// Initialize the router.
r := mux.NewRouter()
- r.HandleFunc("/initial-setup", b.SetupHandler)
- b.AuthRoutes(r)
- b.AdminRoutes(r)
- b.ContactRoutes(r)
+ setup.Register(r)
+ authctl.Register(r)
+ admin.Register(r, b.MustLogin)
+ contact.Register(r, b.Error)
b.BlogRoutes(r)
b.CommentRoutes(r)
@@ -137,3 +142,9 @@ func (b *Blog) ListenAndServe(address string) {
log.Info("Listening on %s", address)
http.ListenAndServe(address, b.n)
}
+
+// MustLogin handles errors from the LoginRequired middleware by redirecting
+// the user to the login page.
+func (b *Blog) MustLogin(w http.ResponseWriter, r *http.Request) {
+ responses.Redirect(w, "/login?next="+r.URL.Path)
+}
diff --git a/core/initial-setup.go b/core/initial-setup.go
deleted file mode 100644
index d0063f5..0000000
--- a/core/initial-setup.go
+++ /dev/null
@@ -1,64 +0,0 @@
-package core
-
-import (
- "net/http"
-
- "github.com/kirsle/blog/core/internal/forms"
- "github.com/kirsle/blog/core/internal/log"
- "github.com/kirsle/blog/core/internal/models/settings"
- "github.com/kirsle/blog/core/internal/models/users"
- "github.com/kirsle/blog/core/internal/render"
- "github.com/kirsle/blog/core/internal/responses"
- "github.com/kirsle/blog/core/internal/sessions"
-)
-
-// SetupHandler is the initial blog setup route.
-func (b *Blog) SetupHandler(w http.ResponseWriter, r *http.Request) {
- form := &forms.Setup{}
- vars := map[string]interface{}{
- "Form": form,
- }
-
- // Reject if we're already set up.
- s, _ := settings.Load()
- if s.Initialized {
- responses.FlashAndRedirect(w, r, "/", "This website has already been configured.")
- return
- }
-
- if r.Method == http.MethodPost {
- form.ParseForm(r)
- 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.
- sessions.SetSecretKey([]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)
- responses.FlashAndRedirect(w, r, "/admin", "Admin user created and logged in.")
- return
- }
- }
-
- render.Template(w, r, "initial-setup", vars)
-}
diff --git a/core/internal/controllers/admin/admin.go b/core/internal/controllers/admin/admin.go
new file mode 100644
index 0000000..1ade73c
--- /dev/null
+++ b/core/internal/controllers/admin/admin.go
@@ -0,0 +1,27 @@
+package admin
+
+import (
+ "net/http"
+
+ "github.com/gorilla/mux"
+ "github.com/kirsle/blog/core/internal/middleware/auth"
+ "github.com/kirsle/blog/core/internal/render"
+ "github.com/urfave/negroni"
+)
+
+// Register the initial setup routes.
+func Register(r *mux.Router, authErrorFunc http.HandlerFunc) {
+ adminRouter := mux.NewRouter().PathPrefix("/admin").Subrouter().StrictSlash(true)
+ adminRouter.HandleFunc("/", indexHandler)
+ adminRouter.HandleFunc("/settings", settingsHandler)
+ adminRouter.HandleFunc("/editor", editorHandler)
+
+ r.PathPrefix("/admin").Handler(negroni.New(
+ negroni.HandlerFunc(auth.LoginRequired(authErrorFunc)),
+ negroni.Wrap(adminRouter),
+ ))
+}
+
+func indexHandler(w http.ResponseWriter, r *http.Request) {
+ render.Template(w, r, "admin/index", nil)
+}
diff --git a/core/internal/controllers/admin/editor.go b/core/internal/controllers/admin/editor.go
new file mode 100644
index 0000000..448bde2
--- /dev/null
+++ b/core/internal/controllers/admin/editor.go
@@ -0,0 +1,147 @@
+package admin
+
+import (
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/kirsle/blog/core/internal/render"
+ "github.com/kirsle/blog/core/internal/responses"
+)
+
+// FileTree holds information about files in the document roots.
+type FileTree struct {
+ UserRoot bool // false = CoreRoot
+ Files []render.Filepath
+}
+
+func 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(*render.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(*render.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(*render.DocumentRoot, file)
+ } else {
+ fp = filepath.Join(*render.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(*render.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\" }}\nUntitled Page
\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.
+ editorFileList(w, r)
+}
+
+// editorFileList handles the index view of /admin/editor.
+func editorFileList(w http.ResponseWriter, r *http.Request) {
+ // Listing the file tree?
+ trees := []FileTree{}
+ for i, root := range []string{*render.UserRoot, *render.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)
+}
diff --git a/core/internal/controllers/admin/settings.go b/core/internal/controllers/admin/settings.go
new file mode 100644
index 0000000..be66074
--- /dev/null
+++ b/core/internal/controllers/admin/settings.go
@@ -0,0 +1,72 @@
+package admin
+
+import (
+ "net/http"
+ "strconv"
+
+ "github.com/kirsle/blog/core/internal/forms"
+ "github.com/kirsle/blog/core/internal/models/settings"
+ "github.com/kirsle/blog/core/internal/render"
+ "github.com/kirsle/blog/core/internal/responses"
+)
+
+func 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)
+}
diff --git a/core/auth.go b/core/internal/controllers/authctl/authctl.go
similarity index 75%
rename from core/auth.go
rename to core/internal/controllers/authctl/authctl.go
index ba59417..42da874 100644
--- a/core/auth.go
+++ b/core/internal/controllers/authctl/authctl.go
@@ -1,4 +1,4 @@
-package core
+package authctl
import (
"errors"
@@ -14,33 +14,14 @@ import (
"github.com/kirsle/blog/core/internal/sessions"
)
-// AuthRoutes attaches the auth routes to the app.
-func (b *Blog) AuthRoutes(r *mux.Router) {
- r.HandleFunc("/login", b.LoginHandler)
- r.HandleFunc("/logout", b.LogoutHandler)
- r.HandleFunc("/account", b.AccountHandler)
+// Register the initial setup routes.
+func Register(r *mux.Router) {
+ r.HandleFunc("/login", loginHandler)
+ r.HandleFunc("/logout", logoutHandler)
+ r.HandleFunc("/account", accountHandler)
}
-// MustLogin handles errors from the LoginRequired middleware by redirecting
-// the user to the login page.
-func (b *Blog) MustLogin(w http.ResponseWriter, r *http.Request) {
- responses.Redirect(w, "/login?next="+r.URL.Path)
-}
-
-// 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 := sessions.Store.Get(r, "session") // TODO session name
- if err != nil {
- return err
- }
- session.Values["logged-in"] = true
- session.Values["user-id"] = u.ID
- session.Save(r, w)
- return nil
-}
-
-// LoginHandler shows and handles the login page.
-func (b *Blog) LoginHandler(w http.ResponseWriter, r *http.Request) {
+func loginHandler(w http.ResponseWriter, r *http.Request) {
vars := map[string]interface{}{
"Form": forms.Setup{},
}
@@ -70,7 +51,7 @@ func (b *Blog) LoginHandler(w http.ResponseWriter, r *http.Request) {
} else {
// Login OK!
responses.Flash(w, r, "Login OK!")
- b.Login(w, r, user)
+ auth.Login(w, r, user)
// A next URL given? TODO: actually get to work
log.Info("Redirect after login to: %s", nextURL)
@@ -87,8 +68,7 @@ func (b *Blog) LoginHandler(w http.ResponseWriter, r *http.Request) {
render.Template(w, r, "login", vars)
}
-// LogoutHandler logs the user out and redirects to the home page.
-func (b *Blog) LogoutHandler(w http.ResponseWriter, r *http.Request) {
+func logoutHandler(w http.ResponseWriter, r *http.Request) {
session, _ := sessions.Store.Get(r, "session")
delete(session.Values, "logged-in")
delete(session.Values, "user-id")
@@ -96,8 +76,7 @@ func (b *Blog) LogoutHandler(w http.ResponseWriter, r *http.Request) {
responses.Redirect(w, "/")
}
-// AccountHandler shows the account settings page.
-func (b *Blog) AccountHandler(w http.ResponseWriter, r *http.Request) {
+func accountHandler(w http.ResponseWriter, r *http.Request) {
if !auth.LoggedIn(r) {
responses.FlashAndRedirect(w, r, "/login?next=/account", "You must be logged in to do that!")
return
diff --git a/core/contact.go b/core/internal/controllers/contact/contact.go
similarity index 77%
rename from core/contact.go
rename to core/internal/controllers/contact/contact.go
index 8c97717..dfcee94 100644
--- a/core/contact.go
+++ b/core/internal/controllers/contact/contact.go
@@ -1,4 +1,4 @@
-package core
+package contact
import (
"fmt"
@@ -10,14 +10,15 @@ import (
"github.com/gorilla/mux"
"github.com/kirsle/blog/core/internal/forms"
+ "github.com/kirsle/blog/core/internal/mail"
"github.com/kirsle/blog/core/internal/markdown"
"github.com/kirsle/blog/core/internal/models/settings"
"github.com/kirsle/blog/core/internal/render"
"github.com/kirsle/blog/core/internal/responses"
)
-// ContactRoutes attaches the contact URL to the app.
-func (b *Blog) ContactRoutes(r *mux.Router) {
+// Register attaches the contact URL to the app.
+func Register(r *mux.Router, onError func(http.ResponseWriter, *http.Request, string)) {
r.HandleFunc("/contact", func(w http.ResponseWriter, r *http.Request) {
form := &forms.Contact{}
v := map[string]interface{}{
@@ -27,13 +28,13 @@ func (b *Blog) ContactRoutes(r *mux.Router) {
// If there is no site admin, show an error.
cfg, err := settings.Load()
if err != nil {
- b.Error(w, r, "Error loading site configuration!")
+ onError(w, r, "Error loading site configuration!")
return
} else if cfg.Site.AdminEmail == "" {
- b.Error(w, r, "There is no admin email configured for this website!")
+ onError(w, r, "There is no admin email configured for this website!")
return
} else if !cfg.Mail.Enabled {
- b.Error(w, r, "This website doesn't have an e-mail gateway configured.")
+ onError(w, r, "This website doesn't have an e-mail gateway configured.")
return
}
@@ -43,7 +44,7 @@ func (b *Blog) ContactRoutes(r *mux.Router) {
if err = form.Validate(); err != nil {
responses.Flash(w, r, err.Error())
} else {
- go b.SendEmail(Email{
+ go mail.SendEmail(mail.Email{
To: cfg.Site.AdminEmail,
Admin: true,
ReplyTo: form.Email,
@@ -57,7 +58,7 @@ func (b *Blog) ContactRoutes(r *mux.Router) {
})
// Log it to disk, too.
- fh, err := os.OpenFile(filepath.Join(b.UserRoot, ".contact.log"), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
+ fh, err := os.OpenFile(filepath.Join(*render.UserRoot, ".contact.log"), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
responses.Flash(w, r, "Error logging the message to disk: %s", err)
} else {
diff --git a/core/internal/controllers/setup/setup.go b/core/internal/controllers/setup/setup.go
index 9bca696..2e0a093 100644
--- a/core/internal/controllers/setup/setup.go
+++ b/core/internal/controllers/setup/setup.go
@@ -1 +1,70 @@
package setup
+
+import (
+ "net/http"
+
+ "github.com/gorilla/mux"
+ "github.com/kirsle/blog/core/internal/forms"
+ "github.com/kirsle/blog/core/internal/log"
+ "github.com/kirsle/blog/core/internal/middleware/auth"
+ "github.com/kirsle/blog/core/internal/models/settings"
+ "github.com/kirsle/blog/core/internal/models/users"
+ "github.com/kirsle/blog/core/internal/render"
+ "github.com/kirsle/blog/core/internal/responses"
+ "github.com/kirsle/blog/core/internal/sessions"
+)
+
+// Register the initial setup routes.
+func Register(r *mux.Router) {
+ r.HandleFunc("/initial-setup", handler)
+}
+
+func handler(w http.ResponseWriter, r *http.Request) {
+ form := &forms.Setup{}
+ vars := map[string]interface{}{
+ "Form": form,
+ }
+
+ // Reject if we're already set up.
+ s, _ := settings.Load()
+ if s.Initialized {
+ responses.FlashAndRedirect(w, r, "/", "This website has already been configured.")
+ return
+ }
+
+ if r.Method == http.MethodPost {
+ form.ParseForm(r)
+ 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.
+ sessions.SetSecretKey([]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!
+ auth.Login(w, r, user)
+ responses.FlashAndRedirect(w, r, "/admin", "Admin user created and logged in.")
+ return
+ }
+ }
+
+ render.Template(w, r, "initial-setup", vars)
+}
diff --git a/core/mail.go b/core/internal/mail/mail.go
similarity index 93%
rename from core/mail.go
rename to core/internal/mail/mail.go
index f6f5552..d5090b9 100644
--- a/core/mail.go
+++ b/core/internal/mail/mail.go
@@ -1,10 +1,10 @@
-package core
+package mail
import (
"bytes"
- "crypto/tls"
"fmt"
"html/template"
+ "net/mail"
"net/url"
"strings"
@@ -30,7 +30,7 @@ type Email struct {
}
// SendEmail sends an email.
-func (b *Blog) SendEmail(email Email) {
+func SendEmail(email Email) {
s, _ := settings.Load()
if !s.Mail.Enabled || s.Mail.Host == "" || s.Mail.Port == 0 || s.Mail.Sender == "" {
log.Info("Suppressing email: not completely configured")
@@ -83,11 +83,6 @@ func (b *Blog) SendEmail(email Email) {
m.AddAlternative("text/html", html.String())
d := gomail.NewDialer(s.Mail.Host, s.Mail.Port, s.Mail.Username, s.Mail.Password)
- if b.Debug {
- d.TLSConfig = &tls.Config{
- InsecureSkipVerify: true,
- }
- }
log.Info("SendEmail: %s (%s) to %s", email.Subject, email.Template, email.To)
if err := d.DialAndSend(m); err != nil {
@@ -96,7 +91,7 @@ func (b *Blog) SendEmail(email Email) {
}
// NotifyComment sends notification emails about comments.
-func (b *Blog) NotifyComment(c *comments.Comment) {
+func NotifyComment(c *comments.Comment) {
s, _ := settings.Load()
if s.Site.URL == "" {
log.Error("Can't send comment notification because the site URL is not configured")
@@ -126,7 +121,7 @@ func (b *Blog) NotifyComment(c *comments.Comment) {
email.To = config.Site.AdminEmail
email.Admin = true
log.Info("Mail site admin '%s' about comment notification on '%s'", email.To, c.ThreadID)
- b.SendEmail(email)
+ SendEmail(email)
}
// Email the subscribers.
@@ -143,6 +138,11 @@ func (b *Blog) NotifyComment(c *comments.Comment) {
url.QueryEscape(to),
)
log.Info("Mail subscriber '%s' about comment notification on '%s'", email.To, c.ThreadID)
- b.SendEmail(email)
+ SendEmail(email)
}
}
+
+// ParseAddress parses an email address.
+func ParseAddress(addr string) (*mail.Address, error) {
+ return mail.ParseAddress(addr)
+}
diff --git a/core/internal/middleware/auth/auth.go b/core/internal/middleware/auth/auth.go
index dad1fd8..74b410d 100644
--- a/core/internal/middleware/auth/auth.go
+++ b/core/internal/middleware/auth/auth.go
@@ -26,6 +26,18 @@ func CurrentUser(r *http.Request) (*users.User, error) {
}, errors.New("not authenticated")
}
+// Login logs the browser in as the given user.
+func Login(w http.ResponseWriter, r *http.Request, u *users.User) error {
+ session, err := sessions.Store.Get(r, "session") // TODO session name
+ if err != nil {
+ return err
+ }
+ session.Values["logged-in"] = true
+ session.Values["user-id"] = u.ID
+ session.Save(r, w)
+ return nil
+}
+
// LoggedIn returns whether the current user is logged in to an account.
func LoggedIn(r *http.Request) bool {
session := sessions.Get(r)
diff --git a/root/account.gohtml b/root/account.gohtml
index a23df14..be97901 100644
--- a/root/account.gohtml
+++ b/root/account.gohtml
@@ -2,6 +2,7 @@
{{ define "content" }}
Account Settings
+{{ $form := .Data.Form }}