package web import ( "errors" "fmt" "net/http" "strconv" "strings" "git.t5de.dev/t5de/t5de-hub/internal/store" ) // The moderation console: counts, who's online, people (search, link caps, bans, roles), and the audit log. func (s *Server) console(q *request) error { counts, err := s.Store.Counts(s.ctx(q)) if err != nil { return err } log, err := s.Store.AuditLog(s.ctx(q), 50) if err != nil { return err } s.render(q, "console", map[string]any{"Counts": counts, "Connections": s.Realtime.Connections(), "Online": s.visibleOnline(q), "Audit": log}) return nil } func (s *Server) consoleOnline(q *request) error { s.fragment(q, "console", "console-online", map[string]any{"Online": s.visibleOnline(q), "Connections": s.Realtime.Connections()}) return nil } func (s *Server) consolePeople(q *request) error { search := strings.TrimSpace(q.r.URL.Query().Get("q")) people, err := s.Store.People(s.ctx(q), search, 100) if err != nil { return err } s.render(q, "console_people", map[string]any{"People": people, "Search": search}) return nil } func (s *Server) personParam(q *request) (*store.Person, error) { id, err := strconv.ParseInt(q.r.PathValue("id"), 10, 64) if err != nil { return nil, fail(http.StatusNotFound, "No such person.") } p, err := s.Store.PersonByID(s.ctx(q), id) if errors.Is(err, store.ErrNotFound) { return nil, fail(http.StatusNotFound, "No such person.") } return p, err } func (s *Server) consolePerson(q *request) error { p, err := s.personParam(q) if err != nil { return err } links, err := s.Store.Links(s.ctx(q), p.ID) if err != nil { return err } signins, err := s.Store.Signins(s.ctx(q), p.ID) if err != nil { return err } s.render(q, "console_person", map[string]any{"Subject": p, "Links": links, "Signins": signins}) return nil } func (s *Server) setCap(q *request) error { p, err := s.personParam(q) if err != nil { return err } cap, err := strconv.Atoi(q.r.PostFormValue("cap")) if err != nil || cap < 0 || cap > 1000 { return fail(http.StatusBadRequest, "The link cap is a number from 0 to 1000.") } if err := s.Store.SetLinkCap(s.ctx(q), p.ID, cap); err != nil { return err } s.Store.Audit(s.ctx(q), q.person.ID, "person.link_cap", p.Username, map[string]any{"from": p.LinkCap, "to": cap}) return q.redirect(fmt.Sprintf("/console/people/%d", p.ID), fmt.Sprintf("%s can link %d accounts.", p.Username, cap)) } func (s *Server) ban(q *request) error { p, err := s.personParam(q) if err != nil { return err } if p.ID == q.person.ID { return fail(http.StatusBadRequest, "You can't ban yourself.") } if p.IsModerator() && !q.person.IsAdmin() { return fail(http.StatusForbidden, "Only admins can ban moderators.") } reason := strings.TrimSpace(q.r.PostFormValue("reason")) if reason == "" || len(reason) > 500 { return fail(http.StatusBadRequest, "Give a reason (up to 500 characters); the person sees it.") } if err := s.Store.SetBanned(s.ctx(q), p.ID, true, reason); err != nil { return err } s.Realtime.Disconnect(p.ID) s.Store.Audit(s.ctx(q), q.person.ID, "person.ban", p.Username, map[string]any{"reason": reason}) return q.redirect(fmt.Sprintf("/console/people/%d", p.ID), "Banned "+p.Username+".") } func (s *Server) unban(q *request) error { p, err := s.personParam(q) if err != nil { return err } if err := s.Store.SetBanned(s.ctx(q), p.ID, false, ""); err != nil { return err } s.Store.Audit(s.ctx(q), q.person.ID, "person.unban", p.Username, nil) return q.redirect(fmt.Sprintf("/console/people/%d", p.ID), "Unbanned "+p.Username+".") } func (s *Server) setRole(q *request) error { p, err := s.personParam(q) if err != nil { return err } role := q.r.PostFormValue("role") if role != "user" && role != "moderator" && role != "admin" { return fail(http.StatusBadRequest, "No such role.") } if p.ID == q.person.ID { return fail(http.StatusBadRequest, "Another admin has to change your role.") } if err := s.Store.SetRole(s.ctx(q), p.ID, role); err != nil { return err } s.Store.Audit(s.ctx(q), q.person.ID, "person.role", p.Username, map[string]any{"from": p.Role, "to": role}) return q.redirect(fmt.Sprintf("/console/people/%d", p.ID), p.Username+" is now "+role+".") }