forked from mirrors/pronouns.cc
Compare commits
20 commits
feature/be
...
main
Author | SHA1 | Date | |
---|---|---|---|
8f34367d1a | |||
|
5fcd87a94a | ||
|
0633a32f64 | ||
|
623cdb545e | ||
|
4745a1c04b | ||
|
4e78d36eff | ||
|
31e1862ca9 | ||
|
4308bd4d98 | ||
|
40672d6d41 | ||
|
cfed74d6bf | ||
|
b29a0c86db | ||
|
1339550c80 | ||
|
55479ae8da | ||
|
ebc10d9558 | ||
|
ac603ac18e | ||
|
00abe1cb32 | ||
|
c13c4e90b6 | ||
|
e37b5be376 | ||
|
44b667ff43 | ||
|
e0ba5ea0dc |
77 changed files with 484 additions and 1046 deletions
13
.woodpecker/.backend.yml
Normal file
13
.woodpecker/.backend.yml
Normal file
|
@ -0,0 +1,13 @@
|
|||
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
|
20
.woodpecker/.frontend.yml
Normal file
20
.woodpecker/.frontend.yml
Normal file
|
@ -0,0 +1,20 @@
|
|||
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
|
2
Makefile
2
Makefile
|
@ -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`" .
|
||||
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`" .
|
||||
|
||||
.PHONY: generate
|
||||
generate:
|
||||
|
|
|
@ -37,15 +37,3 @@ 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) }
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -79,7 +79,7 @@ func (db *DB) CreateExport(ctx context.Context, userID xid.ID, filename string,
|
|||
return de, errors.Wrap(err, "building query")
|
||||
}
|
||||
|
||||
pgxscan.Get(ctx, db, &de, sql, args...)
|
||||
err = pgxscan.Get(ctx, db, &de, sql, args...)
|
||||
if err != nil {
|
||||
return de, errors.Wrap(err, "executing sql")
|
||||
}
|
||||
|
|
|
@ -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 == "pixelfed" || f.InstanceType == "gotosocial"
|
||||
return f.InstanceType == "mastodon" || f.InstanceType == "pleroma" || f.InstanceType == "akkoma" || f.InstanceType == "incestoma" || f.InstanceType == "pixelfed" || f.InstanceType == "gotosocial"
|
||||
}
|
||||
|
||||
func (f FediverseApp) Misskey() bool {
|
||||
return f.InstanceType == "misskey" || f.InstanceType == "foundkey" || f.InstanceType == "calckey" || f.InstanceType == "firefish"
|
||||
return f.InstanceType == "misskey" || f.InstanceType == "foundkey" || f.InstanceType == "calckey" || f.InstanceType == "firefish" || f.InstanceType == "sharkey"
|
||||
}
|
||||
|
||||
const ErrNoInstanceApp = errors.Sentinel("instance doesn't have an app")
|
||||
|
|
|
@ -6,6 +6,7 @@ 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"
|
||||
|
@ -43,7 +44,12 @@ 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 tx.Rollback(ctx)
|
||||
defer func() {
|
||||
err := tx.Rollback(ctx)
|
||||
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||
log.Error("rolling back transaction:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
var maxInvites, inviteCount int
|
||||
err = tx.QueryRow(ctx, "SELECT max_invites FROM users WHERE id = $1", userID).Scan(&maxInvites)
|
||||
|
|
|
@ -7,6 +7,7 @@ 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"
|
||||
|
@ -287,7 +288,12 @@ func (db *DB) RerollMemberSID(ctx context.Context, userID, memberID xid.ID) (new
|
|||
if err != nil {
|
||||
return "", errors.Wrap(err, "beginning transaction")
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer func() {
|
||||
err := tx.Rollback(ctx)
|
||||
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||
log.Error("rolling back transaction:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
sql, args, err := sq.Update("members").
|
||||
Set("sid", squirrel.Expr("find_free_member_sid()")).
|
||||
|
|
|
@ -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,
|
||||
}).Suffix("RETURNING *").ToSql()
|
||||
}).ToSql()
|
||||
if err != nil {
|
||||
return w, errors.Wrap(err, "building sql")
|
||||
}
|
||||
|
|
|
@ -26,7 +26,8 @@ var Command = &cli.Command{
|
|||
func run(c *cli.Context) error {
|
||||
// initialize sentry
|
||||
if dsn := os.Getenv("SENTRY_DSN"); dsn != "" {
|
||||
sentry.Init(sentry.ClientOptions{
|
||||
// We don't need to check the error here--it's fine if no DSN is set.
|
||||
_ = sentry.Init(sentry.ClientOptions{
|
||||
Dsn: dsn,
|
||||
Debug: os.Getenv("DEBUG") == "true",
|
||||
Release: server.Tag,
|
||||
|
|
|
@ -6,7 +6,6 @@ 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"
|
||||
|
@ -27,6 +26,5 @@ func mountRoutes(s *server.Server) {
|
|||
|
||||
s.Router.Route("/v2", func(r chi.Router) {
|
||||
user2.Mount(s, r)
|
||||
admin2.Mount(s, r)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ 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"
|
||||
|
@ -291,7 +292,12 @@ func (s *Server) discordSignup(w http.ResponseWriter, r *http.Request) error {
|
|||
if err != nil {
|
||||
return errors.Wrap(err, "beginning transaction")
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer func() {
|
||||
err := tx.Rollback(ctx)
|
||||
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||
log.Error("rolling back transaction:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
du := new(discordgo.User)
|
||||
err = s.DB.GetJSON(ctx, "discord:"+req.Ticket, &du)
|
||||
|
|
|
@ -11,6 +11,7 @@ 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"
|
||||
)
|
||||
|
@ -319,7 +320,12 @@ func (s *Server) mastodonSignup(w http.ResponseWriter, r *http.Request) error {
|
|||
if err != nil {
|
||||
return errors.Wrap(err, "beginning transaction")
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer func() {
|
||||
err := tx.Rollback(ctx)
|
||||
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||
log.Error("rolling back transaction:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
mu := new(partialMastodonAccount)
|
||||
err = s.DB.GetJSON(ctx, "mastodon:"+req.Ticket, &mu)
|
||||
|
|
|
@ -12,6 +12,7 @@ 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"
|
||||
)
|
||||
|
@ -247,7 +248,12 @@ func (s *Server) misskeySignup(w http.ResponseWriter, r *http.Request) error {
|
|||
if err != nil {
|
||||
return errors.Wrap(err, "beginning transaction")
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer func() {
|
||||
err := tx.Rollback(ctx)
|
||||
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||
log.Error("rolling back transaction:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
mu := new(partialMisskeyAccount)
|
||||
err = s.DB.GetJSON(ctx, "misskey:"+req.Ticket, &mu)
|
||||
|
|
|
@ -68,13 +68,10 @@ func (s *Server) noAppFediverseURL(ctx context.Context, w http.ResponseWriter, r
|
|||
case "iceshrimp":
|
||||
softwareName = "firefish"
|
||||
fallthrough
|
||||
case "misskey", "foundkey", "calckey", "firefish":
|
||||
case "misskey", "foundkey", "calckey", "firefish", "sharkey":
|
||||
return s.noAppMisskeyURL(ctx, w, r, softwareName, instance)
|
||||
case "mastodon", "pleroma", "akkoma", "pixelfed", "gotosocial":
|
||||
case "mastodon", "pleroma", "akkoma", "incestoma", "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}
|
||||
|
|
|
@ -10,6 +10,7 @@ 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"
|
||||
|
@ -294,7 +295,12 @@ func (s *Server) googleSignup(w http.ResponseWriter, r *http.Request) error {
|
|||
if err != nil {
|
||||
return errors.Wrap(err, "beginning transaction")
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer func() {
|
||||
err := tx.Rollback(ctx)
|
||||
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||
log.Error("rolling back transaction:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
gu := new(partialGoogleUser)
|
||||
err = s.DB.GetJSON(ctx, "google:"+req.Ticket, &gu)
|
||||
|
|
|
@ -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)
|
||||
resp.Google = googleCfg.AuthCodeURL(state) + "&prompt=select_account"
|
||||
}
|
||||
|
||||
render.JSON(w, r, resp)
|
||||
|
|
|
@ -5,9 +5,11 @@ 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"
|
||||
)
|
||||
|
||||
|
@ -63,7 +65,12 @@ func (s *Server) deleteToken(w http.ResponseWriter, r *http.Request) error {
|
|||
if err != nil {
|
||||
return errors.Wrap(err, "beginning transaction")
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer func() {
|
||||
err := tx.Rollback(ctx)
|
||||
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||
log.Error("rolling back transaction:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
err = s.DB.InvalidateAllTokens(ctx, tx, claims.UserID)
|
||||
if err != nil {
|
||||
|
|
|
@ -12,6 +12,7 @@ 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"
|
||||
|
@ -327,7 +328,12 @@ func (s *Server) tumblrSignup(w http.ResponseWriter, r *http.Request) error {
|
|||
if err != nil {
|
||||
return errors.Wrap(err, "beginning transaction")
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer func() {
|
||||
err := tx.Rollback(ctx)
|
||||
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||
log.Error("rolling back transaction:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
tui := new(tumblrUserInfo)
|
||||
err = s.DB.GetJSON(ctx, "tumblr:"+req.Ticket, &tui)
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"codeberg.org/pronounscc/pronouns.cc/backend/server"
|
||||
"emperror.dev/errors"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
type CreateMemberRequest struct {
|
||||
|
@ -119,7 +120,12 @@ func (s *Server) createMember(w http.ResponseWriter, r *http.Request) (err error
|
|||
if err != nil {
|
||||
return errors.Wrap(err, "starting transaction")
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer func() {
|
||||
err := tx.Rollback(ctx)
|
||||
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||
log.Error("rolling back transaction:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
m, err := s.DB.CreateMember(ctx, tx, claims.UserID, cmr.Name, cmr.DisplayName, cmr.Bio, cmr.Links)
|
||||
if err != nil {
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"emperror.dev/errors"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/rs/xid"
|
||||
)
|
||||
|
||||
|
@ -246,7 +247,12 @@ func (s *Server) patchMember(w http.ResponseWriter, r *http.Request) error {
|
|||
log.Errorf("creating transaction: %v", err)
|
||||
return errors.Wrap(err, "creating transaction")
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer func() {
|
||||
err := tx.Rollback(ctx)
|
||||
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||
log.Error("rolling back transaction:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
m, err = s.DB.UpdateMember(ctx, tx, m.ID, req.Name, req.DisplayName, req.Bio, req.Unlisted, req.Links, avatarHash)
|
||||
if err != nil {
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"emperror.dev/errors"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
type resolveReportRequest struct {
|
||||
|
@ -43,7 +44,12 @@ func (s *Server) resolveReport(w http.ResponseWriter, r *http.Request) error {
|
|||
log.Errorf("creating transaction: %v", err)
|
||||
return errors.Wrap(err, "creating transaction")
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer func() {
|
||||
err := tx.Rollback(ctx)
|
||||
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||
log.Error("rolling back transaction:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
report, err := s.DB.Report(ctx, tx, id)
|
||||
if err != nil {
|
||||
|
|
|
@ -3,9 +3,11 @@ 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 {
|
||||
|
@ -20,7 +22,12 @@ func (s *Server) deleteUser(w http.ResponseWriter, r *http.Request) error {
|
|||
if err != nil {
|
||||
return errors.Wrap(err, "creating transaction")
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer func() {
|
||||
err := tx.Rollback(ctx)
|
||||
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||
log.Error("rolling back transaction:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
err = s.DB.DeleteUser(ctx, tx, claims.UserID, true, "")
|
||||
if err != nil {
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"emperror.dev/errors"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/rs/xid"
|
||||
)
|
||||
|
||||
|
@ -80,7 +81,12 @@ func (s *Server) postUserFlag(w http.ResponseWriter, r *http.Request) error {
|
|||
if err != nil {
|
||||
return errors.Wrap(err, "starting transaction")
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer func() {
|
||||
err := tx.Rollback(ctx)
|
||||
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||
log.Error("rolling back transaction:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
flag, err := s.DB.CreateFlag(ctx, tx, claims.UserID, req.Name, req.Description)
|
||||
if err != nil {
|
||||
|
@ -192,7 +198,12 @@ func (s *Server) patchUserFlag(w http.ResponseWriter, r *http.Request) error {
|
|||
if err != nil {
|
||||
return errors.Wrap(err, "beginning transaction")
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer func() {
|
||||
err := tx.Rollback(ctx)
|
||||
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||
log.Error("rolling back transaction:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
flag, err = s.DB.EditFlag(ctx, tx, flag.ID, req.Name, req.Description, nil)
|
||||
if err != nil {
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"emperror.dev/errors"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/rs/xid"
|
||||
)
|
||||
|
||||
|
@ -221,7 +222,12 @@ func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error {
|
|||
log.Errorf("creating transaction: %v", err)
|
||||
return errors.Wrap(err, "creating transaction")
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer func() {
|
||||
err := tx.Rollback(ctx)
|
||||
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||
log.Error("rolling back transaction:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// update username
|
||||
if req.Username != nil && *req.Username != u.Username {
|
||||
|
|
|
@ -1,72 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
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)
|
||||
}
|
|
@ -1,375 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,121 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -28,10 +28,6 @@ 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
|
||||
|
@ -47,6 +43,7 @@ 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}
|
||||
|
|
|
@ -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,21 +154,3 @@ 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
|
||||
}
|
||||
|
|
|
@ -1,72 +0,0 @@
|
|||
# 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 |
|
|
@ -17,4 +17,15 @@ 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
25
frontend/src/app.d.ts
vendored
|
@ -1,38 +1,19 @@
|
|||
// See https://kit.svelte.dev/docs/types#app
|
||||
|
||||
import type { APIError, ErrorCode } from "$lib/api/entities";
|
||||
import type { ErrorCode } from "$lib/api/entities";
|
||||
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
type Error = {
|
||||
interface 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 {};
|
||||
|
|
|
@ -2,7 +2,9 @@ import { PRIVATE_SENTRY_DSN } from "$env/static/private";
|
|||
import * as Sentry from "@sentry/node";
|
||||
import type { HandleServerError } from "@sveltejs/kit";
|
||||
|
||||
Sentry.init({ dsn: PRIVATE_SENTRY_DSN });
|
||||
if (PRIVATE_SENTRY_DSN) {
|
||||
Sentry.init({ dsn: PRIVATE_SENTRY_DSN });
|
||||
}
|
||||
|
||||
export const handleError = (({ error, event }) => {
|
||||
console.log(error);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable no-unused-vars */
|
||||
import { PUBLIC_BASE_URL, PUBLIC_MEDIA_URL } from "$env/static/public";
|
||||
|
||||
export const MAX_MEMBERS = 500;
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
export let urls: string[];
|
||||
export let alt: string;
|
||||
export let width = 300;
|
||||
export let lazyLoad = false;
|
||||
|
||||
const contentTypeFor = (url: string) => {
|
||||
if (url.endsWith(".webp")) {
|
||||
|
@ -31,6 +32,7 @@
|
|||
src={urls[0] || defaultAvatars[0]}
|
||||
{alt}
|
||||
class="rounded-circle img-fluid"
|
||||
loading={lazyLoad ? "lazy" : "eager"}
|
||||
/>
|
||||
</picture>
|
||||
{:else}
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
export let user: User;
|
||||
export let member: PartialMember & {
|
||||
unlisted?: boolean
|
||||
unlisted?: boolean;
|
||||
};
|
||||
|
||||
let pronouns: string | undefined;
|
||||
|
@ -46,13 +46,18 @@
|
|||
|
||||
<div>
|
||||
<a href="/@{user.name}/{member.name}">
|
||||
<FallbackImage urls={memberAvatars(member)} width={200} alt="Avatar for {member.name}" />
|
||||
<FallbackImage
|
||||
urls={memberAvatars(member)}
|
||||
width={200}
|
||||
lazyLoad
|
||||
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>
|
||||
|
|
|
@ -31,6 +31,8 @@
|
|||
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,
|
||||
);
|
||||
|
|
|
@ -11,7 +11,7 @@ export const load = async ({ params }) => {
|
|||
return resp;
|
||||
} catch (e) {
|
||||
if ((e as APIError).code === ErrorCode.UserNotFound) {
|
||||
error(404, e as APIError);
|
||||
error(404, e as App.Error);
|
||||
}
|
||||
|
||||
throw e;
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Icon,
|
||||
|
@ -15,7 +14,7 @@
|
|||
ModalFooter,
|
||||
Tooltip,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import { DateTime, Duration, FixedOffsetZone, Zone } from "luxon";
|
||||
import { DateTime, FixedOffsetZone } from "luxon";
|
||||
import FieldCard from "$lib/components/FieldCard.svelte";
|
||||
import PronounLink from "$lib/components/PronounLink.svelte";
|
||||
import PartialMemberCard from "$lib/components/PartialMemberCard.svelte";
|
||||
|
@ -46,6 +45,7 @@
|
|||
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,14 +190,15 @@
|
|||
{/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>
|
||||
|
@ -258,6 +259,12 @@
|
|||
</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>
|
||||
|
|
65
frontend/src/routes/@[username]/PreferencesCheatsheet.svelte
Normal file
65
frontend/src/routes/@[username]/PreferencesCheatsheet.svelte
Normal file
|
@ -0,0 +1,65 @@
|
|||
<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>
|
|
@ -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 APIError);
|
||||
error(404, e as App.Error);
|
||||
}
|
||||
|
||||
error(500, e as APIError);
|
||||
error(500, e as App.Error);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
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;
|
||||
|
||||
|
@ -154,6 +155,12 @@
|
|||
</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>
|
||||
|
|
|
@ -5,7 +5,15 @@
|
|||
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";
|
||||
|
|
|
@ -41,8 +41,9 @@ export const load = (async ({ params }) => {
|
|||
pronouns: pronouns.autocomplete,
|
||||
flags,
|
||||
};
|
||||
} catch (e) {
|
||||
if ("code" in e) error(500, e as APIError);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (e: any) {
|
||||
if ("code" in e) error(500, e as App.Error);
|
||||
throw e;
|
||||
}
|
||||
}) satisfies LayoutLoad;
|
||||
|
|
|
@ -45,9 +45,7 @@
|
|||
</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>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { PrideFlag, APIError, MeUser, PronounsJson } from "$lib/api/entities";
|
||||
import type { PrideFlag, MeUser, PronounsJson } from "$lib/api/entities";
|
||||
import { apiFetchClient } from "$lib/api/fetch";
|
||||
import { error, redirect, type Redirect } from "@sveltejs/kit";
|
||||
import { error, redirect } from "@sveltejs/kit";
|
||||
|
||||
import pronounsRaw from "$lib/pronouns.json";
|
||||
const pronouns = pronounsRaw as PronounsJson;
|
||||
|
@ -21,8 +21,9 @@ export const load = async ({ params }) => {
|
|||
pronouns: pronouns.autocomplete,
|
||||
flags,
|
||||
};
|
||||
} catch (e) {
|
||||
if ("code" in e) error(500, e as APIError);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (e: any) {
|
||||
if ("code" in e) error(500, e as App.Error);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -4,7 +4,14 @@
|
|||
|
||||
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";
|
||||
|
|
|
@ -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 APIError);
|
||||
error(404, e as App.Error);
|
||||
}
|
||||
|
||||
throw e;
|
||||
|
|
|
@ -64,8 +64,10 @@
|
|||
) => 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;
|
||||
};
|
||||
|
@ -88,6 +90,8 @@
|
|||
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!,
|
||||
},
|
||||
});
|
||||
|
@ -105,6 +109,8 @@
|
|||
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!,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
export let data: PageData;
|
||||
|
||||
let callbackPage: any;
|
||||
let callbackPage: CallbackPage;
|
||||
|
||||
const signupForm = async (username: string, invite: string, captchaToken: string) => {
|
||||
try {
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
export let data: PageData;
|
||||
|
||||
let callbackPage: any;
|
||||
let callbackPage: CallbackPage;
|
||||
|
||||
const signupForm = async (username: string, invite: string, captchaToken: string) => {
|
||||
try {
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
export let data: PageData;
|
||||
|
||||
let callbackPage: any;
|
||||
let callbackPage: CallbackPage;
|
||||
|
||||
const signupForm = async (username: string, invite: string, captchaToken: string) => {
|
||||
try {
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
export let data: PageData;
|
||||
|
||||
let callbackPage: any;
|
||||
let callbackPage: CallbackPage;
|
||||
|
||||
const signupForm = async (username: string, invite: string, captchaToken: string) => {
|
||||
try {
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
export let data: PageData;
|
||||
|
||||
let callbackPage: any;
|
||||
let callbackPage: CallbackPage;
|
||||
|
||||
const signupForm = async (username: string, invite: string, captchaToken: string) => {
|
||||
try {
|
||||
|
|
|
@ -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 APIError);
|
||||
error(403, e as App.Error);
|
||||
}
|
||||
|
||||
throw e;
|
||||
|
|
|
@ -14,7 +14,7 @@ export const load = async () => {
|
|||
(e as APIError).code === ErrorCode.Forbidden ||
|
||||
(e as APIError).code === ErrorCode.InvalidToken
|
||||
) {
|
||||
error(403, e as APIError);
|
||||
error(403, e as App.Error);
|
||||
}
|
||||
|
||||
throw e;
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
<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>
|
||||
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<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";
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
<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>
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
<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>
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -3,7 +3,14 @@
|
|||
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";
|
||||
|
||||
|
|
|
@ -25,7 +25,6 @@
|
|||
Table,
|
||||
} from "@sveltestrap/sveltestrap";
|
||||
import type { PageData } from "./$types";
|
||||
import { onMount } from "svelte";
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
export let data: PageData;
|
||||
|
|
|
@ -10,6 +10,6 @@ export const load = async () => {
|
|||
} catch (e) {
|
||||
if ((e as APIError).code === ErrorCode.NotFound) return { exportData: null };
|
||||
|
||||
error(500, e as APIError);
|
||||
error(500, e as App.Error);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
<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";
|
||||
|
|
|
@ -2,7 +2,14 @@
|
|||
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>;
|
||||
|
|
18
frontend/src/svelte-hcaptcha.d.ts
vendored
Normal file
18
frontend/src/svelte-hcaptcha.d.ts
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
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;
|
||||
}
|
|
@ -11,7 +11,7 @@ const config = {
|
|||
kit: {
|
||||
adapter: adapter(),
|
||||
version: {
|
||||
name: child_process.execSync("git describe --tags --long").toString().trim(),
|
||||
name: child_process.execSync("git describe --tags --long --always").toString().trim(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
55
go.mod
55
go.mod
|
@ -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.11
|
||||
github.com/go-chi/chi/v5 v5.0.10
|
||||
github.com/go-chi/cors v1.2.1
|
||||
github.com/go-chi/httprate v0.8.0
|
||||
github.com/go-chi/httprate v0.7.4
|
||||
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.5.0
|
||||
github.com/jackc/pgx/v5 v5.5.1
|
||||
github.com/google/uuid v1.4.0
|
||||
github.com/jackc/pgx/v5 v5.4.3
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mediocregopher/radix/v4 v4.1.4
|
||||
github.com/minio/minio-go/v7 v7.0.66
|
||||
github.com/minio/minio-go/v7 v7.0.63
|
||||
github.com/prometheus/client_golang v1.17.0
|
||||
github.com/rs/xid v1.5.0
|
||||
github.com/rubenv/sql-migrate v1.6.0
|
||||
github.com/rubenv/sql-migrate v1.5.2
|
||||
github.com/toshi0607/chi-prometheus v0.1.4
|
||||
github.com/urfave/cli/v2 v2.27.0
|
||||
github.com/urfave/cli/v2 v2.25.7
|
||||
go.uber.org/zap v1.26.0
|
||||
golang.org/x/oauth2 v0.15.0
|
||||
google.golang.org/api v0.154.0
|
||||
golang.org/x/oauth2 v0.13.0
|
||||
google.golang.org/api v0.148.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute v1.23.3 // indirect
|
||||
cloud.google.com/go/compute v1.23.2 // 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,21 +41,18 @@ 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.1 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // 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.4 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
|
||||
github.com/klauspost/compress v1.17.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 // 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
|
||||
|
@ -70,22 +67,18 @@ 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-20231213231151-1d8dd44e695e // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // 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.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
|
||||
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
|
||||
google.golang.org/appengine v1.6.8 // 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
|
||||
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
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
)
|
||||
|
|
123
go.sum
123
go.sum
|
@ -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.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk=
|
||||
cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI=
|
||||
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/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,29 +37,25 @@ 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.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA=
|
||||
github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
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/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.8.0 h1:CyKng28yhGnlGXH9EDGC/Qizj29afJQSNW15W/yj34o=
|
||||
github.com/go-chi/httprate v0.8.0/go.mod h1:6GOYBSwnpra4CQfAKXu8sQZg+nZ0M1g9QnyFvxrAB8A=
|
||||
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/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=
|
||||
|
@ -95,31 +91,32 @@ 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.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||
github.com/google/uuid v1.5.0/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/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.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
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/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-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/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/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/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
||||
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||
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/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
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/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/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=
|
||||
|
@ -129,6 +126,9 @@ 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.66 h1:bnTOXOHjOqv/gcMuiVbN9o2ngRItvqE774dG9nq0Dzw=
|
||||
github.com/minio/minio-go/v7 v7.0.66/go.mod h1:DHAgmyQEGdW3Cif0UooKOyrT3Vxs82zNdV6tkKhRtbs=
|
||||
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/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.6.0 h1:IZpcTlAx/VKXphWEpwWJ7BaMq05tYtE80zYz+8a5Il8=
|
||||
github.com/rubenv/sql-migrate v1.6.0/go.mod h1:m3ilnKP7sNb4eYkLsp6cGdPOl4OBcXM6rcbzU+Oqc5k=
|
||||
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/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,28 +181,19 @@ 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.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
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.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/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/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=
|
||||
|
@ -214,12 +205,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.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
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.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
|
||||
golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||
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/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=
|
||||
|
@ -234,17 +225,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.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ=
|
||||
golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM=
|
||||
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/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.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
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/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=
|
||||
|
@ -255,19 +246,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.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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.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/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/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=
|
||||
|
@ -277,8 +268,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.154.0 h1:X7QkVKZBskztmpPKWQXgjJRPA2dJYrL6r+sYPRLj050=
|
||||
google.golang.org/api v0.154.0/go.mod h1:qhSMkM85hgqiokIYsrRyKxrjfBeIhgl4Z2JmeRkYylc=
|
||||
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/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=
|
||||
|
@ -286,15 +277,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-20231212172506-995d672761c0 h1:/jFB8jK5R3Sq3i/lmeZO0cATSzFfZaJq1J2Euan3XKU=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0/go.mod h1:FUoWkonphQm3RhTS+kOEhF8h0iDpm4tdXolVCeZ9KKA=
|
||||
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/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.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU=
|
||||
google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
|
||||
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/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=
|
||||
|
@ -306,8 +297,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.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
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=
|
||||
|
|
|
@ -2,12 +2,15 @@ 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"
|
||||
|
@ -77,7 +80,12 @@ func run(c *cli.Context) error {
|
|||
fmt.Printf("error starting transaction: %v\n", err)
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer func() {
|
||||
err := tx.Rollback(ctx)
|
||||
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||
log.Error("rolling back transaction:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
inactiveUsers, err := db.InactiveUsers(ctx, tx)
|
||||
if err != nil {
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
-- 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;
|
|
@ -1,10 +1,12 @@
|
|||
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"
|
||||
|
@ -82,7 +84,12 @@ func run(c *cli.Context) error {
|
|||
log.Println("error beginning transaction:", err)
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
defer func() {
|
||||
err := tx.Rollback(ctx)
|
||||
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||
log.Println("error rolling back transaction:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
for i, su := range seed.Users {
|
||||
u, err := pg.CreateUser(ctx, tx, su.Username)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package snowflakes
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
|
@ -39,7 +40,12 @@ func run(c *cli.Context) error {
|
|||
log.Error("creating transaction:", err)
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(c.Context)
|
||||
defer func() {
|
||||
err := tx.Rollback(c.Context)
|
||||
if err != nil && !errors.Is(err, pgx.ErrTxClosed) {
|
||||
log.Error("rolling back transaction:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
var userIDs []xid.ID
|
||||
err = pgxscan.Select(c.Context, conn, &userIDs, "SELECT id FROM users WHERE snowflake_id IS NULL")
|
||||
|
|
Loading…
Reference in a new issue