// GoToSocial // Copyright (C) GoToSocial Authors admin@gotosocial.org // SPDX-License-Identifier: AGPL-3.0-or-later // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package bundb import ( "context" "crypto/rand" "crypto/rsa" "errors" "fmt" "net/mail" "strings" "time" "github.com/google/uuid" "github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/log" "github.com/superseriousbusiness/gotosocial/internal/state" "github.com/superseriousbusiness/gotosocial/internal/uris" "github.com/superseriousbusiness/gotosocial/internal/util" "github.com/uptrace/bun" "golang.org/x/crypto/bcrypt" ) // generate RSA keys of this length const rsaKeyBits = 2048 type adminDB struct { db *bun.DB state *state.State } func (a *adminDB) IsUsernameAvailable(ctx context.Context, username string) (bool, error) { q := a.db. NewSelect(). TableExpr("? AS ?", bun.Ident("accounts"), bun.Ident("account")). Column("account.id"). Where("? = ?", bun.Ident("account.username"), username). Where("? IS NULL", bun.Ident("account.domain")) return notExists(ctx, q) } func (a *adminDB) IsEmailAvailable(ctx context.Context, email string) (bool, error) { // parse the domain from the email m, err := mail.ParseAddress(email) if err != nil { return false, fmt.Errorf("error parsing email address %s: %s", email, err) } domain := strings.Split(m.Address, "@")[1] // domain will always be the second part after @ // check if the email domain is blocked emailDomainBlockedQ := a.db. NewSelect(). TableExpr("? AS ?", bun.Ident("email_domain_blocks"), bun.Ident("email_domain_block")). Column("email_domain_block.id"). Where("? = ?", bun.Ident("email_domain_block.domain"), domain) emailDomainBlocked, err := exists(ctx, emailDomainBlockedQ) if err != nil { return false, err } if emailDomainBlocked { return false, fmt.Errorf("email domain %s is blocked", domain) } // check if this email is associated with a user already q := a.db. NewSelect(). TableExpr("? AS ?", bun.Ident("users"), bun.Ident("user")). Column("user.id"). Where("? = ?", bun.Ident("user.email"), email). WhereOr("? = ?", bun.Ident("user.unconfirmed_email"), email) return notExists(ctx, q) } func (a *adminDB) NewSignup(ctx context.Context, newSignup gtsmodel.NewSignup) (*gtsmodel.User, error) { // If something went wrong previously while doing a new // sign up with this username, we might already have an // account, so check first. account, err := a.state.DB.GetAccountByUsernameDomain(ctx, newSignup.Username, "") if err != nil && !errors.Is(err, db.ErrNoEntries) { // Real error occurred. err := gtserror.Newf("error checking for existing account: %w", err) return nil, err } // If we didn't yet have an account // with this username, create one now. if account == nil { uris := uris.GenerateURIsForAccount(newSignup.Username) accountID, err := id.NewRandomULID() if err != nil { err := gtserror.Newf("error creating new account id: %w", err) return nil, err } privKey, err := rsa.GenerateKey(rand.Reader, rsaKeyBits) if err != nil { err := gtserror.Newf("error creating new rsa private key: %w", err) return nil, err } settings := >smodel.AccountSettings{ AccountID: accountID, Privacy: gtsmodel.VisibilityDefault, } // Insert the settings! if err := a.state.DB.PutAccountSettings(ctx, settings); err != nil { return nil, err } account = >smodel.Account{ ID: accountID, Username: newSignup.Username, DisplayName: newSignup.Username, URI: uris.UserURI, URL: uris.UserURL, InboxURI: uris.InboxURI, OutboxURI: uris.OutboxURI, FollowingURI: uris.FollowingURI, FollowersURI: uris.FollowersURI, FeaturedCollectionURI: uris.FeaturedCollectionURI, ActorType: ap.ActorPerson, PrivateKey: privKey, PublicKey: &privKey.PublicKey, PublicKeyURI: uris.PublicKeyURI, Settings: settings, } // Insert the new account! if err := a.state.DB.PutAccount(ctx, account); err != nil { return nil, err } } // Created or already had an account. // Ensure user not already created. user, err := a.state.DB.GetUserByAccountID(ctx, account.ID) if err != nil && !errors.Is(err, db.ErrNoEntries) { // Real error occurred. err := gtserror.Newf("error checking for existing user: %w", err) return nil, err } defer func() { // Pin account to (new) // user before returning. user.Account = account }() if user != nil { // Already had a user for this // account, just return that. return user, nil } // Had no user for this account, time to create one! newUserID, err := id.NewRandomULID() if err != nil { err := gtserror.Newf("error creating new user id: %w", err) return nil, err } encryptedPassword, err := bcrypt.GenerateFromPassword( []byte(newSignup.Password), bcrypt.DefaultCost, ) if err != nil { err := gtserror.Newf("error hashing password: %w", err) return nil, err } user = >smodel.User{ ID: newUserID, AccountID: account.ID, Account: account, EncryptedPassword: string(encryptedPassword), SignUpIP: newSignup.SignUpIP.To4(), Reason: newSignup.Reason, Locale: newSignup.Locale, UnconfirmedEmail: newSignup.Email, CreatedByApplicationID: newSignup.AppID, ExternalID: newSignup.ExternalID, } if newSignup.EmailVerified { // Mark given email as confirmed. user.ConfirmedAt = time.Now() user.Email = newSignup.Email } if newSignup.Admin { // Make new user mod + admin. user.Moderator = util.Ptr(true) user.Admin = util.Ptr(true) } if newSignup.PreApproved { // Mark new user as approved. user.Approved = util.Ptr(true) } // Insert the user! if err := a.state.DB.PutUser(ctx, user); err != nil { err := gtserror.Newf("db error inserting user: %w", err) return nil, err } return user, nil } func (a *adminDB) CreateInstanceAccount(ctx context.Context) error { username := config.GetHost() q := a.db. NewSelect(). TableExpr("? AS ?", bun.Ident("accounts"), bun.Ident("account")). Column("account.id"). Where("? = ?", bun.Ident("account.username"), username). Where("? IS NULL", bun.Ident("account.domain")) exists, err := exists(ctx, q) if err != nil { return err } if exists { log.Infof(ctx, "instance account %s already exists", username) return nil } key, err := rsa.GenerateKey(rand.Reader, rsaKeyBits) if err != nil { log.Errorf(ctx, "error creating new rsa key: %s", err) return err } aID, err := id.NewRandomULID() if err != nil { return err } newAccountURIs := uris.GenerateURIsForAccount(username) acct := >smodel.Account{ ID: aID, Username: username, DisplayName: username, URL: newAccountURIs.UserURL, PrivateKey: key, PublicKey: &key.PublicKey, PublicKeyURI: newAccountURIs.PublicKeyURI, ActorType: ap.ActorPerson, URI: newAccountURIs.UserURI, InboxURI: newAccountURIs.InboxURI, OutboxURI: newAccountURIs.OutboxURI, FollowersURI: newAccountURIs.FollowersURI, FollowingURI: newAccountURIs.FollowingURI, FeaturedCollectionURI: newAccountURIs.FeaturedCollectionURI, } // insert the new account! if err := a.state.DB.PutAccount(ctx, acct); err != nil { return err } log.Infof(ctx, "instance account %s CREATED with id %s", username, acct.ID) return nil } func (a *adminDB) CreateInstanceInstance(ctx context.Context) error { protocol := config.GetProtocol() host := config.GetHost() // check if instance entry already exists q := a.db. NewSelect(). Column("instance.id"). TableExpr("? AS ?", bun.Ident("instances"), bun.Ident("instance")). Where("? = ?", bun.Ident("instance.domain"), host) exists, err := exists(ctx, q) if err != nil { return err } if exists { log.Infof(ctx, "instance entry already exists") return nil } iID, err := id.NewRandomULID() if err != nil { return err } i := >smodel.Instance{ ID: iID, Domain: host, Title: host, URI: fmt.Sprintf("%s://%s", protocol, host), } insertQ := a.db. NewInsert(). Model(i) _, err = insertQ.Exec(ctx) if err != nil { return err } log.Infof(ctx, "created instance instance %s with id %s", host, i.ID) return nil } func (a *adminDB) CreateInstanceApplication(ctx context.Context) error { // Check if instance application already exists. // Instance application client_id always = the // instance account's ID so this is an easy check. instanceAcct, err := a.state.DB.GetInstanceAccount(ctx, "") if err != nil { return err } exists, err := exists( ctx, a.db. NewSelect(). Column("application.id"). TableExpr("? AS ?", bun.Ident("applications"), bun.Ident("application")). Where("? = ?", bun.Ident("application.client_id"), instanceAcct.ID), ) if err != nil { return err } if exists { log.Infof(ctx, "instance application already exists") return nil } // Generate new IDs for this // application and its client. protocol := config.GetProtocol() host := config.GetHost() url := protocol + "://" + host clientID := instanceAcct.ID clientSecret := uuid.NewString() appID, err := id.NewRandomULID() if err != nil { return err } // Generate the application // to put in the database. app := >smodel.Application{ ID: appID, Name: host + " instance application", Website: url, RedirectURI: url, ClientID: clientID, ClientSecret: clientSecret, Scopes: "write:accounts", } // Store it. if err := a.state.DB.PutApplication(ctx, app); err != nil { return err } // Model an oauth client // from the application. oc := >smodel.Client{ ID: clientID, Secret: clientSecret, Domain: url, } // Store it. return a.state.DB.PutClient(ctx, oc) } func (a *adminDB) GetInstanceApplication(ctx context.Context) (*gtsmodel.Application, error) { // Instance app clientID == instanceAcct.ID, // so get the instance account first. instanceAcct, err := a.state.DB.GetInstanceAccount(ctx, "") if err != nil { return nil, err } app := new(gtsmodel.Application) if err := a.db. NewSelect(). Model(app). Where("? = ?", bun.Ident("application.client_id"), instanceAcct.ID). Scan(ctx); err != nil { return nil, err } return app, nil } func (a *adminDB) CountApprovedSignupsSince(ctx context.Context, since time.Time) (int, error) { return a.db. NewSelect(). TableExpr("? AS ?", bun.Ident("users"), bun.Ident("user")). Where("? > ?", bun.Ident("user.created_at"), since). Where("? = ?", bun.Ident("user.approved"), true). Count(ctx) } func (a *adminDB) CountUnhandledSignups(ctx context.Context) (int, error) { return a.db. NewSelect(). TableExpr("? AS ?", bun.Ident("users"), bun.Ident("user")). // Approved is false by default. // Explicitly rejected sign-ups end up elsewhere. Where("? = ?", bun.Ident("user.approved"), false). Count(ctx) } /* ACTION FUNCS */ func (a *adminDB) GetAdminAction(ctx context.Context, id string) (*gtsmodel.AdminAction, error) { action := new(gtsmodel.AdminAction) if err := a.db. NewSelect(). Model(action). Scan(ctx); err != nil { return nil, err } return action, nil } func (a *adminDB) GetAdminActions(ctx context.Context) ([]*gtsmodel.AdminAction, error) { actions := make([]*gtsmodel.AdminAction, 0) if err := a.db. NewSelect(). Model(&actions). Scan(ctx); err != nil { return nil, err } return actions, nil } func (a *adminDB) PutAdminAction(ctx context.Context, action *gtsmodel.AdminAction) error { _, err := a.db. NewInsert(). Model(action). Exec(ctx) return err } func (a *adminDB) UpdateAdminAction(ctx context.Context, action *gtsmodel.AdminAction, columns ...string) error { // Update the action's last-updated action.UpdatedAt = time.Now() if len(columns) != 0 { columns = append(columns, "updated_at") } _, err := a.db. NewUpdate(). Model(action). Where("? = ?", bun.Ident("admin_action.id"), action.ID). Column(columns...). Exec(ctx) return err } func (a *adminDB) DeleteAdminAction(ctx context.Context, id string) error { _, err := a.db. NewDelete(). TableExpr("? AS ?", bun.Ident("admin_actions"), bun.Ident("admin_action")). Where("? = ?", bun.Ident("admin_action"), id). Exec(ctx) return err }