Break out some controllers and move mail to subpackage

Controllers moved into sub-packages:
* Initial setup
* Admin routes
* Auth login/out routes
* Contact
This commit is contained in:
Noah 2018-02-10 14:36:21 -08:00
parent 6d3de7da69
commit eb1880d348
14 changed files with 382 additions and 367 deletions

View File

@ -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<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)
}

View File

@ -2,15 +2,14 @@ package core
import ( import (
"bytes" "bytes"
"errors"
"html/template" "html/template"
"net/http" "net/http"
"net/mail"
"strings" "strings"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/kirsle/blog/core/internal/log" "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/markdown"
"github.com/kirsle/blog/core/internal/middleware/auth" "github.com/kirsle/blog/core/internal/middleware/auth"
"github.com/kirsle/blog/core/internal/models/comments" "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) responses.FlashAndRedirect(w, r, c.OriginURL, "Error posting comment: %s", err)
return return
} }
b.NotifyComment(c) mail.NotifyComment(c)
// Are they subscribing to future comments? // Are they subscribing to future comments?
if c.Subscribe && len(c.Email) > 0 { 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. // SubscriptionHandler to opt out of subscriptions.
func (b *Blog) SubscriptionHandler(w http.ResponseWriter, r *http.Request) { func (b *Blog) SubscriptionHandler(w http.ResponseWriter, r *http.Request) {
var err error
// POST to unsubscribe from all threads. // POST to unsubscribe from all threads.
if r.Method == http.MethodPost { if r.Method == http.MethodPost {
email := r.FormValue("email") email := r.FormValue("email")
if 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 { } else if _, err := mail.ParseAddress(email); err != nil {
err = errors.New("invalid email address") b.BadRequest(w, r, "invalid email address")
} }
m := comments.LoadMailingList() m := comments.LoadMailingList()
@ -294,9 +292,7 @@ func (b *Blog) SubscriptionHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
render.Template(w, r, "comments/subscription.gohtml", map[string]error{ render.Template(w, r, "comments/subscription.gohtml", nil)
"Error": err,
})
} }
// QuickDeleteHandler allows the admin to quickly delete spam without logging in. // QuickDeleteHandler allows the admin to quickly delete spam without logging in.

View File

@ -7,6 +7,10 @@ import (
"path/filepath" "path/filepath"
"github.com/gorilla/mux" "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/log"
"github.com/kirsle/blog/core/internal/markdown" "github.com/kirsle/blog/core/internal/markdown"
"github.com/kirsle/blog/core/internal/middleware" "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/settings"
"github.com/kirsle/blog/core/internal/models/users" "github.com/kirsle/blog/core/internal/models/users"
"github.com/kirsle/blog/core/internal/render" "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/core/internal/sessions"
"github.com/kirsle/blog/jsondb" "github.com/kirsle/blog/jsondb"
"github.com/kirsle/blog/jsondb/caches" "github.com/kirsle/blog/jsondb/caches"
@ -105,10 +110,10 @@ func (b *Blog) Configure() {
func (b *Blog) SetupHTTP() { func (b *Blog) SetupHTTP() {
// Initialize the router. // Initialize the router.
r := mux.NewRouter() r := mux.NewRouter()
r.HandleFunc("/initial-setup", b.SetupHandler) setup.Register(r)
b.AuthRoutes(r) authctl.Register(r)
b.AdminRoutes(r) admin.Register(r, b.MustLogin)
b.ContactRoutes(r) contact.Register(r, b.Error)
b.BlogRoutes(r) b.BlogRoutes(r)
b.CommentRoutes(r) b.CommentRoutes(r)
@ -137,3 +142,9 @@ func (b *Blog) ListenAndServe(address string) {
log.Info("Listening on %s", address) log.Info("Listening on %s", address)
http.ListenAndServe(address, b.n) 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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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\" }}\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.
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)
}

View File

@ -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)
}

View File

@ -1,4 +1,4 @@
package core package authctl
import ( import (
"errors" "errors"
@ -14,33 +14,14 @@ import (
"github.com/kirsle/blog/core/internal/sessions" "github.com/kirsle/blog/core/internal/sessions"
) )
// AuthRoutes attaches the auth routes to the app. // Register the initial setup routes.
func (b *Blog) AuthRoutes(r *mux.Router) { func Register(r *mux.Router) {
r.HandleFunc("/login", b.LoginHandler) r.HandleFunc("/login", loginHandler)
r.HandleFunc("/logout", b.LogoutHandler) r.HandleFunc("/logout", logoutHandler)
r.HandleFunc("/account", b.AccountHandler) r.HandleFunc("/account", accountHandler)
} }
// MustLogin handles errors from the LoginRequired middleware by redirecting func loginHandler(w http.ResponseWriter, r *http.Request) {
// 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) {
vars := map[string]interface{}{ vars := map[string]interface{}{
"Form": forms.Setup{}, "Form": forms.Setup{},
} }
@ -70,7 +51,7 @@ func (b *Blog) LoginHandler(w http.ResponseWriter, r *http.Request) {
} else { } else {
// Login OK! // Login OK!
responses.Flash(w, r, "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 // A next URL given? TODO: actually get to work
log.Info("Redirect after login to: %s", nextURL) 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) render.Template(w, r, "login", vars)
} }
// LogoutHandler logs the user out and redirects to the home page. func logoutHandler(w http.ResponseWriter, r *http.Request) {
func (b *Blog) LogoutHandler(w http.ResponseWriter, r *http.Request) {
session, _ := sessions.Store.Get(r, "session") session, _ := sessions.Store.Get(r, "session")
delete(session.Values, "logged-in") delete(session.Values, "logged-in")
delete(session.Values, "user-id") delete(session.Values, "user-id")
@ -96,8 +76,7 @@ func (b *Blog) LogoutHandler(w http.ResponseWriter, r *http.Request) {
responses.Redirect(w, "/") responses.Redirect(w, "/")
} }
// AccountHandler shows the account settings page. func accountHandler(w http.ResponseWriter, r *http.Request) {
func (b *Blog) AccountHandler(w http.ResponseWriter, r *http.Request) {
if !auth.LoggedIn(r) { if !auth.LoggedIn(r) {
responses.FlashAndRedirect(w, r, "/login?next=/account", "You must be logged in to do that!") responses.FlashAndRedirect(w, r, "/login?next=/account", "You must be logged in to do that!")
return return

View File

@ -1,4 +1,4 @@
package core package contact
import ( import (
"fmt" "fmt"
@ -10,14 +10,15 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/kirsle/blog/core/internal/forms" "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/markdown"
"github.com/kirsle/blog/core/internal/models/settings" "github.com/kirsle/blog/core/internal/models/settings"
"github.com/kirsle/blog/core/internal/render" "github.com/kirsle/blog/core/internal/render"
"github.com/kirsle/blog/core/internal/responses" "github.com/kirsle/blog/core/internal/responses"
) )
// ContactRoutes attaches the contact URL to the app. // Register attaches the contact URL to the app.
func (b *Blog) ContactRoutes(r *mux.Router) { func Register(r *mux.Router, onError func(http.ResponseWriter, *http.Request, string)) {
r.HandleFunc("/contact", func(w http.ResponseWriter, r *http.Request) { r.HandleFunc("/contact", func(w http.ResponseWriter, r *http.Request) {
form := &forms.Contact{} form := &forms.Contact{}
v := map[string]interface{}{ v := map[string]interface{}{
@ -27,13 +28,13 @@ func (b *Blog) ContactRoutes(r *mux.Router) {
// If there is no site admin, show an error. // If there is no site admin, show an error.
cfg, err := settings.Load() cfg, err := settings.Load()
if err != nil { if err != nil {
b.Error(w, r, "Error loading site configuration!") onError(w, r, "Error loading site configuration!")
return return
} else if cfg.Site.AdminEmail == "" { } 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 return
} else if !cfg.Mail.Enabled { } 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 return
} }
@ -43,7 +44,7 @@ func (b *Blog) ContactRoutes(r *mux.Router) {
if err = form.Validate(); err != nil { if err = form.Validate(); err != nil {
responses.Flash(w, r, err.Error()) responses.Flash(w, r, err.Error())
} else { } else {
go b.SendEmail(Email{ go mail.SendEmail(mail.Email{
To: cfg.Site.AdminEmail, To: cfg.Site.AdminEmail,
Admin: true, Admin: true,
ReplyTo: form.Email, ReplyTo: form.Email,
@ -57,7 +58,7 @@ func (b *Blog) ContactRoutes(r *mux.Router) {
}) })
// Log it to disk, too. // 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 { if err != nil {
responses.Flash(w, r, "Error logging the message to disk: %s", err) responses.Flash(w, r, "Error logging the message to disk: %s", err)
} else { } else {

View File

@ -1 +1,70 @@
package setup 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)
}

View File

@ -1,10 +1,10 @@
package core package mail
import ( import (
"bytes" "bytes"
"crypto/tls"
"fmt" "fmt"
"html/template" "html/template"
"net/mail"
"net/url" "net/url"
"strings" "strings"
@ -30,7 +30,7 @@ type Email struct {
} }
// SendEmail sends an email. // SendEmail sends an email.
func (b *Blog) SendEmail(email Email) { func SendEmail(email Email) {
s, _ := settings.Load() s, _ := settings.Load()
if !s.Mail.Enabled || s.Mail.Host == "" || s.Mail.Port == 0 || s.Mail.Sender == "" { if !s.Mail.Enabled || s.Mail.Host == "" || s.Mail.Port == 0 || s.Mail.Sender == "" {
log.Info("Suppressing email: not completely configured") log.Info("Suppressing email: not completely configured")
@ -83,11 +83,6 @@ func (b *Blog) SendEmail(email Email) {
m.AddAlternative("text/html", html.String()) m.AddAlternative("text/html", html.String())
d := gomail.NewDialer(s.Mail.Host, s.Mail.Port, s.Mail.Username, s.Mail.Password) 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) log.Info("SendEmail: %s (%s) to %s", email.Subject, email.Template, email.To)
if err := d.DialAndSend(m); err != nil { if err := d.DialAndSend(m); err != nil {
@ -96,7 +91,7 @@ func (b *Blog) SendEmail(email Email) {
} }
// NotifyComment sends notification emails about comments. // NotifyComment sends notification emails about comments.
func (b *Blog) NotifyComment(c *comments.Comment) { func NotifyComment(c *comments.Comment) {
s, _ := settings.Load() s, _ := settings.Load()
if s.Site.URL == "" { if s.Site.URL == "" {
log.Error("Can't send comment notification because the site URL is not configured") 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.To = config.Site.AdminEmail
email.Admin = true email.Admin = true
log.Info("Mail site admin '%s' about comment notification on '%s'", email.To, c.ThreadID) log.Info("Mail site admin '%s' about comment notification on '%s'", email.To, c.ThreadID)
b.SendEmail(email) SendEmail(email)
} }
// Email the subscribers. // Email the subscribers.
@ -143,6 +138,11 @@ func (b *Blog) NotifyComment(c *comments.Comment) {
url.QueryEscape(to), url.QueryEscape(to),
) )
log.Info("Mail subscriber '%s' about comment notification on '%s'", email.To, c.ThreadID) 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)
}

View File

@ -26,6 +26,18 @@ func CurrentUser(r *http.Request) (*users.User, error) {
}, errors.New("not authenticated") }, 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. // LoggedIn returns whether the current user is logged in to an account.
func LoggedIn(r *http.Request) bool { func LoggedIn(r *http.Request) bool {
session := sessions.Get(r) session := sessions.Get(r)

View File

@ -2,6 +2,7 @@
{{ define "content" }} {{ define "content" }}
<h1>Account Settings</h1> <h1>Account Settings</h1>
{{ $form := .Data.Form }}
<form action="/account" method="POST"> <form action="/account" method="POST">
<input type="hidden" name="_csrf" value="{{ .CSRF }}"> <input type="hidden" name="_csrf" value="{{ .CSRF }}">
@ -14,7 +15,7 @@
class="form-control" class="form-control"
name="username" name="username"
id="username" id="username"
value="{{ .Form.Username }}" value="{{ $form.Username }}"
placeholder="soandso"> placeholder="soandso">
</div> </div>
@ -24,8 +25,8 @@
class="form-control" class="form-control"
name="name" name="name"
id="name" id="name"
value="{{ .Form.Name }}" value="{{ $form.Name }}"
placeholder="{{ or .Form.Username "Anonymous" }}"> placeholder="{{ or $form.Username "Anonymous" }}">
</div> </div>
<div class="form-group"> <div class="form-group">
@ -34,7 +35,7 @@
class="form-control" class="form-control"
name="email" name="email"
id="email" id="email"
value="{{ .Form.Email }}" value="{{ $form.Email }}"
placeholder="name@domain.com"> placeholder="name@domain.com">
</div> </div>

View File

@ -7,8 +7,6 @@
administrator. administrator.
</p> </p>
data={{ .Data }}
{{ $form := .Data.Form }} {{ $form := .Data.Form }}
<form method="POST" action="/contact"> <form method="POST" action="/contact">
<input type="hidden" name="_csrf" value="{{ .CSRF }}"> <input type="hidden" name="_csrf" value="{{ .CSRF }}">