mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-01 15:00:00 +00:00
5e2bf0bdca
* [chore] Remove years from all license headers Years or year ranges aren't required in license headers. Many projects have removed them in recent years and it avoids a bit of yearly toil. In many cases our copyright claim was also a bit dodgy since we added the 2021-2023 header to files created after 2021 but you can't claim copyright into the past that way. * [chore] Add license header check This ensures a license header is always added to any new file. This avoids maintainers/reviewers needing to remember to check for and ask for it in case a contribution doesn't include it. * [chore] Add missing license headers * [chore] Further updates to license header * Use the more common // indentend comment format * Remove the hack we had for the linter now that we use the // format * Add SPDX license identifier
295 lines
8.5 KiB
Go
295 lines
8.5 KiB
Go
// 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 <http://www.gnu.org/licenses/>.
|
|
|
|
package bundb
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"fmt"
|
|
"net"
|
|
"net/mail"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
|
"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/uptrace/bun"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
// generate RSA keys of this length
|
|
const rsaKeyBits = 2048
|
|
|
|
type adminDB struct {
|
|
conn *DBConn
|
|
state *state.State
|
|
}
|
|
|
|
func (a *adminDB) IsUsernameAvailable(ctx context.Context, username string) (bool, db.Error) {
|
|
q := a.conn.
|
|
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 a.conn.NotExists(ctx, q)
|
|
}
|
|
|
|
func (a *adminDB) IsEmailAvailable(ctx context.Context, email string) (bool, db.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.conn.
|
|
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 := a.conn.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.conn.
|
|
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 a.conn.NotExists(ctx, q)
|
|
}
|
|
|
|
func (a *adminDB) NewSignup(ctx context.Context, username string, reason string, requireApproval bool, email string, password string, signUpIP net.IP, locale string, appID string, emailVerified bool, externalID string, admin bool) (*gtsmodel.User, db.Error) {
|
|
key, err := rsa.GenerateKey(rand.Reader, rsaKeyBits)
|
|
if err != nil {
|
|
log.Errorf(ctx, "error creating new rsa key: %s", err)
|
|
return nil, err
|
|
}
|
|
|
|
// if something went wrong while creating a user, we might already have an account, so check here first...
|
|
acct := >smodel.Account{}
|
|
if err := a.conn.
|
|
NewSelect().
|
|
Model(acct).
|
|
Where("? = ?", bun.Ident("account.username"), username).
|
|
WhereGroup(" AND ", whereEmptyOrNull("account.domain")).
|
|
Scan(ctx); err != nil {
|
|
err = a.conn.ProcessError(err)
|
|
if err != db.ErrNoEntries {
|
|
log.Errorf(ctx, "error checking for existing account: %s", err)
|
|
return nil, err
|
|
}
|
|
|
|
// if we have db.ErrNoEntries, we just don't have an
|
|
// account yet so create one before we proceed
|
|
accountURIs := uris.GenerateURIsForAccount(username)
|
|
accountID, err := id.NewRandomULID()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
acct = >smodel.Account{
|
|
ID: accountID,
|
|
Username: username,
|
|
DisplayName: username,
|
|
Reason: reason,
|
|
Privacy: gtsmodel.VisibilityDefault,
|
|
URL: accountURIs.UserURL,
|
|
PrivateKey: key,
|
|
PublicKey: &key.PublicKey,
|
|
PublicKeyURI: accountURIs.PublicKeyURI,
|
|
ActorType: ap.ActorPerson,
|
|
URI: accountURIs.UserURI,
|
|
InboxURI: accountURIs.InboxURI,
|
|
OutboxURI: accountURIs.OutboxURI,
|
|
FollowersURI: accountURIs.FollowersURI,
|
|
FollowingURI: accountURIs.FollowingURI,
|
|
FeaturedCollectionURI: accountURIs.FeaturedCollectionURI,
|
|
}
|
|
|
|
// insert the new account!
|
|
if err := a.state.DB.PutAccount(ctx, acct); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// we either created or already had an account by now,
|
|
// so proceed with creating a user for that account
|
|
|
|
pw, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error hashing password: %s", err)
|
|
}
|
|
|
|
newUserID, err := id.NewRandomULID()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// if we don't require moderator approval, just pre-approve the user
|
|
approved := !requireApproval
|
|
u := >smodel.User{
|
|
ID: newUserID,
|
|
AccountID: acct.ID,
|
|
Account: acct,
|
|
EncryptedPassword: string(pw),
|
|
SignUpIP: signUpIP.To4(),
|
|
Locale: locale,
|
|
UnconfirmedEmail: email,
|
|
CreatedByApplicationID: appID,
|
|
Approved: &approved,
|
|
ExternalID: externalID,
|
|
}
|
|
|
|
if emailVerified {
|
|
u.ConfirmedAt = time.Now()
|
|
u.Email = email
|
|
}
|
|
|
|
if admin {
|
|
admin := true
|
|
moderator := true
|
|
u.Admin = &admin
|
|
u.Moderator = &moderator
|
|
}
|
|
|
|
// insert the user!
|
|
if err := a.state.DB.PutUser(ctx, u); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return u, nil
|
|
}
|
|
|
|
func (a *adminDB) CreateInstanceAccount(ctx context.Context) db.Error {
|
|
username := config.GetHost()
|
|
|
|
q := a.conn.
|
|
NewSelect().
|
|
TableExpr("? AS ?", bun.Ident("accounts"), bun.Ident("account")).
|
|
Column("account.id").
|
|
Where("? = ?", bun.Ident("account.username"), username).
|
|
WhereGroup(" AND ", whereEmptyOrNull("account.domain"))
|
|
|
|
exists, err := a.conn.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) db.Error {
|
|
protocol := config.GetProtocol()
|
|
host := config.GetHost()
|
|
|
|
// check if instance entry already exists
|
|
q := a.conn.
|
|
NewSelect().
|
|
Column("instance.id").
|
|
TableExpr("? AS ?", bun.Ident("instances"), bun.Ident("instance")).
|
|
Where("? = ?", bun.Ident("instance.domain"), host)
|
|
|
|
exists, err := a.conn.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.conn.
|
|
NewInsert().
|
|
Model(i)
|
|
|
|
_, err = insertQ.Exec(ctx)
|
|
if err != nil {
|
|
return a.conn.ProcessError(err)
|
|
}
|
|
|
|
log.Infof(ctx, "created instance instance %s with id %s", host, i.ID)
|
|
return nil
|
|
}
|