diff options
Diffstat (limited to 'internal/web/handlers_admin.go')
| -rw-r--r-- | internal/web/handlers_admin.go | 154 |
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.") +} |
