package web import ( "errors" "net/http" "regexp" "strconv" "strings" "github.com/gramanas/blogspace/internal/auth" "github.com/gramanas/blogspace/internal/config" "github.com/gramanas/blogspace/internal/db" "github.com/gramanas/blogspace/internal/store" ) var usernameRe = regexp.MustCompile(`^[a-zA-Z0-9_.-]{2,40}$`) // 58 chars at most: "blog_" + subdomain must fit a Postgres database name. var subdomainRe = regexp.MustCompile(`^[a-z0-9](-?[a-z0-9]){0,57}$`) 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 } if errors.Is(err, db.ErrDatabaseExists) { f["error"] = "A database " + db.DBName(sub) + " already exists in Postgres (a leftover of a deleted blog?). Drop it or pick another subdomain." 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 b, err := s.st.BlogByOwner(r.Context(), u.ID); err == nil && b.Subdomain == config.RootSubdomain { s.plainError(w, http.StatusBadRequest, "This user owns the root blog and cannot be deleted.") return } if err := s.st.DeleteUser(r.Context(), u.ID); err != nil { // drops their blog database too s.serverError(w, err) return } redirectOK(w, r, "/admin/", "Deleted "+u.Username+" and their blog.") }