diff --git a/backend/db/report.go b/backend/db/report.go new file mode 100644 index 0000000..9162638 --- /dev/null +++ b/backend/db/report.go @@ -0,0 +1,47 @@ +package db + +import ( + "context" + "time" + + "emperror.dev/errors" + "github.com/georgysavva/scany/pgxscan" + "github.com/rs/xid" +) + +type Report struct { + ID int64 + UserID xid.ID + MemberID *xid.ID + Reason string + ReporterID xid.ID + + CreatedAt time.Time + ResolvedAt *time.Time + AdminID *xid.ID + AdminComment *string +} + +const reportPageSize = 100 + +func (db *DB) Reports(ctx context.Context, closed bool, page int) (rs []Report, err error) { + builder := sq.Select("*").From("reports").Offset(uint64(reportPageSize * page)).Limit(reportPageSize).OrderBy("id ASC") + if closed { + builder = builder.Where("resolved_at IS NOT NULL") + } else { + builder = builder.Where("resolved_at IS NULL") + } + sql, args, err := builder.ToSql() + if err != nil { + return nil, errors.Wrap(err, "building sql") + } + + err = pgxscan.Select(ctx, db, &rs, sql, args...) + if err != nil { + return nil, errors.Wrap(err, "executing query") + } + if len(rs) == 0 { + return []Report{}, nil + } + return rs, nil +} diff --git a/backend/db/user.go b/backend/db/user.go index 6e288c7..6e875fd 100644 --- a/backend/db/user.go +++ b/backend/db/user.go @@ -34,6 +34,7 @@ type User struct { FediverseInstance *string MaxInvites int + IsAdmin bool DeletedAt *time.Time SelfDelete *bool diff --git a/backend/routes.go b/backend/routes.go index f3f44e7..d97a3dc 100644 --- a/backend/routes.go +++ b/backend/routes.go @@ -5,6 +5,7 @@ import ( "codeberg.org/u1f320/pronouns.cc/backend/routes/bot" "codeberg.org/u1f320/pronouns.cc/backend/routes/member" "codeberg.org/u1f320/pronouns.cc/backend/routes/meta" + "codeberg.org/u1f320/pronouns.cc/backend/routes/mod" "codeberg.org/u1f320/pronouns.cc/backend/routes/user" "codeberg.org/u1f320/pronouns.cc/backend/server" "github.com/go-chi/chi/v5" @@ -20,5 +21,6 @@ func mountRoutes(s *server.Server) { member.Mount(s, r) bot.Mount(s, r) meta.Mount(s, r) + mod.Mount(s, r) }) } diff --git a/backend/routes/auth/discord.go b/backend/routes/auth/discord.go index 7bccfcc..63a08c1 100644 --- a/backend/routes/auth/discord.go +++ b/backend/routes/auth/discord.go @@ -107,7 +107,7 @@ func (s *Server) discordCallback(w http.ResponseWriter, r *http.Request) error { // TODO: implement user + token permissions tokenID := xid.New() - token, err := s.Auth.CreateToken(u.ID, tokenID, false, false, true) + token, err := s.Auth.CreateToken(u.ID, tokenID, u.IsAdmin, false, true) if err != nil { return err } diff --git a/backend/routes/auth/fedi_mastodon.go b/backend/routes/auth/fedi_mastodon.go index 7a29a1a..581fa32 100644 --- a/backend/routes/auth/fedi_mastodon.go +++ b/backend/routes/auth/fedi_mastodon.go @@ -128,7 +128,7 @@ func (s *Server) mastodonCallback(w http.ResponseWriter, r *http.Request) error // TODO: implement user + token permissions tokenID := xid.New() - token, err := s.Auth.CreateToken(u.ID, tokenID, false, false, true) + token, err := s.Auth.CreateToken(u.ID, tokenID, u.IsAdmin, false, true) if err != nil { return err } diff --git a/backend/routes/mod/get_reports.go b/backend/routes/mod/get_reports.go new file mode 100644 index 0000000..62707d1 --- /dev/null +++ b/backend/routes/mod/get_reports.go @@ -0,0 +1,13 @@ +package mod + +import ( + "fmt" + "net/http" +) + +func (s *Server) getReports(w http.ResponseWriter, r *http.Request) error { + showClosed := r.FormValue("closed") == "true" + + fmt.Println("closed =", showClosed) + return nil +} diff --git a/backend/routes/mod/routes.go b/backend/routes/mod/routes.go new file mode 100644 index 0000000..6d76ee0 --- /dev/null +++ b/backend/routes/mod/routes.go @@ -0,0 +1,52 @@ +package mod + +import ( + "net/http" + + "codeberg.org/u1f320/pronouns.cc/backend/server" + "github.com/go-chi/chi/v5" + "github.com/go-chi/render" +) + +type Server struct { + *server.Server +} + +func Mount(srv *server.Server, r chi.Router) { + s := &Server{Server: srv} + + r.With(MustAdmin).Route("/admin", func(r chi.Router) { + r.Get("/reports", server.WrapHandler(s.getReports)) + r.Get("/reports/by-user/{id}", nil) + r.Get("/reports/by-reporter/{id}", nil) + + r.Patch("/reports/{id}", nil) + }) +} + +func MustAdmin(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + claims, ok := server.ClaimsFromContext(r.Context()) + if !ok { + render.Status(r, http.StatusForbidden) + render.JSON(w, r, server.APIError{ + Code: server.ErrForbidden, + Message: "Forbidden", + }) + return + } + + if !claims.UserIsAdmin { + render.Status(r, http.StatusForbidden) + render.JSON(w, r, server.APIError{ + Code: server.ErrForbidden, + Message: "Forbidden", + }) + return + } + + next.ServeHTTP(w, r) + } + + return http.HandlerFunc(fn) +} diff --git a/scripts/migrate/010_reports.sql b/scripts/migrate/010_reports.sql new file mode 100644 index 0000000..90233cb --- /dev/null +++ b/scripts/migrate/010_reports.sql @@ -0,0 +1,19 @@ +-- +migrate Up + +-- 2023-03-19: Add moderation-related tables + +alter table users add column is_admin boolean not null default false; + +create table reports ( + id serial primary key, + -- we keep deleted users for 180 days after deletion, so it's fine to tie this to a user object + user_id text not null references users (id) on delete cascade, + member_id text null references members (id) on delete set null, + reason text not null, + reporter_id text not null, + + created_at timestamptz not null default now(), + resolved_at timestamptz, + admin_id text null references users (id) on delete set null, + admin_comment text +);