aboutsummaryrefslogtreecommitdiffstats
path: root/internal/web/handlers_admin.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/web/handlers_admin.go')
-rw-r--r--internal/web/handlers_admin.go154
1 files changed, 154 insertions, 0 deletions
diff --git a/internal/web/handlers_admin.go b/internal/web/handlers_admin.go
new file mode 100644
index 0000000..30fdc2e
--- /dev/null
+++ b/internal/web/handlers_admin.go
@@ -0,0 +1,154 @@
+package web
+
+import (
+ "errors"
+ "net/http"
+ "regexp"
+ "strconv"
+ "strings"
+
+ "github.com/gramanas/blogspace/internal/auth"
+ "github.com/gramanas/blogspace/internal/store"
+)
+
+var usernameRe = regexp.MustCompile(`^[a-zA-Z0-9_.-]{2,40}$`)
+var subdomainRe = regexp.MustCompile(`^[a-z0-9](-?[a-z0-9]){0,62}$`)
+
+func (s *Server) handleAdmin(w http.ResponseWriter, r *http.Request) {
+ users, err := s.st.ListUsers(r.Context())
+ if err != nil {
+ s.serverError(w, err)
+ return
+ }
+ s.render(w, r, "admin/index.html", map[string]any{"users": users, "cfg": s.cfg})
+}
+
+func (s *Server) handleAdminNewUserForm(w http.ResponseWriter, r *http.Request) {
+ s.render(w, r, "admin/new_user.html", map[string]any{"cfg": s.cfg})
+}
+
+func (s *Server) handleAdminNewUser(w http.ResponseWriter, r *http.Request) {
+ f := map[string]any{
+ "username": strings.TrimSpace(r.FormValue("username")),
+ "subdomain": strings.ToLower(strings.TrimSpace(r.FormValue("subdomain"))),
+ "title": strings.TrimSpace(r.FormValue("title")),
+ "cfg": s.cfg,
+ }
+ pw := r.FormValue("password")
+ username, sub, title := f["username"].(string), f["subdomain"].(string), f["title"].(string)
+ if sub == "" {
+ sub = strings.ToLower(username)
+ f["subdomain"] = sub
+ }
+ if title == "" {
+ title = username + "'s blog"
+ }
+ var msg string
+ switch {
+ case !usernameRe.MatchString(username):
+ msg = "Username: 2-40 letters, digits, dots, dashes or underscores."
+ case len(pw) < 8:
+ msg = "Password must be at least 8 characters."
+ case !subdomainRe.MatchString(sub) || reservedSubdomains[sub]:
+ msg = "Subdomain: lowercase letters, digits and single dashes; not a reserved name."
+ }
+ if msg != "" {
+ f["error"] = msg
+ s.renderStatus(w, r, http.StatusBadRequest, "admin/new_user.html", f)
+ return
+ }
+ hash, err := auth.HashPassword(pw)
+ if err != nil {
+ s.serverError(w, err)
+ return
+ }
+ _, blog, err := s.st.CreateBlogger(r.Context(), username, hash, sub, title)
+ if err != nil {
+ if errors.Is(err, store.ErrConflict) {
+ f["error"] = "Username or subdomain already taken."
+ s.renderStatus(w, r, http.StatusConflict, "admin/new_user.html", f)
+ return
+ }
+ s.serverError(w, err)
+ return
+ }
+ redirectOK(w, r, "/admin/", "Created "+username+" with blog "+blog.Subdomain+".")
+}
+
+func (s *Server) adminTargetUser(w http.ResponseWriter, r *http.Request) *store.User {
+ id, _ := strconv.ParseInt(r.PathValue("id"), 10, 64)
+ u, err := s.st.UserByID(r.Context(), id)
+ if err != nil {
+ if errors.Is(err, store.ErrNotFound) {
+ http.NotFound(w, r)
+ } else {
+ s.serverError(w, err)
+ }
+ return nil
+ }
+ if u.ID == currentUser(r).ID {
+ s.plainError(w, http.StatusBadRequest, "You cannot do that to your own account here; use Account > Password.")
+ return nil
+ }
+ return u
+}
+
+func (s *Server) handleAdminResetPassword(w http.ResponseWriter, r *http.Request) {
+ u := s.adminTargetUser(w, r)
+ if u == nil {
+ return
+ }
+ pw := r.FormValue("password")
+ if len(pw) < 8 {
+ s.plainError(w, http.StatusBadRequest, "Password must be at least 8 characters.")
+ return
+ }
+ hash, err := auth.HashPassword(pw)
+ if err != nil {
+ s.serverError(w, err)
+ return
+ }
+ if err := s.st.SetPassword(r.Context(), u.ID, hash); err != nil {
+ s.serverError(w, err)
+ return
+ }
+ redirectOK(w, r, "/admin/", "Password reset for "+u.Username+".")
+}
+
+func (s *Server) handleAdminSetDisabled(disabled bool) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ u := s.adminTargetUser(w, r)
+ if u == nil {
+ return
+ }
+ if err := s.st.SetUserDisabled(r.Context(), u.ID, disabled); err != nil {
+ s.serverError(w, err)
+ return
+ }
+ verb := "enabled"
+ if disabled {
+ verb = "disabled"
+ }
+ redirectOK(w, r, "/admin/", u.Username+" "+verb+".")
+ }
+}
+
+func (s *Server) handleAdminDeleteUserConfirm(w http.ResponseWriter, r *http.Request) {
+ u := s.adminTargetUser(w, r)
+ if u == nil {
+ return
+ }
+ s.render(w, r, "admin/delete_user.html", map[string]any{"target": u})
+}
+
+func (s *Server) handleAdminDeleteUser(w http.ResponseWriter, r *http.Request) {
+ u := s.adminTargetUser(w, r)
+ if u == nil {
+ return
+ }
+ if err := s.st.DeleteUser(r.Context(), u.ID); err != nil { // cascades to blog, pages, posts, images
+ s.serverError(w, err)
+ return
+ }
+ redirectOK(w, r, "/admin/", "Deleted "+u.Username+" and their blog.")
+}