diff options
author | 2024-04-13 13:25:10 +0200 | |
---|---|---|
committer | 2024-04-13 13:25:10 +0200 | |
commit | 89e0cfd8741b6763ca04e90558bccf4c3c380cfa (patch) | |
tree | 5858ada73473816fa1982f12717b66996d163f9d /internal/db | |
parent | [performance] update GetAccountsByIDs() to use the new multi cache loader end... (diff) | |
download | gotosocial-89e0cfd8741b6763ca04e90558bccf4c3c380cfa.tar.xz |
[feature] Admin accounts endpoints; approve/reject sign-ups (#2826)
* update settings panels, add pending overview + approve/deny functions
* add admin accounts get, approve, reject
* send approved/rejected emails
* use signup URL
* docs!
* email
* swagger
* web linting
* fix email tests
* wee lil fixerinos
* use new paging logic for GetAccounts() series of admin endpoints, small changes to query building
* shuffle useAccountIDIn check *before* adding to query
* fix parse from toot react error
* use `netip.Addr`
* put valid slices in globals
* optimistic updates for account state
---------
Co-authored-by: kim <grufwub@gmail.com>
Diffstat (limited to 'internal/db')
-rw-r--r-- | internal/db/account.go | 21 | ||||
-rw-r--r-- | internal/db/bundb/account.go | 254 | ||||
-rw-r--r-- | internal/db/bundb/account_test.go | 185 | ||||
-rw-r--r-- | internal/db/bundb/user.go | 20 | ||||
-rw-r--r-- | internal/db/user.go | 6 |
5 files changed, 486 insertions, 0 deletions
diff --git a/internal/db/account.go b/internal/db/account.go index 45276f41f..7cdf7b57f 100644 --- a/internal/db/account.go +++ b/internal/db/account.go @@ -19,9 +19,11 @@ package db import ( "context" + "net/netip" "time" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/paging" ) // Account contains functions related to account getting/setting/creation. @@ -56,6 +58,25 @@ type Account interface { // GetAccountByFollowersURI returns one account with the given followers_uri, or an error if something goes wrong. GetAccountByFollowersURI(ctx context.Context, uri string) (*gtsmodel.Account, error) + // GetAccounts returns accounts + // with the given parameters. + GetAccounts( + ctx context.Context, + origin string, + status string, + mods bool, + invitedBy string, + username string, + displayName string, + domain string, + email string, + ip netip.Addr, + page *paging.Page, + ) ( + []*gtsmodel.Account, + error, + ) + // PopulateAccount ensures that all sub-models of an account are populated (e.g. avatar, header etc). PopulateAccount(ctx context.Context, account *gtsmodel.Account) error diff --git a/internal/db/bundb/account.go b/internal/db/bundb/account.go index 1ecf28e42..45e67c10b 100644 --- a/internal/db/bundb/account.go +++ b/internal/db/bundb/account.go @@ -20,6 +20,8 @@ package bundb import ( "context" "errors" + "fmt" + "net/netip" "slices" "strings" "time" @@ -31,6 +33,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/log" + "github.com/superseriousbusiness/gotosocial/internal/paging" "github.com/superseriousbusiness/gotosocial/internal/state" "github.com/superseriousbusiness/gotosocial/internal/util" "github.com/uptrace/bun" @@ -249,6 +252,257 @@ func (a *accountDB) GetInstanceAccount(ctx context.Context, domain string) (*gts return a.GetAccountByUsernameDomain(ctx, username, domain) } +func (a *accountDB) GetAccounts( + ctx context.Context, + origin string, + status string, + mods bool, + invitedBy string, + username string, + displayName string, + domain string, + email string, + ip netip.Addr, + page *paging.Page, +) ( + []*gtsmodel.Account, + error, +) { + var ( + // local users lists, + // required for some + // limiting parameters. + users []*gtsmodel.User + + // lazyLoadUsers only loads the users + // slice if it's required by params. + lazyLoadUsers = func() (err error) { + if users == nil { + users, err = a.state.DB.GetAllUsers(gtscontext.SetBarebones(ctx)) + if err != nil { + return fmt.Errorf("error getting users: %w", err) + } + } + return nil + } + + // Get paging params. + // + // Note this may be min_id OR since_id + // from the API, this gets handled below + // when checking order to reverse slice. + minID = page.GetMin() + maxID = page.GetMax() + limit = page.GetLimit() + order = page.GetOrder() + + // Make educated guess for slice size + accountIDs = make([]string, 0, limit) + accountIDIn []string + + useAccountIDIn bool + ) + + q := a.db. + NewSelect(). + TableExpr("? AS ?", bun.Ident("accounts"), bun.Ident("account")). + // Select only IDs from table + Column("account.id") + + // Return only accounts OLDER + // than account with maxID. + if maxID != "" { + maxIDAcct, err := a.GetAccountByID( + gtscontext.SetBarebones(ctx), + maxID, + ) + if err != nil { + return nil, fmt.Errorf("error getting maxID account %s: %w", maxID, err) + } + + q = q.Where("? < ?", bun.Ident("account.created_at"), maxIDAcct.CreatedAt) + } + + // Return only accounts NEWER + // than account with minID. + if minID != "" { + minIDAcct, err := a.GetAccountByID( + gtscontext.SetBarebones(ctx), + minID, + ) + if err != nil { + return nil, fmt.Errorf("error getting minID account %s: %w", minID, err) + } + + q = q.Where("? > ?", bun.Ident("account.created_at"), minIDAcct.CreatedAt) + } + + switch status { + + case "active": + // Get only enabled accounts. + if err := lazyLoadUsers(); err != nil { + return nil, err + } + for _, user := range users { + if !*user.Disabled { + accountIDIn = append(accountIDIn, user.AccountID) + } + } + useAccountIDIn = true + + case "pending": + // Get only unapproved accounts. + if err := lazyLoadUsers(); err != nil { + return nil, err + } + for _, user := range users { + if !*user.Approved { + accountIDIn = append(accountIDIn, user.AccountID) + } + } + useAccountIDIn = true + + case "disabled": + // Get only disabled accounts. + if err := lazyLoadUsers(); err != nil { + return nil, err + } + for _, user := range users { + if *user.Disabled { + accountIDIn = append(accountIDIn, user.AccountID) + } + } + useAccountIDIn = true + + case "silenced": + // Get only silenced accounts. + q = q.Where("? IS NOT NULL", bun.Ident("account.silenced_at")) + + case "suspended": + // Get only suspended accounts. + q = q.Where("? IS NOT NULL", bun.Ident("account.suspended_at")) + } + + if mods { + // Get only mod accounts. + if err := lazyLoadUsers(); err != nil { + return nil, err + } + for _, user := range users { + if *user.Moderator || *user.Admin { + accountIDIn = append(accountIDIn, user.AccountID) + } + } + useAccountIDIn = true + } + + // TODO: invitedBy + + if username != "" { + q = q.Where("? = ?", bun.Ident("account.username"), username) + } + + if displayName != "" { + q = q.Where("? = ?", bun.Ident("account.display_name"), displayName) + } + + if domain != "" { + q = q.Where("? = ?", bun.Ident("account.domain"), domain) + } + + if email != "" { + if err := lazyLoadUsers(); err != nil { + return nil, err + } + for _, user := range users { + if user.Email == email || user.UnconfirmedEmail == email { + accountIDIn = append(accountIDIn, user.AccountID) + } + } + useAccountIDIn = true + } + + // Use ip if not zero value. + if ip.IsValid() { + if err := lazyLoadUsers(); err != nil { + return nil, err + } + for _, user := range users { + if user.SignUpIP.String() == ip.String() { + accountIDIn = append(accountIDIn, user.AccountID) + } + } + useAccountIDIn = true + } + + if origin == "local" && !useAccountIDIn { + // In the case we're not already limiting + // by specific subset of account IDs, just + // use existing list of user.AccountIDs + // instead of adding WHERE to the query. + if err := lazyLoadUsers(); err != nil { + return nil, err + } + for _, user := range users { + accountIDIn = append(accountIDIn, user.AccountID) + } + useAccountIDIn = true + + } else if origin == "remote" { + if useAccountIDIn { + // useAccountIDIn specifically indicates + // a parameter that limits querying to + // local accounts, there will be none. + return nil, nil + } + + // Get only remote accounts. + q = q.Where("? IS NOT NULL", bun.Ident("account.domain")) + } + + if useAccountIDIn { + if len(accountIDIn) == 0 { + // There will be no + // possible answer. + return nil, nil + } + + q = q.Where("? IN (?)", bun.Ident("account.id"), bun.In(accountIDIn)) + } + + if limit > 0 { + // Limit amount of + // accounts returned. + q = q.Limit(limit) + } + + if order == paging.OrderAscending { + // Page up. + q = q.Order("account.created_at ASC") + } else { + // Page down. + q = q.Order("account.created_at DESC") + } + + if err := q.Scan(ctx, &accountIDs); err != nil { + return nil, err + } + + if len(accountIDs) == 0 { + return nil, nil + } + + // If we're paging up, we still want accounts + // to be sorted by createdAt desc, so reverse ids slice. + if order == paging.OrderAscending { + slices.Reverse(accountIDs) + } + + // Return account IDs loaded from cache + db. + return a.state.DB.GetAccountsByIDs(ctx, accountIDs) +} + func (a *accountDB) getAccount(ctx context.Context, lookup string, dbQuery func(*gtsmodel.Account) error, keyParts ...any) (*gtsmodel.Account, error) { // Fetch account from database cache with loader callback account, err := a.state.Caches.GTS.Account.LoadOne(lookup, func() (*gtsmodel.Account, error) { diff --git a/internal/db/bundb/account_test.go b/internal/db/bundb/account_test.go index 21e04dedc..dd96543b6 100644 --- a/internal/db/bundb/account_test.go +++ b/internal/db/bundb/account_test.go @@ -23,6 +23,7 @@ import ( "crypto/rsa" "errors" "fmt" + "net/netip" "reflect" "strings" "testing" @@ -33,6 +34,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db/bundb" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/paging" "github.com/superseriousbusiness/gotosocial/internal/util" "github.com/uptrace/bun" ) @@ -491,6 +493,189 @@ func (suite *AccountTestSuite) TestPopulateAccountWithUnknownMovedToURI() { suite.NoError(err) } +func (suite *AccountTestSuite) TestGetAccountsAll() { + var ( + ctx = context.Background() + origin = "" + status = "" + mods = false + invitedBy = "" + username = "" + displayName = "" + domain = "" + email = "" + ip netip.Addr + page *paging.Page = nil + ) + + accounts, err := suite.db.GetAccounts( + ctx, + origin, + status, + mods, + invitedBy, + username, + displayName, + domain, + email, + ip, + page, + ) + if err != nil { + suite.FailNow(err.Error()) + } + + suite.Len(accounts, 9) +} + +func (suite *AccountTestSuite) TestGetAccountsModsOnly() { + var ( + ctx = context.Background() + origin = "" + status = "" + mods = true + invitedBy = "" + username = "" + displayName = "" + domain = "" + email = "" + ip netip.Addr + page = &paging.Page{ + Limit: 100, + } + ) + + accounts, err := suite.db.GetAccounts( + ctx, + origin, + status, + mods, + invitedBy, + username, + displayName, + domain, + email, + ip, + page, + ) + if err != nil { + suite.FailNow(err.Error()) + } + + suite.Len(accounts, 1) +} + +func (suite *AccountTestSuite) TestGetAccountsLocalWithEmail() { + var ( + ctx = context.Background() + origin = "local" + status = "" + mods = false + invitedBy = "" + username = "" + displayName = "" + domain = "" + email = "tortle.dude@example.org" + ip netip.Addr + page = &paging.Page{ + Limit: 100, + } + ) + + accounts, err := suite.db.GetAccounts( + ctx, + origin, + status, + mods, + invitedBy, + username, + displayName, + domain, + email, + ip, + page, + ) + if err != nil { + suite.FailNow(err.Error()) + } + + suite.Len(accounts, 1) +} + +func (suite *AccountTestSuite) TestGetAccountsWithIP() { + var ( + ctx = context.Background() + origin = "" + status = "" + mods = false + invitedBy = "" + username = "" + displayName = "" + domain = "" + email = "" + ip = netip.MustParseAddr("199.222.111.89") + page = &paging.Page{ + Limit: 100, + } + ) + + accounts, err := suite.db.GetAccounts( + ctx, + origin, + status, + mods, + invitedBy, + username, + displayName, + domain, + email, + ip, + page, + ) + if err != nil { + suite.FailNow(err.Error()) + } + + suite.Len(accounts, 1) +} + +func (suite *AccountTestSuite) TestGetPendingAccounts() { + var ( + ctx = context.Background() + origin = "" + status = "pending" + mods = false + invitedBy = "" + username = "" + displayName = "" + domain = "" + email = "" + ip netip.Addr + page = &paging.Page{ + Limit: 100, + } + ) + + accounts, err := suite.db.GetAccounts( + ctx, + origin, + status, + mods, + invitedBy, + username, + displayName, + domain, + email, + ip, + page, + ) + if err != nil { + suite.FailNow(err.Error()) + } + + suite.Len(accounts, 1) +} + func TestAccountTestSuite(t *testing.T) { suite.Run(t, new(AccountTestSuite)) } diff --git a/internal/db/bundb/user.go b/internal/db/bundb/user.go index 2854c0caa..f0221eeb1 100644 --- a/internal/db/bundb/user.go +++ b/internal/db/bundb/user.go @@ -230,3 +230,23 @@ func (u *userDB) DeleteUserByID(ctx context.Context, userID string) error { Exec(ctx) return err } + +func (u *userDB) PutDeniedUser(ctx context.Context, deniedUser *gtsmodel.DeniedUser) error { + _, err := u.db.NewInsert(). + Model(deniedUser). + Exec(ctx) + return err +} + +func (u *userDB) GetDeniedUserByID(ctx context.Context, id string) (*gtsmodel.DeniedUser, error) { + deniedUser := new(gtsmodel.DeniedUser) + if err := u.db. + NewSelect(). + Model(deniedUser). + Where("? = ?", bun.Ident("denied_user.id"), id). + Scan(ctx); err != nil { + return nil, err + } + + return deniedUser, nil +} diff --git a/internal/db/user.go b/internal/db/user.go index c762ef2b3..28fa59130 100644 --- a/internal/db/user.go +++ b/internal/db/user.go @@ -54,4 +54,10 @@ type User interface { // DeleteUserByID deletes one user by its ID. DeleteUserByID(ctx context.Context, userID string) error + + // PutDeniedUser inserts the given deniedUser into the db. + PutDeniedUser(ctx context.Context, deniedUser *gtsmodel.DeniedUser) error + + // GetDeniedUserByID returns one denied user with the given ID. + GetDeniedUserByID(ctx context.Context, id string) (*gtsmodel.DeniedUser, error) } |