Compare commits

..

6 commits

77 changed files with 1046 additions and 484 deletions

View file

@ -1,13 +0,0 @@
when:
branch:
exclude: stable
steps:
check:
image: golang:alpine
commands:
- apk update && apk add curl vips-dev build-base
- make backend
# Install golangci-lint
- curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.55.2
- golangci-lint run

View file

@ -1,20 +0,0 @@
when:
branch:
exclude: stable
steps:
check:
image: node
directory: frontend
environment: # SvelteKit expects these in the environment during build time.
- PRIVATE_SENTRY_DSN=
- PUBLIC_BASE_URL=http://pronouns.localhost
- PUBLIC_MEDIA_URL=http://pronouns.localhost/media
- PUBLIC_SHORT_BASE=http://prns.localhost
- PUBLIC_HCAPTCHA_SITEKEY=non_existent_sitekey
commands:
- corepack enable
- pnpm install
- pnpm check
- pnpm lint
- pnpm build

View file

@ -2,7 +2,7 @@ all: generate backend frontend
.PHONY: backend
backend:
go build -v -o pronouns -ldflags="-buildid= -X codeberg.org/pronounscc/pronouns.cc/backend/server.Revision=`git rev-parse --short HEAD` -X codeberg.org/pronounscc/pronouns.cc/backend/server.Tag=`git describe --tags --long --always`" .
go build -v -o pronouns -ldflags="-buildid= -X codeberg.org/pronounscc/pronouns.cc/backend/server.Revision=`git rev-parse --short HEAD` -X codeberg.org/pronounscc/pronouns.cc/backend/server.Tag=`git describe --tags --long`" .
.PHONY: generate
generate:

View file

@ -37,3 +37,15 @@ func (id FlagID) Increment() uint16 { return Snowflake(id).Increment() }
func (id FlagID) MarshalJSON() ([]byte, error) { return Snowflake(id).MarshalJSON() }
func (id *FlagID) UnmarshalJSON(src []byte) error { return (*Snowflake)(id).UnmarshalJSON(src) }
type AuditLogID Snowflake
func (id AuditLogID) String() string { return Snowflake(id).String() }
func (id AuditLogID) Time() time.Time { return Snowflake(id).Time() }
func (id AuditLogID) IsValid() bool { return Snowflake(id).IsValid() }
func (id AuditLogID) Worker() uint8 { return Snowflake(id).Worker() }
func (id AuditLogID) PID() uint8 { return Snowflake(id).PID() }
func (id AuditLogID) Increment() uint16 { return Snowflake(id).Increment() }
func (id AuditLogID) MarshalJSON() ([]byte, error) { return Snowflake(id).MarshalJSON() }
func (id *AuditLogID) UnmarshalJSON(src []byte) error { return (*Snowflake)(id).UnmarshalJSON(src) }

84
backend/db/audit_log.go Normal file
View file

@ -0,0 +1,84 @@
package db
import (
"context"
"fmt"
"codeberg.org/pronounscc/pronouns.cc/backend/common"
"emperror.dev/errors"
"github.com/georgysavva/scany/v2/pgxscan"
"github.com/jackc/pgx/v5"
)
type AuditLogEntry struct {
ID common.AuditLogID
TargetUserID common.UserID
TargetMemberID *common.MemberID
ModeratorID common.UserID
ReportID *int64
Reason string
ActionTaken string
ClearedData *AuditLogClearedData
TargetUsername *string
TargetMemberName *string
ModeratorUsername *string
}
type AuditLogClearedData struct {
DisplayName *string `json:"display_name,omitempty"`
Bio *string `json:"bio,omitempty"`
Links []string `json:"links,omitempty"`
Names []FieldEntry `json:"names,omitempty"`
Pronouns []PronounEntry `json:"pronouns,omitempty"`
Fields []Field `json:"fields,omitempty"`
CustomPreferences []CustomPreference `json:"custom_preferences"`
}
// Returns a max of 100 audit log entries created before the time in `before`.
// If `before` is 0, returns the latest entries.
func (db *DB) AuditLog(ctx context.Context, before common.AuditLogID) (es []AuditLogEntry, err error) {
b := sq.Select("a.*", "u1.username as target_username", "u2.username as moderator_username", "m.name as target_member_name").
From("audit_log a").Limit(100).OrderBy("id DESC").
LeftJoin("users u1 ON a.target_user_id = u1.snowflake_id").
LeftJoin("users u2 ON a.moderator_id = u2.snowflake_id").
LeftJoin("members m ON a.target_member_id = m.snowflake_id")
if before.IsValid() {
b = b.Where("id < ?", before)
}
sql, args, err := b.ToSql()
if err != nil {
return nil, errors.Wrap(err, "building query")
}
fmt.Println(sql)
err = pgxscan.Select(ctx, db, &es, sql, args...)
if err != nil {
return nil, errors.Wrap(err, "executing query")
}
return NotNull(es), nil
}
func (db *DB) CreateAuditLogEntry(ctx context.Context, tx pgx.Tx, data AuditLogEntry) (e AuditLogEntry, err error) {
sql, args, err := sq.Insert("audit_log").SetMap(map[string]any{
"id": common.GenerateID(),
"target_user_id": data.TargetUserID,
"target_member_id": data.TargetMemberID,
"moderator_id": data.ModeratorID,
"report_id": data.ReportID,
"reason": data.Reason,
"action_taken": data.ActionTaken,
"cleared_data": data.ClearedData,
}).Suffix("RETURNING *").ToSql()
if err != nil {
return e, errors.Wrap(err, "building query")
}
err = pgxscan.Get(ctx, tx, &e, sql, args...)
if err != nil {
return e, errors.Wrap(err, "executing query")
}
return e, nil
}

View file

@ -79,7 +79,7 @@ func (db *DB) CreateExport(ctx context.Context, userID xid.ID, filename string,
return de, errors.Wrap(err, "building query")
}
err = pgxscan.Get(ctx, db, &de, sql, args...)
pgxscan.Get(ctx, db, &de, sql, args...)
if err != nil {
return de, errors.Wrap(err, "executing sql")
}

View file

@ -48,11 +48,11 @@ func (f FediverseApp) ClientConfig() *oauth2.Config {
}
func (f FediverseApp) MastodonCompatible() bool {
return f.InstanceType == "mastodon" || f.InstanceType == "pleroma" || f.InstanceType == "akkoma" || f.InstanceType == "incestoma" || f.InstanceType == "pixelfed" || f.InstanceType == "gotosocial"
return f.InstanceType == "mastodon" || f.InstanceType == "pleroma" || f.InstanceType == "akkoma" || f.InstanceType == "pixelfed" || f.InstanceType == "gotosocial"
}
func (f FediverseApp) Misskey() bool {
return f.InstanceType == "misskey" || f.InstanceType == "foundkey" || f.InstanceType == "calckey" || f.InstanceType == "firefish" || f.InstanceType == "sharkey"
return f.InstanceType == "misskey" || f.InstanceType == "foundkey" || f.InstanceType == "calckey" || f.InstanceType == "firefish"
}
const ErrNoInstanceApp = errors.Sentinel("instance doesn't have an app")

View file

