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 = s.tr(r, "Username: 2-40 letters, digits, dots, dashes or underscores.") case len(pw) < 8: msg = s.tr(r, "Password must be at least 8 characters.") case !subdomainRe.MatchString(sub) || reservedSubdomains[sub]: msg = s.tr(r, "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"] = s.tr(r, "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"] = s.trf(r, "A database %s already exists in Postgres (a leftover of a deleted blog?). Drop it or pick another subdomain.", db.DBName(sub)) s.renderStatus(w, r, http.StatusConflict, "admin/new_user.html", f) return } s.serverError(w, err) return } redirectOK(w, r, "/admin/", s.trf(r, "Created %s with blog %s.", username, 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, s.tr(r, "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, s.tr(r, "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/", s.trf(r, "Password reset for %s.", 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 } msg := s.trf(r, "%s enabled.", u.Username) if disabled { msg = s.trf(r, "%s disabled.", u.Username) } redirectOK(w, r, "/admin/", msg) } } 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, s.tr(r, "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/", s.trf(r, "Deleted %s and their blog.", u.Username)) }