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\" }}\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. + 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 }}
@@ -14,7 +15,7 @@ class="form-control" name="username" id="username" - value="{{ .Form.Username }}" + value="{{ $form.Username }}" placeholder="soandso"> @@ -24,8 +25,8 @@ class="form-control" name="name" id="name" - value="{{ .Form.Name }}" - placeholder="{{ or .Form.Username "Anonymous" }}"> + value="{{ $form.Name }}" + placeholder="{{ or $form.Username "Anonymous" }}">
@@ -34,7 +35,7 @@ class="form-control" name="email" id="email" - value="{{ .Form.Email }}" + value="{{ $form.Email }}" placeholder="name@domain.com">
diff --git a/root/contact.gohtml b/root/contact.gohtml index 66f0d99..13b4dd9 100644 --- a/root/contact.gohtml +++ b/root/contact.gohtml @@ -7,8 +7,6 @@ administrator.

-data={{ .Data }} - {{ $form := .Data.Form }}