@ -6,7 +6,6 @@ import (
"encoding/base64"
"time"
"codeberg.org/pronounscc/pronouns.cc/backend/log"
"emperror.dev/errors"
"github.com/georgysavva/scany/v2/pgxscan"
"github.com/jackc/pgx/v5"
@ -44,12 +43,7 @@ func (db *DB) CreateInvite(ctx context.Context, userID xid.ID) (i Invite, err er
if err != nil {
return i, errors.Wrap(err, "beginning transaction")
}
defer func() {
err := tx.Rollback(ctx)
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
log.Error("rolling back transaction:", err)
}
}()
defer tx.Rollback(ctx)
var maxInvites, inviteCount int
err = tx.QueryRow(ctx, "SELECT max_invites FROM users WHERE id = $1", userID).Scan(&maxInvites)

View file

@ -7,7 +7,6 @@ import (
"time"
"codeberg.org/pronounscc/pronouns.cc/backend/common"
"codeberg.org/pronounscc/pronouns.cc/backend/log"
"emperror.dev/errors"
"github.com/Masterminds/squirrel"
"github.com/georgysavva/scany/v2/pgxscan"
@ -288,12 +287,7 @@ func (db *DB) RerollMemberSID(ctx context.Context, userID, memberID xid.ID) (new
if err != nil {
return "", errors.Wrap(err, "beginning transaction")
}
defer func() {
err := tx.Rollback(ctx)
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
log.Error("rolling back transaction:", err)
}
}()
defer tx.Rollback(ctx)
sql, args, err := sq.Update("members").
Set("sid", squirrel.Expr("find_free_member_sid()")).

View file

@ -174,7 +174,7 @@ func (db *DB) CreateWarning(ctx context.Context, tx pgx.Tx, userID xid.ID, reaso
sql, args, err := sq.Insert("warnings").SetMap(map[string]any{
"user_id": userID,
"reason": reason,
}).ToSql()
}).Suffix("RETURNING *").ToSql()
if err != nil {
return w, errors.Wrap(err, "building sql")
}

View file

@ -26,8 +26,7 @@ var Command = &cli.Command{
func run(c *cli.Context) error {
// initialize sentry
if dsn := os.Getenv("SENTRY_DSN"); dsn != "" {
// We don't need to check the error here--it's fine if no DSN is set.
_ = sentry.Init(sentry.ClientOptions{
sentry.Init(sentry.ClientOptions{
Dsn: dsn,
Debug: os.Getenv("DEBUG") == "true",
Release: server.Tag,

View file

@ -6,6 +6,7 @@ import (
"codeberg.org/pronounscc/pronouns.cc/backend/routes/v1/meta"
"codeberg.org/pronounscc/pronouns.cc/backend/routes/v1/mod"
"codeberg.org/pronounscc/pronouns.cc/backend/routes/v1/user"
admin2 "codeberg.org/pronounscc/pronouns.cc/backend/routes/v2/admin"
user2 "codeberg.org/pronounscc/pronouns.cc/backend/routes/v2/user"
"codeberg.org/pronounscc/pronouns.cc/backend/server"
"github.com/go-chi/chi/v5"
@ -26,5 +27,6 @@ func mountRoutes(s *server.Server) {
s.Router.Route("/v2", func(r chi.Router) {
user2.Mount(s, r)
admin2.Mount(s, r)
})
}

View file

@ -11,7 +11,6 @@ import (
"emperror.dev/errors"
"github.com/bwmarrin/discordgo"
"github.com/go-chi/render"
"github.com/jackc/pgx/v5"
"github.com/mediocregopher/radix/v4"
"github.com/rs/xid"
"golang.org/x/oauth2"
@ -292,12 +291,7 @@ func (s *Server) discordSignup(w http.ResponseWriter, r *http.Request) error {
if err != nil {
return errors.Wrap(err, "beginning transaction")
}
defer func() {
err := tx.Rollback(ctx)
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
log.Error("rolling back transaction:", err)
}
}()
defer tx.Rollback(ctx)
du := new(discordgo.User)
err = s.DB.GetJSON(ctx, "discord:"+req.Ticket, &du)

View file

@ -11,7 +11,6 @@ import (
"codeberg.org/pronounscc/pronouns.cc/backend/server"
"emperror.dev/errors"
"github.com/go-chi/render"
"github.com/jackc/pgx/v5"
"github.com/mediocregopher/radix/v4"
"github.com/rs/xid"
)
@ -320,12 +319,7 @@ func (s *Server) mastodonSignup(w http.ResponseWriter, r *http.Request) error {
if err != nil {
return errors.Wrap(err, "beginning transaction")
}
defer func() {
err := tx.Rollback(ctx)
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
log.Error("rolling back transaction:", err)
}
}()
defer tx.Rollback(ctx)
mu := new(partialMastodonAccount)
err = s.DB.GetJSON(ctx, "mastodon:"+req.Ticket, &mu)

View file

@ -12,7 +12,6 @@ import (
"codeberg.org/pronounscc/pronouns.cc/backend/server"
"emperror.dev/errors"
"github.com/go-chi/render"
"github.com/jackc/pgx/v5"
"github.com/mediocregopher/radix/v4"
"github.com/rs/xid"
)
@ -248,12 +247,7 @@ func (s *Server) misskeySignup(w http.ResponseWriter, r *http.Request) error {
if err != nil {
return errors.Wrap(err, "beginning transaction")
}
defer func() {
err := tx.Rollback(ctx)
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
log.Error("rolling back transaction:", err)
}
}()
defer tx.Rollback(ctx)
mu := new(partialMisskeyAccount)
err = s.DB.GetJSON(ctx, "misskey:"+req.Ticket, &mu)

View file

@ -68,10 +68,13 @@ func (s *Server) noAppFediverseURL(ctx context.Context, w http.ResponseWriter, r
case "iceshrimp":
softwareName = "firefish"
fallthrough
case "misskey", "foundkey", "calckey", "firefish", "sharkey":
case "misskey", "foundkey", "calckey", "firefish":
return s.noAppMisskeyURL(ctx, w, r, softwareName, instance)
case "mastodon", "pleroma", "akkoma", "incestoma", "pixelfed", "gotosocial":
case "mastodon", "pleroma", "akkoma", "pixelfed", "gotosocial":
case "glitchcafe", "hometown":
// plural.cafe (potentially other instances too?) runs Mastodon but changes the software name
// Hometown is a lightweight fork of Mastodon so we can just treat it the same
// changing it back to mastodon here for consistency
softwareName = "mastodon"
default:
return server.APIError{Code: server.ErrUnsupportedInstance}

View file

@ -10,7 +10,6 @@ import (
"codeberg.org/pronounscc/pronouns.cc/backend/server"
"emperror.dev/errors"
"github.com/go-chi/render"
"github.com/jackc/pgx/v5"
"github.com/mediocregopher/radix/v4"
"github.com/rs/xid"
"golang.org/x/oauth2"
@ -295,12 +294,7 @@ func (s *Server) googleSignup(w http.ResponseWriter, r *http.Request) error {
if err != nil {
return errors.Wrap(err, "beginning transaction")
}
defer func() {
err := tx.Rollback(ctx)
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
log.Error("rolling back transaction:", err)
}
}()
defer tx.Rollback(ctx)
gu := new(partialGoogleUser)
err = s.DB.GetJSON(ctx, "google:"+req.Ticket, &gu)

View file

@ -185,7 +185,7 @@ func (s *Server) oauthURLs(w http.ResponseWriter, r *http.Request) error {
if googleOAuthConfig.ClientID != "" {
googleCfg := googleOAuthConfig
googleCfg.RedirectURL = req.CallbackDomain + "/auth/login/google"
resp.Google = googleCfg.AuthCodeURL(state) + "&prompt=select_account"
resp.Google = googleCfg.AuthCodeURL(state)
}
render.JSON(w, r, resp)

View file

@ -5,11 +5,9 @@ import (
"time"
"codeberg.org/pronounscc/pronouns.cc/backend/db"
"codeberg.org/pronounscc/pronouns.cc/backend/log"
"codeberg.org/pronounscc/pronouns.cc/backend/server"
"emperror.dev/errors"
"github.com/go-chi/render"
"github.com/jackc/pgx/v5"
"github.com/rs/xid"
)
@ -65,12 +63,7 @@ func (s *Server) deleteToken(w http.ResponseWriter, r *http.Request) error {
if err != nil {
return errors.Wrap(err, "beginning transaction")
}
defer func() {
err := tx.Rollback(ctx)
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
log.Error("rolling back transaction:", err)
}
}()
defer tx.Rollback(ctx)
err = s.DB.InvalidateAllTokens(ctx, tx, claims.UserID)
if err != nil {

View file

@ -12,7 +12,6 @@ import (
"codeberg.org/pronounscc/pronouns.cc/backend/server"
"emperror.dev/errors"
"github.com/go-chi/render"
"github.com/jackc/pgx/v5"
"github.com/mediocregopher/radix/v4"
"github.com/rs/xid"
"golang.org/x/oauth2"
@ -328,12 +327,7 @@ func (s *Server) tumblrSignup(w http.ResponseWriter, r *http.Request) error {
if err != nil {
return errors.Wrap(err, "beginning transaction")
}
defer func() {
err := tx.Rollback(ctx)
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
log.Error("rolling back transaction:", err)
}
}()
defer tx.Rollback(ctx)
tui := new(tumblrUserInfo)
err = s.DB.GetJSON(ctx, "tumblr:"+req.Ticket, &tui)

View file

@ -11,7 +11,6 @@ import (
"codeberg.org/pronounscc/pronouns.cc/backend/server"
"emperror.dev/errors"
"github.com/go-chi/render"
"github.com/jackc/pgx/v5"
)
type CreateMemberRequest struct {
@ -120,12 +119,7 @@ func (s *Server) createMember(w http.ResponseWriter, r *http.Request) (err error
if err != nil {
return errors.Wrap(err, "starting transaction")
}
defer func() {
err := tx.Rollback(ctx)
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
log.Error("rolling back transaction:", err)
}
}()
defer tx.Rollback(ctx)
m, err := s.DB.CreateMember(ctx, tx, claims.UserID, cmr.Name, cmr.DisplayName, cmr.Bio, cmr.Links)
if err != nil {

View file

@ -13,7 +13,6 @@ import (
"emperror.dev/errors"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"github.com/jackc/pgx/v5"
"github.com/rs/xid"
)
@ -247,12 +246,7 @@ func (s *Server) patchMember(w http.ResponseWriter, r *http.Request) error {
log.Errorf("creating transaction: %v", err)
return errors.Wrap(err, "creating transaction")
}
defer func() {
err := tx.Rollback(ctx)
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
log.Error("rolling back transaction:", err)
}
}()
defer tx.Rollback(ctx)
m, err = s.DB.UpdateMember(ctx, tx, m.ID, req.Name, req.DisplayName, req.Bio, req.Unlisted, req.Links, avatarHash)
if err != nil {

View file

@ -10,7 +10,6 @@ import (
"emperror.dev/errors"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"github.com/jackc/pgx/v5"
)
type resolveReportRequest struct {
@ -44,12 +43,7 @@ func (s *Server) resolveReport(w http.ResponseWriter, r *http.Request) error {
log.Errorf("creating transaction: %v", err)
return errors.Wrap(err, "creating transaction")
}
defer func() {
err := tx.Rollback(ctx)
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
log.Error("rolling back transaction:", err)
}
}()
defer tx.Rollback(ctx)
report, err := s.DB.Report(ctx, tx, id)
if err != nil {

View file

@ -3,11 +3,9 @@ package user
import (
"net/http"
"codeberg.org/pronounscc/pronouns.cc/backend/log"
"codeberg.org/pronounscc/pronouns.cc/backend/server"
"emperror.dev/errors"
"github.com/go-chi/render"
"github.com/jackc/pgx/v5"
)
func (s *Server) deleteUser(w http.ResponseWriter, r *http.Request) error {
@ -22,12 +20,7 @@ func (s *Server) deleteUser(w http.ResponseWriter, r *http.Request) error {
if err != nil {
return errors.Wrap(err, "creating transaction")
}
defer func() {
err := tx.Rollback(ctx)
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
log.Error("rolling back transaction:", err)
}
}()
defer tx.Rollback(ctx)
err = s.DB.DeleteUser(ctx, tx, claims.UserID, true, "")
if err != nil {

View file

@ -13,7 +13,6 @@ import (
"emperror.dev/errors"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"github.com/jackc/pgx/v5"
"github.com/rs/xid"
)
@ -81,12 +80,7 @@ func (s *Server) postUserFlag(w http.ResponseWriter, r *http.Request) error {
if err != nil {
return errors.Wrap(err, "starting transaction")
}
defer func() {
err := tx.Rollback(ctx)
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
log.Error("rolling back transaction:", err)
}
}()
defer tx.Rollback(ctx)
flag, err := s.DB.CreateFlag(ctx, tx, claims.UserID, req.Name, req.Description)
if err != nil {
@ -198,12 +192,7 @@ func (s *Server) patchUserFlag(w http.ResponseWriter, r *http.Request) error {
if err != nil {
return errors.Wrap(err, "beginning transaction")
}
defer func() {
err := tx.Rollback(ctx)
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
log.Error("rolling back transaction:", err)
}
}()
defer tx.Rollback(ctx)
flag, err = s.DB.EditFlag(ctx, tx, flag.ID, req.Name, req.Description, nil)
if err != nil {

View file

@ -12,7 +12,6 @@ import (
"emperror.dev/errors"
"github.com/go-chi/render"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/rs/xid"
)
@ -222,12 +221,7 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error {
log.Errorf("creating transaction: %v", err)
return errors.Wrap(err, "creating transaction")
}
defer func() {
err := tx.Rollback(ctx)
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
log.Error("rolling back transaction:", err)
}
}()
defer tx.Rollback(ctx)
// update username
if req.Username != nil && *req.Username != u.Username {

View file

@ -0,0 +1,72 @@
package admin
import (
"net/http"
"codeberg.org/pronounscc/pronouns.cc/backend/common"
"codeberg.org/pronounscc/pronouns.cc/backend/db"
"codeberg.org/pronounscc/pronouns.cc/backend/log"
"codeberg.org/pronounscc/pronouns.cc/backend/server"
"emperror.dev/errors"
"github.com/go-chi/render"
)
type AuditLogEntryResponse struct {
ID common.AuditLogID `json:"id"`
TargetUserID common.UserID `json:"target_user_id"`
TargetMemberID *common.MemberID `json:"target_member_id"`
ModeratorID common.UserID `json:"moderator_id"`
ReportID *int64 `json:"report_id"`
Reason string `json:"reason"`
ActionTaken string `json:"action_taken"`
ClearedData *db.AuditLogClearedData `json:"cleared_data"`
TargetUsername *string `json:"target_name"`
TargetMemberName *string `json:"target_member_name"`
ModeratorUsername *string `json:"moderator_name"`
}
func dbAuditLogToResponse(e db.AuditLogEntry) AuditLogEntryResponse {
return AuditLogEntryResponse{
ID: e.ID,
TargetUserID: e.TargetUserID,
TargetMemberID: e.TargetMemberID,
ModeratorID: e.ModeratorID,
ReportID: e.ReportID,
Reason: e.Reason,
ActionTaken: e.ActionTaken,
ClearedData: e.ClearedData,
TargetUsername: e.TargetUsername,
TargetMemberName: e.TargetMemberName,
ModeratorUsername: e.ModeratorUsername,
}
}
func dbAuditLogsToResponse(es []db.AuditLogEntry) []AuditLogEntryResponse {
resps := make([]AuditLogEntryResponse, len(es))
for i := range es {
resps[i] = dbAuditLogToResponse(es[i])
}
return resps
}
func (s *Server) AuditLog(w http.ResponseWriter, r *http.Request) (err error) {
beforeID := common.AuditLogID(0)
if s := r.FormValue("before"); s != "" {
sf, err := common.ParseSnowflake(s)
if err != nil {
return server.NewV2Error(server.ErrBadRequest, "", server.NewModelParseError("query", "Invalid snowflake"))
}
beforeID = common.AuditLogID(sf)
}
es, err := s.DB.AuditLog(r.Context(), beforeID)
if err != nil {
log.Errorf("getting audit logs before %v: %v", beforeID, err)
return errors.Wrap(err, "getting audit logs")
}
render.JSON(w, r, dbAuditLogsToResponse(es))
return nil
}

View file

@ -0,0 +1,55 @@
package admin
import (
"net/http"
"codeberg.org/pronounscc/pronouns.cc/backend/server"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
)
const (
ActionTypeIgnore = "ignore"
ActionTypeWarn = "warn"
ActionTypeSuspend = "suspend"
)
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("/audit-log", server.WrapHandler(s.AuditLog))
r.Post("/users/{id}/actions", server.WrapHandler(s.UserAction))
})
}
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)
}

View file

@ -0,0 +1,375 @@
package admin
import (
"context"
"net/http"
"sort"
"codeberg.org/pronounscc/pronouns.cc/backend/common"
"codeberg.org/pronounscc/pronouns.cc/backend/db"
"codeberg.org/pronounscc/pronouns.cc/backend/log"
"codeberg.org/pronounscc/pronouns.cc/backend/server"
"emperror.dev/errors"
"github.com/aarondl/opt/omit"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"github.com/google/uuid"
)
const MaxReasonLength = 2048
type UserActionRequest struct {
Type string `json:"type"` // `ignore`, `warn`, `suspend`
ReportID omit.Val[int64] `json:"report_id"` // The report ID associated with this action, only required if type is ignore. Will close this report.
Reason string `json:"reason"` // The reason for the action. Always logged for moderators, only shown to user for `warn` and `suspend`
Clear struct { // Profile fields to clear
DisplayName bool `json:"display_name"`
Bio bool `json:"bio"`
Links bool `json:"links"`
Names []int `json:"names"` // Indexes of names to clear
Pronouns []int `json:"pronouns"` // Indexes of pronouns to clear
Fields []int `json:"fields"` // Indexes of fields to clear
CustomPreferences []uuid.UUID `json:"custom_preferences"` // Custom preference IDs to clear
// TODO: remove flags, maybe?
} `json:"clear"`
}
type UserActionResponse struct {
AuditLogID common.AuditLogID `json:"audit_log_id"`
}
func (req UserActionRequest) Validate(u db.User, fields []db.Field) (errs []server.ModelParseError) {
switch req.Type {
case ActionTypeIgnore, ActionTypeWarn, ActionTypeSuspend:
default:
errs = append(errs,
server.NewModelParseErrorWithValues("type", "", []any{ActionTypeIgnore, ActionTypeSuspend, ActionTypeWarn}, req.Type))
}
if req.Type != ActionTypeIgnore && req.Reason == "" {
errs = append(errs, server.NewModelParseError("reason", "reason cannot be empty if type is not ignore"))
}
if req.Type == ActionTypeIgnore && req.ReportID.IsUnset() {
errs = append(errs, server.NewModelParseError("report_id", "report_id cannot be empty if type is ignore"))
}
if req.Type == ActionTypeIgnore &&
(req.Clear.DisplayName || req.Clear.Bio || req.Clear.Links ||
len(req.Clear.Names) != 0 || len(req.Clear.Pronouns) != 0 ||
len(req.Clear.Fields) != 0 || len(req.Clear.CustomPreferences) != 0) {
errs = append(errs, server.NewModelParseError("clear", "clear cannot be set if report is being ignored"))
}
// check names
if len(req.Clear.Names) > len(u.Names) {
errs = append(errs, server.NewModelParseError("clear.names", "cannot have more indexes than there are names"))
}
if !allUnique(req.Clear.Names) {
errs = append(errs, server.NewModelParseError("clear.names", "all indexes in clear.names must be unique"))
}
namesOutOfRange := false
for _, i := range req.Clear.Names {
if i >= len(u.Names) {
namesOutOfRange = true
break
}
}
if namesOutOfRange {
errs = append(errs, server.NewModelParseError("clear.names", "one or more indexes is out of range"))
}
// check pronouns
if len(req.Clear.Pronouns) > len(u.Pronouns) {
errs = append(errs, server.NewModelParseError("clear.pronouns", "cannot have more indexes than there are pronouns"))
}
if !allUnique(req.Clear.Pronouns) {
errs = append(errs, server.NewModelParseError("clear.pronouns", "all indexes in clear.pronouns must be unique"))
}
pronounsOutOfRange := false
for _, i := range req.Clear.Pronouns {
if i >= len(u.Pronouns) {
pronounsOutOfRange = true
break
}
}
if pronounsOutOfRange {
errs = append(errs, server.NewModelParseError("clear.pronouns", "one or more indexes is out of range"))
}
// check fields
if len(req.Clear.Fields) > len(fields) {
errs = append(errs, server.NewModelParseError("clear.fields", "cannot have more indexes than there are fields"))
}
if !allUnique(req.Clear.Fields) {
errs = append(errs, server.NewModelParseError("clear.fields", "all indexes in clear.fields must be unique"))
}
fieldsOutOfRange := false
for _, i := range req.Clear.Fields {
if i >= len(fields) {
fieldsOutOfRange = true
break
}
}
if fieldsOutOfRange {
errs = append(errs, server.NewModelParseError("clear.fields", "one or more indexes is out of range"))
}
// check custom preferences
if !allUnique(req.Clear.CustomPreferences) {
errs = append(errs, server.NewModelParseError("clear.custom_preferences", "all IDs in clear.custom_preferences must be unique"))
}
invalidIDs := false
for _, id := range req.Clear.CustomPreferences {
if _, ok := u.CustomPreferences[id.String()]; !ok {
invalidIDs = true
break
}
}
if invalidIDs {
errs = append(errs, server.NewModelParseError("clear.custom_preferences", "unknown ID specified"))
}
return errs
}
func (req UserActionRequest) HasClear() bool {
return req.Clear.DisplayName || req.Clear.Bio || req.Clear.Links ||
len(req.Clear.Names) != 0 || len(req.Clear.Pronouns) != 0 ||
len(req.Clear.Fields) != 0 || len(req.Clear.CustomPreferences) != 0
}
func (s *Server) UserAction(w http.ResponseWriter, r *http.Request) (err error) {
ctx := r.Context()
claims, _ := server.ClaimsFromContext(ctx)
mod, err := s.DB.User(ctx, claims.UserID)
if err != nil {
return errors.Wrap(err, "getting moderator data")
}
// Parse the user we're taking action against
target, err := s.ParseUser(ctx, chi.URLParam(r, "id"))
if err != nil {
return server.NewV2Error(server.ErrUserNotFound, "")
}
fields, err := s.DB.UserFields(ctx, target.ID)
if err != nil {
log.Errorf("getting fields for user %v: %v", target.SnowflakeID, fields)
return server.NewV2Error(server.ErrInternalServerError, "")
}
req, err := server.Decode[UserActionRequest](r)
if err != nil {
return err
}
// validate input
if errs := req.Validate(target, fields); len(errs) != 0 {
return server.NewV2Error(server.ErrBadRequest, "", errs...)
}
// Store removed names for the audit log
var (
deletedNames []db.FieldEntry
deletedPronouns []db.PronounEntry
deletedFields []db.Field
deletedPreferences []db.CustomPreference
)
// Remove names
// Sort to reverse order, so elements don't shift indexes before we remove them
sort.Sort(sort.Reverse(sort.IntSlice(req.Clear.Names)))
for _, i := range req.Clear.Names {
var deleted db.FieldEntry
target.Names, deleted = deleteIndex(target.Names, i)
deletedNames = append(deletedNames, deleted)
}
// Remove pronouns
sort.Sort(sort.Reverse(sort.IntSlice(req.Clear.Pronouns)))
for _, i := range req.Clear.Pronouns {
var deleted db.PronounEntry
target.Pronouns, deleted = deleteIndex(target.Pronouns, i)
deletedPronouns = append(deletedPronouns, deleted)
}
// Remove fields
sort.Sort(sort.Reverse(sort.IntSlice(req.Clear.Fields)))
for _, i := range req.Clear.Fields {
var deleted db.Field
fields, deleted = deleteIndex(fields, i)
deletedFields = append(deletedFields, deleted)
}
// Remove custom preferences
for _, k := range req.Clear.CustomPreferences {
deletedPreferences = append(deletedPreferences, target.CustomPreferences[k.String()])
delete(target.CustomPreferences, k.String())
}
tx, err := s.DB.Begin(ctx)
if err != nil {
log.Errorf("starting transaction: %v", err)
return errors.Wrap(err, "starting transaction")
}
defer tx.Rollback(ctx)
// If report_id is set, check whether the report exists.
var reportID *int64
if id, ok := req.ReportID.Get(); ok {
reportID = &id
_, err := s.DB.Report(ctx, tx, *reportID)
if err != nil {
if err == db.ErrReportNotFound {
return server.NewV2Error(server.ErrBadRequest, "", server.NewModelParseError("report_id", "invalid report ID"))
}
log.Errorf("getting report: %v", err)
return errors.Wrap(err, "getting report")
}
}
// Build the cleared data object
var clearedData *db.AuditLogClearedData
if req.HasClear() {
clearedData = &db.AuditLogClearedData{
Names: deletedNames,
Pronouns: deletedPronouns,
Fields: deletedFields,
CustomPreferences: deletedPreferences,
}
if req.Clear.DisplayName {
clearedData.DisplayName = target.DisplayName
}
if req.Clear.Bio {
clearedData.Bio = target.Bio
}
if req.Clear.Links {
clearedData.Links = target.Links
}
}
// Make sure there's a non-empty reason
reason := req.Reason
if req.Reason == "" {
reason = "None"
}
// Create audit log entry
auditLogEntry, err := s.DB.CreateAuditLogEntry(ctx, tx, db.AuditLogEntry{
TargetUserID: target.SnowflakeID,
TargetMemberID: nil,
ModeratorID: mod.SnowflakeID,
ReportID: reportID,
Reason: reason,
ActionTaken: req.Type,
ClearedData: clearedData,
})
if err != nil {
log.Errorf("creating audit log entry: %v", err)
return errors.Wrap(err, "creating audit log entry")
}
// Resolve report, if an ID was given
if req.ReportID.IsSet() {
err = s.DB.ResolveReport(ctx, tx, req.ReportID.GetOrZero(), mod.ID, req.Reason)
if err != nil {
log.Errorf("resolving report: %v", err)
return errors.Wrap(err, "resolving report")
}
}
// Create user warning
if req.Type == ActionTypeWarn || req.Type == ActionTypeSuspend {
_, err = s.DB.CreateWarning(ctx, tx, target.ID, req.Reason)
if err != nil {
log.Errorf("creating warning: %v", err)
return errors.Wrap(err, "creating warning")
}
}
if req.Type == ActionTypeSuspend {
err = s.DB.DeleteUser(ctx, tx, target.ID, false, req.Reason)
if err != nil {
log.Errorf("suspending user: %v", err)
return errors.Wrap(err, "suspending user")
}
}
// If we're clearing some data, do that
if req.HasClear() {
var displayName *string
if req.Clear.DisplayName {
displayName = ptr("")
}
var bio *string
if req.Clear.Bio {
bio = ptr("")
}
var links *[]string
if req.Clear.Links {
links = &[]string{}
}
// Update most columns
_, err = s.DB.UpdateUser(ctx, tx, target.ID, displayName, bio, nil, nil, links, nil, nil, &target.CustomPreferences)
if err != nil {
log.Errorf("updating user: %v", err)
return errors.Wrap(err, "updating user")
}
// Set fields
err = s.DB.SetUserFields(ctx, tx, target.ID, fields)
if err != nil {
log.Errorf("updating fields: %v", err)
return errors.Wrap(err, "updating fields")
}
// Set names/pronouns
err = s.DB.SetUserNamesPronouns(ctx, tx, target.ID, target.Names, target.Pronouns)
if err != nil {
log.Errorf("updating names/pronouns: %v", err)
return errors.Wrap(err, "updating names/pronouns")
}
}
// Commit transaction and save everything
err = tx.Commit(ctx)
if err != nil {
log.Errorf("committing transaction: %v", err)
return errors.Wrap(err, "committing transaction")
}
render.JSON(w, r, UserActionResponse{
AuditLogID: auditLogEntry.ID,
})
return nil
}
func (s *Server) ParseUser(ctx context.Context, id string) (u db.User, err error) {
sf, err := common.ParseSnowflake(id)
if err != nil {
return u, err
}
return s.DB.UserBySnowflake(ctx, common.UserID(sf))
}
func allUnique[T comparable](slice []T) bool {
m := make(map[T]struct{})
for _, entry := range slice {
_, ok := m[entry]
if ok {
return false
}
m[entry] = struct{}{}
}
return true
}
func deleteIndex[T any](slice []T, idx int) ([]T, T) {
deleted := slice[idx]
return append(slice[:idx], slice[idx+1:]...), deleted
}
func ptr[T any](t T) *T {
return &t
}

121
backend/server/error_v2.go Normal file
View file

@ -0,0 +1,121 @@
package server
import (
"encoding/json"
"fmt"
json2 "github.com/aarondl/json"
"github.com/getsentry/sentry-go"
)
// An error returned by version 2 of the API.
type APIError2 struct {
Code int `json:"code"`
ID *sentry.EventID `json:"id,omitempty"`
Message string `json:"message,omitempty"`
Details string `json:"details,omitempty"`
Errors map[string][]ModelParseError `json:"errors,omitempty"`
RatelimitReset *int `json:"ratelimit_reset,omitempty"`
Status int `json:"-"`
}
func (e APIError2) Error() string {
if e.Message == "" {
e.Message = errCodeMessages[e.Code]
}
if e.Details != "" {
return fmt.Sprintf("%s (code: %d) (%s)", e.Message, e.Code, e.Details)
}
return fmt.Sprintf("%s (code: %d)", e.Message, e.Code)
}
// Returns a new error in APIv2 format.
func NewV2Error(code int, details string, parseErrors ...ModelParseError) APIError2 {
var errors map[string][]ModelParseError
if len(parseErrors) != 0 {
errors = make(map[string][]ModelParseError)
for _, p := range parseErrors {
errors[p.Key] = append(errors[p.Key], p)
}
}
return APIError2{
Code: code,
Message: errCodeMessages[code],
Details: details,
Errors: errors,
Status: errCodeStatuses[code],
}
}
type ModelParseError struct {
Message string `json:"message,omitempty"`
MaxLength int `json:"max_length,omitempty"`
ActualLength int `json:"actual_length,omitempty"`
ExpectedValues []any `json:"expected_values,omitempty"`
ActualValue any `json:"actual_value,omitempty"`
ExpectedType string `json:"expected_type,omitempty"`
ActualType string `json:"actual_type,omitempty"`
Key string `json:"-"`
}
func NewModelParseError(key, message string) ModelParseError {
return NewModelParseErrorWithLength(key, message, 0, 0)
}
func NewModelParseErrorWithLength(key, message string, maxLength, actualLength int) ModelParseError {
return ModelParseError{
Key: key,
Message: message,
MaxLength: maxLength,
ActualLength: actualLength,
}
}
func NewModelParseErrorWithValues(key, message string, expectedValues []any, actualValue any) ModelParseError {
return ModelParseError{
Key: key,
Message: message,
ExpectedValues: expectedValues,
ActualValue: actualValue,
}
}
func ParseJSONError(err error) *ModelParseError {
switch pe := err.(type) {
case *json.UnmarshalTypeError:
key := pe.Field
if key == "" {
key = "parse"
}
return &ModelParseError{
Key: key,
ExpectedType: pe.Type.String(),
ActualType: pe.Value,
}
case *json2.UnmarshalTypeError:
// TODO: remove the need for this, somehow
// This seems to be a bug in the
key := pe.Field
if key == "" {
key = "parse"
}
return &ModelParseError{
Key: key,
Message: "Invalid type passed for a field. Make sure your types match those in the documentation.",
ExpectedType: pe.Type.String(),
ActualType: pe.Value,
}
}
return nil
}

View file

@ -28,6 +28,10 @@ func WrapHandler(hn func(w http.ResponseWriter, r *http.Request) error) http.Han
if apiErr, ok := err.(APIError); ok {
apiErr.prepare()
render.Status(r, apiErr.Status)
render.JSON(w, r, apiErr)
return
} else if apiErr, ok := err.(APIError2); ok {
render.Status(r, apiErr.Status)
render.JSON(w, r, apiErr)
return
@ -43,7 +47,6 @@ func WrapHandler(hn func(w http.ResponseWriter, r *http.Request) error) http.Han
if isExpectedError(err) {
log.Infof("expected error in handler for %v %v, ignoring", rctx.RouteMethod, rctx.RoutePattern())
} else {
log.Errorf("error in handler for %v %v: %v", rctx.RouteMethod, rctx.RoutePattern(), err)
eventID = hub.CaptureException(err)
}
apiErr := APIError{ID: eventID, Code: ErrInternalServerError}

View file

@ -100,23 +100,23 @@ func New() (*Server, error) {
// set scopes
// users
_ = rateLimiter.Scope("GET", "/users/*", 60)
_ = rateLimiter.Scope("PATCH", "/users/@me", 10)
rateLimiter.Scope("GET", "/users/*", 60)
rateLimiter.Scope("PATCH", "/users/@me", 10)
// members
_ = rateLimiter.Scope("GET", "/users/*/members", 60)
_ = rateLimiter.Scope("GET", "/users/*/members/*", 60)
rateLimiter.Scope("GET", "/users/*/members", 60)
rateLimiter.Scope("GET", "/users/*/members/*", 60)
_ = rateLimiter.Scope("POST", "/members", 10)
_ = rateLimiter.Scope("GET", "/members/*", 60)
_ = rateLimiter.Scope("PATCH", "/members/*", 20)
_ = rateLimiter.Scope("DELETE", "/members/*", 5)
rateLimiter.Scope("POST", "/members", 10)
rateLimiter.Scope("GET", "/members/*", 60)
rateLimiter.Scope("PATCH", "/members/*", 20)
rateLimiter.Scope("DELETE", "/members/*", 5)
// auth
_ = rateLimiter.Scope("*", "/auth/*", 20)
_ = rateLimiter.Scope("*", "/auth/tokens", 10)
_ = rateLimiter.Scope("*", "/auth/invites", 10)
_ = rateLimiter.Scope("POST", "/auth/discord/*", 10)
rateLimiter.Scope("*", "/auth/*", 20)
rateLimiter.Scope("*", "/auth/tokens", 10)
rateLimiter.Scope("*", "/auth/invites", 10)
rateLimiter.Scope("POST", "/auth/discord/*", 10)
s.Router.Use(rateLimiter.Handler())
@ -154,3 +154,21 @@ type ctxKey int
const (
ctxKeyClaims ctxKey = 1
)
// Decodes a JSON request into a value of T.
// If an error is returned, transform that into a ModelParseError.
// Known issue: parse errors in `github.com/aarondl/opt` types don't return a field name,
// so those are put in the `parse` key of the returned error.
func Decode[T any](r *http.Request) (T, error) {
var t T
err := render.Decode(r, &t)
if err != nil {
pe := ParseJSONError(err)
if pe != nil {
return t, NewV2Error(ErrBadRequest, "", *pe)
}
return t, NewV2Error(ErrBadRequest, "")
}
return t, nil
}

View file

@ -0,0 +1,72 @@
# Admin endpoints (v2)
## Audit log object
| Field | Type | Description |
| ------------------ | ------------------- | ----------------------------------------------------------------------------------- |
| id | snowflake | This action's ID |
| target_user_id | snowflake | The target user of this action. This may not be a valid user. |
| target_member_id | snowflake? | The target member of this action. This may not be a valid member, even if not null. |
| moderator_id | snowflake | The moderator that took this action. This may not be a valid user. |
| report_id | int? | The report closed by this action |
| reason | string | The reason this action was taken |
| action_taken | string | The type of action taken. May be 'ignore', 'warn', or 'suspend'. |
| cleared_data | cleared data object | Any user/member data cleared by this action |
| target_name | string? | The target user's username |
| target_member_name | string? | The target member's name |
| moderator_name | string? | The moderator's name |
### Cleared data object
| Field | Type | Description |
| ------------------ | --------------------------------------------- | ----------------------------------------------------------- |
| display_name | ?string | The previously set display name |
| bio | ?string | The previously set bio |
| links | ?string[] | The previously set links |
| names | ?[field_entry](../#field-entry)[] | The previously set names |
| pronouns | ?[pronoun_entry](../#pronoun-entry)[] | The previously set pronouns |
| fields | ?[field](../#field)[] | The previously set fields |
| custom_preferences | ?[custom_preference](../#custom-preference)[] | The previously set custom preferences _(user actions only)_ |
## Endpoints
### Get audit logs
#### `GET /admin/audit-log`
**Requires authentication.** Gets a list of audit log entries. By default, returns the latest 100 entries;
to page through results, use the `before` query parameter.
Returns an array of [audit log objects](./admin#audit-log-object) on success.
#### Query parameters
| Name | Type | Description |
| ------ | --------- | -------------------------------------------- |
| before | snowflake | Only get entries with IDs lower than this ID |
### Take action on a user
#### `POST /admin/users/{id}/actions`
**Requires authentication.** Take moderation actions on the given user.
#### Request body parameters
| Field | Type | Description |
| ------------------------ | ------- | ------------------------------------------------------------------------------- |
| type | string | The type of action to take. Can be 'ignore', 'warn', 'suspend'. |
| report_id | ?int | The report this action will close. Required if type is 'ignore'. |
| reason | ?string | The reason for this action. Required if type is not 'ignore'. |
| clear.display_name | bool | Whether to clear the user's display name |
| clear.bio | bool | Whether to clear the user's bio |
| clear.links | bool | Whether to clear the user's profile links |
| clear.names | int[] | Array of indexes of the user's names to clear. Must not have any duplicates. |
| clear.pronouns | int[] | Array of indexes of the user's pronouns to clear. Must not have any duplicates. |
| clear.fields | int[] | Array of indexes of the user's fields to clear. Must not have any duplicates. |
| clear.custom_preferences | uuid[] | Array of custom preference IDs to clear. Must not have any duplicates. |
#### Response body
| Field | Type | Description |
| ------------ | --------- | ---------------------------------- |
| audit_log_id | snowflake | The ID of the newly created action |

View file

@ -17,15 +17,4 @@ module.exports = {
es2017: true,
node: true,
},
rules: {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
destructuredArrayIgnorePattern: "^_",
varsIgnorePattern: "^_",
},
],
},
};

25
frontend/src/app.d.ts vendored
View file

@ -1,19 +1,38 @@
// See https://kit.svelte.dev/docs/types#app
import type { ErrorCode } from "$lib/api/entities";
import type { APIError, ErrorCode } from "$lib/api/entities";
// for information about these interfaces
declare global {
namespace App {
interface Error {
type Error = {
code: ErrorCode;
message?: string | undefined;
details?: string | undefined;
}
} | APIError
// interface Locals {}
// interface PageData {}
// interface Platform {}
}
}
declare module "svelte-hcaptcha" {
import type { SvelteComponent } from "svelte";
export interface HCaptchaProps {
sitekey?: string;
apihost?: string;
hl?: string;
reCaptchaCompat?: boolean;
theme?: CaptchaTheme;
size?: string;
}
declare class HCaptcha extends SvelteComponent {
$$prop_def: HCaptchaProps;
}
export default HCaptcha;
}
export {};

View file

@ -2,9 +2,7 @@ import { PRIVATE_SENTRY_DSN } from "$env/static/private";
import * as Sentry from "@sentry/node";
import type { HandleServerError } from "@sveltejs/kit";
if (PRIVATE_SENTRY_DSN) {
Sentry.init({ dsn: PRIVATE_SENTRY_DSN });
}
Sentry.init({ dsn: PRIVATE_SENTRY_DSN });
export const handleError = (({ error, event }) => {
console.log(error);

View file

@ -1,4 +1,3 @@
/* eslint-disable no-unused-vars */
import { PUBLIC_BASE_URL, PUBLIC_MEDIA_URL } from "$env/static/public";
export const MAX_MEMBERS = 500;

View file

@ -4,7 +4,6 @@
export let urls: string[];
export let alt: string;
export let width = 300;
export let lazyLoad = false;
const contentTypeFor = (url: string) => {
if (url.endsWith(".webp")) {
@ -32,7 +31,6 @@
src={urls[0] || defaultAvatars[0]}
{alt}
class="rounded-circle img-fluid"
loading={lazyLoad ? "lazy" : "eager"}
/>
</picture>
{:else}

View file

@ -6,12 +6,12 @@
type User,
type CustomPreferences,
} from "$lib/api/entities";
import { Icon, Tooltip } from "@sveltestrap/sveltestrap";
import { Icon, Tooltip } from "@sveltestrap/sveltestrap";
import FallbackImage from "./FallbackImage.svelte";
export let user: User;
export let member: PartialMember & {
unlisted?: boolean;
unlisted?: boolean
};
let pronouns: string | undefined;
@ -46,18 +46,13 @@
<div>
<a href="/@{user.name}/{member.name}">
<FallbackImage
urls={memberAvatars(member)}
width={200}
lazyLoad
alt="Avatar for {member.name}"
/>
<FallbackImage urls={memberAvatars(member)} width={200} alt="Avatar for {member.name}" />
</a>
<p class="m-2">
<a class="text-reset fs-5 text-break" href="/@{user.name}/{member.name}">
{member.display_name ?? member.name}
{#if member.unlisted === true}
<span bind:this={iconElement} tabindex={0}><Icon name="lock" /></span>
<span bind:this={iconElement} tabindex={0}><Icon name="lock"/></span>
<Tooltip target={iconElement} placement="top">This member is hidden</Tooltip>
{/if}
</a>

View file

@ -31,8 +31,6 @@
const resp = await apiFetchClient<Settings>(
"/users/@me/settings",
"PATCH",
// If this function is run, notice will always be non-null
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
{ read_global_notice: data.notice!.id },
2,
);

View file

@ -11,7 +11,7 @@ export const load = async ({ params }) => {
return resp;
} catch (e) {
if ((e as APIError).code === ErrorCode.UserNotFound) {
error(404, e as App.Error);
error(404, e as APIError);
}
throw e;

View file

@ -4,6 +4,7 @@
import {
Alert,
Badge,
Button,
ButtonGroup,
Icon,
@ -14,7 +15,7 @@
ModalFooter,
Tooltip,
} from "@sveltestrap/sveltestrap";
import { DateTime, FixedOffsetZone } from "luxon";
import { DateTime, Duration, FixedOffsetZone, Zone } from "luxon";
import FieldCard from "$lib/components/FieldCard.svelte";
import PronounLink from "$lib/components/PronounLink.svelte";
import PartialMemberCard from "$lib/components/PartialMemberCard.svelte";
@ -45,7 +46,6 @@
import ProfileFlag from "./ProfileFlag.svelte";
import IconButton from "$lib/components/IconButton.svelte";
import Badges from "./badges/Badges.svelte";
import PreferencesCheatsheet from "./PreferencesCheatsheet.svelte";
export let data: PageData;
@ -190,15 +190,14 @@
{/if}
{#if data.utc_offset}
<Tooltip target="user-clock" placement="top">Current time</Tooltip>
<Icon id="user-clock" name="clock" aria-label="This user's current time" />
{currentTime} <span class="text-body-secondary">(UTC{timezone})</span>
<Icon id="user-clock" name="clock" aria-label="This user's current time" /> {currentTime} <span class="text-body-secondary">(UTC{timezone})</span>
{/if}
{#if profileEmpty && $userStore?.id === data.id}
<hr />
<p>
<em>
Your profile is empty! You can customize it by going to the <a
href="/@{data.name}/edit">edit profile</a
Your profile is empty! You can customize it by going to the <a href="/@{data.name}/edit"
>edit profile</a
> page.</em
> <span class="text-muted">(only you can see this)</span>
</p>
@ -259,12 +258,6 @@
</div>
{/each}
</div>
<PreferencesCheatsheet
preferences={data.custom_preferences}
names={data.names}
pronouns={data.pronouns}
fields={data.fields}
/>
<div class="row">
<div class="col-md-6">
<InputGroup>

View file

@ -1,65 +0,0 @@
<script lang="ts">
import type {
CustomPreferences,
CustomPreference,
Field,
FieldEntry,
Pronoun,
} from "$lib/api/entities";
import defaultPreferences from "$lib/api/default_preferences";
import StatusIcon from "$lib/components/StatusIcon.svelte";
export let preferences: CustomPreferences;
export let names: FieldEntry[];
export let pronouns: Pronoun[];
export let fields: Field[];
let mergedPreferences: CustomPreferences;
$: mergedPreferences = Object.assign({}, defaultPreferences, preferences);
// Filter default preferences to the ones the user/member has used
// This is done separately from custom preferences to make the shown list cleaner
let usedDefaultPreferences: Array<{ id: string; preference: CustomPreference }>;
$: usedDefaultPreferences = Object.keys(defaultPreferences)
.filter(
(pref) =>
names.some((entry) => entry.status === pref) ||
pronouns.some((entry) => entry.status === pref) ||
fields.some((field) => field.entries.some((entry) => entry.status === pref)),
)
.map((key) => ({
id: key,
preference: defaultPreferences[key],
}));
// Do the same for custom preferences
let usedCustomPreferences: Array<{ id: string; preference: CustomPreference }>;
$: usedCustomPreferences = Object.keys(preferences)
.filter(
(pref) =>
names.some((entry) => entry.status === pref) ||
pronouns.some((entry) => entry.status === pref) ||
fields.some((field) => field.entries.some((entry) => entry.status === pref)),
)
.map((pref) => ({ id: pref, preference: mergedPreferences[pref] }));
</script>
<div class="text-center">
<ul class="list-inline text-body-secondary">
{#each usedDefaultPreferences as pref (pref.id)}
<li class="list-inline-item mx-2">
<StatusIcon {preferences} status={pref.id} />
{pref.preference.tooltip}
</li>
{/each}
</ul>
{#if usedCustomPreferences}
<ul class="list-inline text-body-secondary">
{#each usedCustomPreferences as pref (pref.id)}
<li class="list-inline-item mx-2">
<StatusIcon {preferences} status={pref.id} />
{pref.preference.tooltip}
</li>
{/each}
</ul>
{/if}
</div>

View file

@ -14,9 +14,9 @@ export const load = async ({ params }) => {
(e as APIError).code === ErrorCode.UserNotFound ||
(e as APIError).code === ErrorCode.MemberNotFound
) {
error(404, e as App.Error);
error(404, e as APIError);
}
error(500, e as App.Error);
error(500, e as APIError);
}
};

View file

@ -22,7 +22,6 @@
import { addToast } from "$lib/toast";
import ProfileFlag from "../ProfileFlag.svelte";
import IconButton from "$lib/components/IconButton.svelte";
import PreferencesCheatsheet from "../PreferencesCheatsheet.svelte";
export let data: PageData;
@ -155,12 +154,6 @@
</div>
{/each}
</div>
<PreferencesCheatsheet
preferences={data.user.custom_preferences}
names={data.names}
pronouns={data.pronouns}
fields={data.fields}
/>
<div class="row">
<div class="col-md-6">
<InputGroup>

View file

@ -5,15 +5,7 @@
import type { LayoutData } from "./$types";
import { addToast, delToast } from "$lib/toast";
import { apiFetchClient, fastFetchClient } from "$lib/api/fetch";
import {
Button,
ButtonGroup,
Modal,
ModalBody,
ModalFooter,
Nav,
NavItem,
} from "@sveltestrap/sveltestrap";
import { Button, ButtonGroup, Modal, ModalBody, ModalFooter, Nav, NavItem } from "@sveltestrap/sveltestrap";
import { goto } from "$app/navigation";
import ErrorAlert from "$lib/components/ErrorAlert.svelte";
import IconButton from "$lib/components/IconButton.svelte";

View file

@ -41,9 +41,8 @@ export const load = (async ({ params }) => {
pronouns: pronouns.autocomplete,
flags,
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
if ("code" in e) error(500, e as App.Error);
} catch (e) {
if ("code" in e) error(500, e as APIError);
throw e;
}
}) satisfies LayoutLoad;

View file

@ -45,7 +45,9 @@
</div>
</div>
<div>
<Button on:click={() => ($member.fields = [...$member.fields, { name: null, entries: [] }])}>
<Button
on:click={() => ($member.fields = [...$member.fields, { name: null, entries: [] }])}
>
<Icon name="plus" aria-hidden /> Add new field
</Button>
</div>

View file

@ -1,6 +1,6 @@
import type { PrideFlag, MeUser, PronounsJson } from "$lib/api/entities";
import type { PrideFlag, APIError, MeUser, PronounsJson } from "$lib/api/entities";
import { apiFetchClient } from "$lib/api/fetch";
import { error, redirect } from "@sveltejs/kit";
import { error, redirect, type Redirect } from "@sveltejs/kit";
import pronounsRaw from "$lib/pronouns.json";
const pronouns = pronounsRaw as PronounsJson;
@ -21,9 +21,8 @@ export const load = async ({ params }) => {
pronouns: pronouns.autocomplete,
flags,
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
if ("code" in e) error(500, e as App.Error);
} catch (e) {
if ("code" in e) error(500, e as APIError);
throw e;
}
};

View file

@ -4,14 +4,7 @@
import { PreferenceSize, type APIError, type MeUser } from "$lib/api/entities";
import IconButton from "$lib/components/IconButton.svelte";
import {
Button,
ButtonGroup,
FormGroup,
Icon,
Input,
InputGroup,
} from "@sveltestrap/sveltestrap";
import { Button, ButtonGroup, FormGroup, Icon, Input, InputGroup } from "@sveltestrap/sveltestrap";
import { PUBLIC_SHORT_BASE } from "$env/static/public";
import CustomPreference from "./CustomPreference.svelte";
import { DateTime, FixedOffsetZone } from "luxon";

View file

@ -11,7 +11,7 @@ export const load = async ({ params }) => {
redirect(303, `/@${resp.name}`);
} catch (e) {
if ((e as APIError).code === ErrorCode.UserNotFound) {
error(404, e as App.Error);
error(404, e as APIError);
}
throw e;

View file

@ -64,10 +64,8 @@
) => Promise<void>;
let captchaToken = "";
// svelte-hcaptcha doesn't have types, so we can't use anything except `any` here.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let captcha: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const captchaSuccess = (token: any) => {
captchaToken = token.detail.token;
};
@ -90,8 +88,6 @@
await fastFetch("/auth/force-delete", {
method: "GET",
headers: {
// We know for sure this value is non-null if this function is run at all
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
"X-Delete-Token": token!,
},
});
@ -109,8 +105,6 @@
await fastFetch("/auth/cancel-delete", {
method: "GET",
headers: {
// We know for sure this value is non-null if this function is run at all
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
"X-Delete-Token": token!,
},
});

View file

@ -10,7 +10,7 @@
export let data: PageData;
let callbackPage: CallbackPage;
let callbackPage: any;
const signupForm = async (username: string, invite: string, captchaToken: string) => {
try {

View file

@ -10,7 +10,7 @@
export let data: PageData;
let callbackPage: CallbackPage;
let callbackPage: any;
const signupForm = async (username: string, invite: string, captchaToken: string) => {
try {

View file

@ -10,7 +10,7 @@
export let data: PageData;
let callbackPage: CallbackPage;
let callbackPage: any;
const signupForm = async (username: string, invite: string, captchaToken: string) => {
try {

View file

@ -10,7 +10,7 @@
export let data: PageData;
let callbackPage: CallbackPage;
let callbackPage: any;
const signupForm = async (username: string, invite: string, captchaToken: string) => {
try {

View file

@ -10,7 +10,7 @@
export let data: PageData;
let callbackPage: CallbackPage;
let callbackPage: any;
const signupForm = async (username: string, invite: string, captchaToken: string) => {
try {

View file

@ -23,7 +23,7 @@ export const load = async ({ params }) => {
(e as APIError).code === ErrorCode.InvalidToken ||
(e as APIError).code === ErrorCode.NotOwnMember
) {
error(403, e as App.Error);
error(403, e as APIError);
}
throw e;

View file

@ -14,7 +14,7 @@ export const load = async () => {
(e as APIError).code === ErrorCode.Forbidden ||
(e as APIError).code === ErrorCode.InvalidToken
) {
error(403, e as App.Error);
error(403, e as APIError);
}
throw e;

View file

@ -1,7 +1,4 @@
<script lang="ts">
// Ignoring the TS error here, because this file imports fine, typescript just chokes on markdown files
// eslint-disable-next-line
//@ts-ignore
import { html } from "./about.md";
</script>

View file

@ -1,8 +1,5 @@
<script lang="ts">
import { onMount } from "svelte";
// Ignoring the TS error here, because this file imports fine, typescript just chokes on markdown files
// eslint-disable-next-line
//@ts-ignore
import { html } from "./changelog.md";
import { CURRENT_CHANGELOG } from "$lib/store";

View file

@ -1,7 +1,4 @@
<script lang="ts">
// Ignoring the TS error here, because this file imports fine, typescript just chokes on markdown files
// eslint-disable-next-line
//@ts-ignore
import { html } from "./privacy.md";
</script>

View file

@ -1,7 +1,4 @@
<script lang="ts">
// Ignoring the TS error here, because this file imports fine, typescript just chokes on markdown files
// eslint-disable-next-line
//@ts-ignore
import { html } from "./terms.md";
</script>

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { PUBLIC_BASE_URL } from "$env/static/public";
import type { PageData } from "./$types";
import type { PageData } from "../../$types";
export let data: PageData;

View file

@ -3,14 +3,7 @@
import { fastFetchClient } from "$lib/api/fetch";
import ErrorAlert from "$lib/components/ErrorAlert.svelte";
import { addToast } from "$lib/toast";
import {
Button,
ButtonGroup,
FormGroup,
Modal,
ModalBody,
ModalFooter,
} from "@sveltestrap/sveltestrap";
import { Button, ButtonGroup, FormGroup, Modal, ModalBody, ModalFooter } from "@sveltestrap/sveltestrap";
import type { PageData } from "./$types";
import ReportCard from "./ReportCard.svelte";

View file

@ -25,6 +25,7 @@
Table,
} from "@sveltestrap/sveltestrap";
import type { PageData } from "./$types";
import { onMount } from "svelte";
import { DateTime } from "luxon";
export let data: PageData;

View file

@ -10,6 +10,6 @@ export const load = async () => {
} catch (e) {
if ((e as APIError).code === ErrorCode.NotFound) return { exportData: null };
error(500, e as App.Error);
error(500, e as APIError);
}
};

View file

@ -1,14 +1,6 @@
<script lang="ts">
import { MAX_FLAGS, type APIError, type PrideFlag } from "$lib/api/entities";
import {
Button,
Icon,
Input,
Modal,
ModalBody,
ModalFooter,
ModalHeader,
} from "@sveltestrap/sveltestrap";
import { Button, Icon, Input, Modal, ModalBody, ModalFooter, ModalHeader } from "@sveltestrap/sveltestrap";
import type { PageData } from "./$types";
import Flag from "./Flag.svelte";
import prettyBytes from "pretty-bytes";

View file

@ -2,14 +2,7 @@
import { flagURL, type APIError, type PrideFlag } from "$lib/api/entities";
import { apiFetchClient } from "$lib/api/fetch";
import { addToast } from "$lib/toast";
import {
Button,
Input,
Modal,
ModalBody,
ModalFooter,
ModalHeader,
} from "@sveltestrap/sveltestrap";
import { Button, Input, Modal, ModalBody, ModalFooter, ModalHeader } from "@sveltestrap/sveltestrap";
export let flag: PrideFlag;
export let deleteFlag: (id: string) => Promise<void>;

View file

@ -1,18 +0,0 @@
declare module "svelte-hcaptcha" {
import type { SvelteComponent } from "svelte";
export interface HCaptchaProps {
sitekey?: string;
apihost?: string;
hl?: string;
reCaptchaCompat?: boolean;
theme?: CaptchaTheme;
size?: string;
}
declare class HCaptcha extends SvelteComponent {
$$prop_def: HCaptchaProps;
}
export default HCaptcha;
}

View file

@ -11,7 +11,7 @@ const config = {
kit: {
adapter: adapter(),
version: {
name: child_process.execSync("git describe --tags --long --always").toString().trim(),
name: child_process.execSync("git describe --tags --long").toString().trim(),
},
},
};

55
go.mod
View file

@ -10,30 +10,30 @@ require (
github.com/davidbyttow/govips/v2 v2.13.0
github.com/georgysavva/scany/v2 v2.0.0
github.com/getsentry/sentry-go v0.25.0
github.com/go-chi/chi/v5 v5.0.10
github.com/go-chi/chi/v5 v5.0.11
github.com/go-chi/cors v1.2.1
github.com/go-chi/httprate v0.7.4
github.com/go-chi/httprate v0.8.0
github.com/go-chi/render v1.0.3
github.com/gobwas/glob v0.2.3
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/google/uuid v1.4.0
github.com/jackc/pgx/v5 v5.4.3
github.com/google/uuid v1.5.0
github.com/jackc/pgx/v5 v5.5.1
github.com/joho/godotenv v1.5.1
github.com/mediocregopher/radix/v4 v4.1.4
github.com/minio/minio-go/v7 v7.0.63
github.com/minio/minio-go/v7 v7.0.66
github.com/prometheus/client_golang v1.17.0
github.com/rs/xid v1.5.0
github.com/rubenv/sql-migrate v1.5.2
github.com/rubenv/sql-migrate v1.6.0
github.com/toshi0607/chi-prometheus v0.1.4
github.com/urfave/cli/v2 v2.25.7
github.com/urfave/cli/v2 v2.27.0
go.uber.org/zap v1.26.0
golang.org/x/oauth2 v0.13.0
google.golang.org/api v0.148.0
golang.org/x/oauth2 v0.15.0
google.golang.org/api v0.154.0
gopkg.in/yaml.v3 v3.0.1
)
require (
cloud.google.com/go/compute v1.23.2 // indirect
cloud.google.com/go/compute v1.23.3 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf // indirect
github.com/ajg/form v1.5.1 // indirect
@ -41,18 +41,21 @@ require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
github.com/go-logr/logr v1.3.0 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/gorilla/websocket v1.5.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.2 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
@ -67,18 +70,22 @@ require (
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/tilinna/clock v1.1.0 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect
go.opentelemetry.io/otel v1.21.0 // indirect
go.opentelemetry.io/otel/metric v1.21.0 // indirect
go.opentelemetry.io/otel/trace v1.21.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/image v0.13.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sync v0.4.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/image v0.14.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sync v0.5.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231012201019-e917dd12ba7a // indirect
google.golang.org/grpc v1.59.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 // indirect
google.golang.org/grpc v1.60.1 // indirect
google.golang.org/protobuf v1.32.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
)

123
go.sum
View file

@ -1,6 +1,6 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go/compute v1.23.2 h1:nWEMDhgbBkBJjfpVySqU4jgWdc22PLR0o4vEexZHers=
cloud.google.com/go/compute v1.23.2/go.mod h1:JJ0atRC0J/oWYiiVBmsSsrRnh92DhZPG4hFDcR04Rns=
cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk=
cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
emperror.dev/errors v0.8.1 h1:UavXZ5cSX/4u9iyvH6aDcuGkVjeexUGJ7Ij7G4VfQT0=
@ -37,25 +37,29 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/georgysavva/scany/v2 v2.0.0 h1:RGXqxDv4row7/FYoK8MRXAZXqoWF/NM+NP0q50k3DKU=
github.com/georgysavva/scany/v2 v2.0.0/go.mod h1:sigOdh+0qb/+aOs3TVhehVT10p8qJL7K/Zhyz8vWo38=
github.com/getsentry/sentry-go v0.25.0 h1:q6Eo+hS+yoJlTO3uu/azhQadsD8V+jQn2D8VvX1eOyI=
github.com/getsentry/sentry-go v0.25.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA=
github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-chi/httprate v0.7.4 h1:a2GIjv8he9LRf3712zxxnRdckQCm7I8y8yQhkJ84V6M=
github.com/go-chi/httprate v0.7.4/go.mod h1:6GOYBSwnpra4CQfAKXu8sQZg+nZ0M1g9QnyFvxrAB8A=
github.com/go-chi/httprate v0.8.0 h1:CyKng28yhGnlGXH9EDGC/Qizj29afJQSNW15W/yj34o=
github.com/go-chi/httprate v0.8.0/go.mod h1:6GOYBSwnpra4CQfAKXu8sQZg+nZ0M1g9QnyFvxrAB8A=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs=
github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/gobuffalo/logger v1.0.6 h1:nnZNpxYo0zx+Aj9RfMPBm+x9zAU2OayFh/xrAWi34HU=
github.com/gobuffalo/packd v1.0.1 h1:U2wXfRr4E9DH8IdsDLlRFwTZTK7hLfq9qT/QHXGVe/0=
github.com/gobuffalo/packr/v2 v2.8.3 h1:xE1yzvnO56cUC0sTpKR3DIbxZgB54AftTFMhB2XEWlY=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
@ -91,32 +95,31 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY=
github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.5.1 h1:5I9etrGkLrN+2XPCsi6XLlV5DITbSL/xBZdmAxFcXPI=
github.com/jackc/pgx/v5 v5.5.1/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw=
github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4=
github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
@ -126,9 +129,6 @@ github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtB
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
github.com/markbates/errx v1.1.0 h1:QDFeR+UP95dO12JgW+tgi2UVfo0V8YBHiUIOaeBPiEI=
github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY=
github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI=
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
@ -136,8 +136,8 @@ github.com/mediocregopher/radix/v4 v4.1.4 h1:Uze6DEbEAvL+VHXUEu/EDBTkUk5CLct5h3n
github.com/mediocregopher/radix/v4 v4.1.4/go.mod h1:ajchozX/6ELmydxWeWM6xCFHVpZ4+67LXHOTOVR0nCE=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.63 h1:GbZ2oCvaUdgT5640WJOpyDhhDxvknAJU2/T3yurwcbQ=
github.com/minio/minio-go/v7 v7.0.63/go.mod h1:Q6X7Qjb7WMhvG65qKf4gUgA5XaiSox74kR1uAEjxRS4=
github.com/minio/minio-go/v7 v7.0.66 h1:bnTOXOHjOqv/gcMuiVbN9o2ngRItvqE774dG9nq0Dzw=
github.com/minio/minio-go/v7 v7.0.66/go.mod h1:DHAgmyQEGdW3Cif0UooKOyrT3Vxs82zNdV6tkKhRtbs=
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -164,8 +164,8 @@ github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3c
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rubenv/sql-migrate v1.5.2 h1:bMDqOnrJVV/6JQgQ/MxOpU+AdO8uzYYA/TxFUBzFtS0=
github.com/rubenv/sql-migrate v1.5.2/go.mod h1:H38GW8Vqf8F0Su5XignRyaRcbXbJunSWxs+kmzlg0Is=
github.com/rubenv/sql-migrate v1.6.0 h1:IZpcTlAx/VKXphWEpwWJ7BaMq05tYtE80zYz+8a5Il8=
github.com/rubenv/sql-migrate v1.6.0/go.mod h1:m3ilnKP7sNb4eYkLsp6cGdPOl4OBcXM6rcbzU+Oqc5k=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
@ -181,19 +181,28 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/tilinna/clock v1.0.2/go.mod h1:ZsP7BcY7sEEz7ktc0IVy8Us6boDrK8VradlKRUGfOao=
github.com/tilinna/clock v1.1.0 h1:6IQQQCo6KoBxVudv6gwtY8o4eDfhHo8ojA5dP0MfhSs=
github.com/tilinna/clock v1.1.0/go.mod h1:ZsP7BcY7sEEz7ktc0IVy8Us6boDrK8VradlKRUGfOao=
github.com/toshi0607/chi-prometheus v0.1.4 h1:5KpqJrmdvMvbfU0JiL9ghOTbe8S9sgHDCCQvXgnyoJo=
github.com/toshi0607/chi-prometheus v0.1.4/go.mod h1:E++tBjqpDsvGWjLYdcFd5rvqJ7HG8wwBux+M6gyIL/Q=
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/urfave/cli/v2 v2.27.0 h1:uNs1K8JwTFL84X68j5Fjny6hfANh9nTlJ6dRtZAFAHY=
github.com/urfave/cli/v2 v2.27.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI=
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 h1:SpGay3w+nEwMpfVnbqOLH5gY52/foP8RE8UzTZ1pdSE=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo=
go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc=
go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
@ -205,12 +214,12 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4=
golang.org/x/image v0.13.0 h1:3cge/F/QTkNLauhf2QoE9zp+7sr+ZcL4HnoZmdwg9sg=
golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk=
golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
@ -225,17 +234,17 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY=
golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0=
golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ=
golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -246,19 +255,19 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@ -268,8 +277,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.148.0 h1:HBq4TZlN4/1pNcu0geJZ/Q50vIwIXT532UIMYoo0vOs=
google.golang.org/api v0.148.0/go.mod h1:8/TBgwaKjfqTdacOJrOv2+2Q6fBDU1uHKK06oGSkxzU=
google.golang.org/api v0.154.0 h1:X7QkVKZBskztmpPKWQXgjJRPA2dJYrL6r+sYPRLj050=
google.golang.org/api v0.154.0/go.mod h1:qhSMkM85hgqiokIYsrRyKxrjfBeIhgl4Z2JmeRkYylc=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
@ -277,15 +286,15 @@ google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJ
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231012201019-e917dd12ba7a h1:a2MQQVoTo96JC9PMGtGBymLp7+/RzpFc2yX/9WfFg1c=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231012201019-e917dd12ba7a/go.mod h1:4cYg8o5yUbm77w8ZX00LhMVNl/YVBFJRYWDc0uYWMs0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 h1:/jFB8jK5R3Sq3i/lmeZO0cATSzFfZaJq1J2Euan3XKU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0/go.mod h1:FUoWkonphQm3RhTS+kOEhF8h0iDpm4tdXolVCeZ9KKA=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU=
google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@ -297,8 +306,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=

View file

@ -2,15 +2,12 @@ package cleandb
import (
"context"
"errors"
"fmt"
"os"
"time"
dbpkg "codeberg.org/pronounscc/pronouns.cc/backend/db"
"codeberg.org/pronounscc/pronouns.cc/backend/log"
"github.com/georgysavva/scany/v2/pgxscan"
"github.com/jackc/pgx/v5"
"github.com/joho/godotenv"
"github.com/rs/xid"
"github.com/urfave/cli/v2"
@ -80,12 +77,7 @@ func run(c *cli.Context) error {
fmt.Printf("error starting transaction: %v\n", err)
return err
}
defer func() {
err := tx.Rollback(ctx)
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
log.Error("rolling back transaction:", err)
}
}()
defer tx.Rollback(ctx)
inactiveUsers, err := db.InactiveUsers(ctx, tx)
if err != nil {

View file

@ -0,0 +1,30 @@
-- 2023-12-27: Add moderation log
-- +migrate Up
create table audit_log (
id bigint primary key,
-- the target user of this action
-- this is *not* a foreign key. if the user is deleted the audit log entry should stay, just showing as "deleted user <id>"
target_user_id bigint not null,
-- the target member of this action
target_member_id bigint,
-- the moderator that took this action
-- as with target_user_id, the audit log entry should not be deleted if the moderator's account is.
moderator_id bigint not null,
-- the report this was in response to. may be null if this action was taken by a moderator not responding to a report.
report_id integer references reports (id) on delete set null,
-- the reason for this action. always set, but may be 'None' if the action taken was 'ignore'.
reason text not null default 'None',
action_taken text not null, -- enum, currently: 'ignore', 'warn', 'suspend'
cleared_data jsonb null -- backup of the cleared data. may be null if no data was cleared
);
create index audit_log_target_user_idx on audit_log (target_user_id);
create index audit_log_target_member_idx on audit_log (target_member_id);
create index audit_log_moderator_idx on audit_log (moderator_id);
-- +migrate Down
drop table audit_log;

View file

@ -1,12 +1,10 @@
package seeddb
import (
"errors"
"log"
"os"
"codeberg.org/pronounscc/pronouns.cc/backend/db"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/joho/godotenv"
"github.com/urfave/cli/v2"
@ -84,12 +82,7 @@ func run(c *cli.Context) error {
log.Println("error beginning transaction:", err)
return err
}
defer func() {
err := tx.Rollback(ctx)
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
log.Println("error rolling back transaction:", err)
}
}()
defer tx.Rollback(ctx)
for i, su := range seed.Users {
u, err := pg.CreateUser(ctx, tx, su.Username)

View file

@ -1,7 +1,6 @@
package snowflakes
import (
"errors"
"os"
"time"
@ -40,12 +39,7 @@ func run(c *cli.Context) error {
log.Error("creating transaction:", err)
return err
}
defer func() {
err := tx.Rollback(c.Context)
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
log.Error("rolling back transaction:", err)
}
}()
defer tx.Rollback(c.Context)
var userIDs []xid.ID
err = pgxscan.Select(c.Context, conn, &userIDs, "SELECT id FROM users WHERE snowflake_id IS NULL")