mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-10-29 13:32:25 -05:00
[feature] Self-serve email change for users (#2957)
* [feature] Email change * frontend stuff for changing email * docs * tests etc * differentiate more clearly between local user+account and account * populate user
This commit is contained in:
parent
131020faeb
commit
bcda048eab
50 changed files with 1118 additions and 309 deletions
|
|
@ -22,7 +22,6 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing/common"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/text"
|
||||
|
|
@ -39,7 +38,6 @@ type Processor struct {
|
|||
state *state.State
|
||||
converter *typeutils.Converter
|
||||
mediaManager *media.Manager
|
||||
oauthServer oauth.Server
|
||||
filter *visibility.Filter
|
||||
formatter *text.Formatter
|
||||
federator *federation.Federator
|
||||
|
|
@ -53,7 +51,6 @@ func New(
|
|||
state *state.State,
|
||||
converter *typeutils.Converter,
|
||||
mediaManager *media.Manager,
|
||||
oauthServer oauth.Server,
|
||||
federator *federation.Federator,
|
||||
filter *visibility.Filter,
|
||||
parseMention gtsmodel.ParseMentionFunc,
|
||||
|
|
@ -63,7 +60,6 @@ func New(
|
|||
state: state,
|
||||
converter: converter,
|
||||
mediaManager: mediaManager,
|
||||
oauthServer: oauthServer,
|
||||
filter: filter,
|
||||
formatter: text.NewFormatter(state.DB),
|
||||
federator: federator,
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ import (
|
|||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing/account"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing/common"
|
||||
|
|
@ -48,7 +47,6 @@ type AccountStandardTestSuite struct {
|
|||
storage *storage.Driver
|
||||
state state.State
|
||||
mediaManager *media.Manager
|
||||
oauthServer oauth.Server
|
||||
transportController transport.Controller
|
||||
federator *federation.Federator
|
||||
emailSender email.Sender
|
||||
|
|
@ -106,7 +104,6 @@ func (suite *AccountStandardTestSuite) SetupTest() {
|
|||
suite.storage = testrig.NewInMemoryStorage()
|
||||
suite.state.Storage = suite.storage
|
||||
suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
|
||||
suite.oauthServer = testrig.NewTestOauthServer(suite.db)
|
||||
|
||||
suite.transportController = testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../testrig/media"))
|
||||
suite.federator = testrig.NewTestFederator(&suite.state, suite.transportController, suite.mediaManager)
|
||||
|
|
@ -115,7 +112,7 @@ func (suite *AccountStandardTestSuite) SetupTest() {
|
|||
|
||||
filter := visibility.NewFilter(&suite.state)
|
||||
common := common.New(&suite.state, suite.tc, suite.federator, filter)
|
||||
suite.accountProcessor = account.New(&common, &suite.state, suite.tc, suite.mediaManager, suite.oauthServer, suite.federator, filter, processing.GetParseMentionFunc(&suite.state, suite.federator))
|
||||
suite.accountProcessor = account.New(&common, &suite.state, suite.tc, suite.mediaManager, suite.federator, filter, processing.GetParseMentionFunc(&suite.state, suite.federator))
|
||||
testrig.StandardDBSetup(suite.db, nil)
|
||||
testrig.StandardStorageSetup(suite.storage, "../../../testrig/media")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,165 +0,0 @@
|
|||
// 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 account
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/text"
|
||||
"github.com/superseriousbusiness/oauth2/v4"
|
||||
)
|
||||
|
||||
// Create processes the given form for creating a new account,
|
||||
// returning a new user (with attached account) if successful.
|
||||
//
|
||||
// App should be the app used to create the account.
|
||||
// If nil, the instance app will be used.
|
||||
//
|
||||
// Precondition: the form's fields should have already been
|
||||
// validated and normalized by the caller.
|
||||
func (p *Processor) Create(
|
||||
ctx context.Context,
|
||||
app *gtsmodel.Application,
|
||||
form *apimodel.AccountCreateRequest,
|
||||
) (*gtsmodel.User, gtserror.WithCode) {
|
||||
const (
|
||||
usersPerDay = 10
|
||||
regBacklog = 20
|
||||
)
|
||||
|
||||
// Ensure no more than usersPerDay
|
||||
// have registered in the last 24h.
|
||||
newUsersCount, err := p.state.DB.CountApprovedSignupsSince(ctx, time.Now().Add(-24*time.Hour))
|
||||
if err != nil {
|
||||
err := fmt.Errorf("db error counting new users: %w", err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
if newUsersCount >= usersPerDay {
|
||||
err := fmt.Errorf("this instance has hit its limit of new sign-ups for today; you can try again tomorrow")
|
||||
return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
||||
}
|
||||
|
||||
// Ensure the new users backlog isn't full.
|
||||
backlogLen, err := p.state.DB.CountUnhandledSignups(ctx)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("db error counting registration backlog length: %w", err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
if backlogLen >= regBacklog {
|
||||
err := fmt.Errorf("this instance's sign-up backlog is currently full; you must wait until pending sign-ups are handled by the admin(s)")
|
||||
return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
||||
}
|
||||
|
||||
emailAvailable, err := p.state.DB.IsEmailAvailable(ctx, form.Email)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("db error checking email availability: %w", err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
if !emailAvailable {
|
||||
err := fmt.Errorf("email address %s is not available", form.Email)
|
||||
return nil, gtserror.NewErrorConflict(err, err.Error())
|
||||
}
|
||||
|
||||
usernameAvailable, err := p.state.DB.IsUsernameAvailable(ctx, form.Username)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("db error checking username availability: %w", err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
if !usernameAvailable {
|
||||
err := fmt.Errorf("username %s is not available", form.Username)
|
||||
return nil, gtserror.NewErrorConflict(err, err.Error())
|
||||
}
|
||||
|
||||
// Only store reason if one is required.
|
||||
var reason string
|
||||
if config.GetAccountsReasonRequired() {
|
||||
reason = form.Reason
|
||||
}
|
||||
|
||||
// Use instance app if no app provided.
|
||||
if app == nil {
|
||||
app, err = p.state.DB.GetInstanceApplication(ctx)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("db error getting instance app: %w", err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
}
|
||||
|
||||
user, err := p.state.DB.NewSignup(ctx, gtsmodel.NewSignup{
|
||||
Username: form.Username,
|
||||
Email: form.Email,
|
||||
Password: form.Password,
|
||||
Reason: text.SanitizeToPlaintext(reason),
|
||||
SignUpIP: form.IP,
|
||||
Locale: form.Locale,
|
||||
AppID: app.ID,
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("db error creating new signup: %w", err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
// There are side effects for creating a new account
|
||||
// (confirmation emails etc), perform these async.
|
||||
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
|
||||
APObjectType: ap.ObjectProfile,
|
||||
APActivityType: ap.ActivityCreate,
|
||||
GTSModel: user,
|
||||
Origin: user.Account,
|
||||
})
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// TokenForNewUser generates an OAuth Bearer token
|
||||
// for a new user (with account) created by Create().
|
||||
func (p *Processor) TokenForNewUser(
|
||||
ctx context.Context,
|
||||
appToken oauth2.TokenInfo,
|
||||
app *gtsmodel.Application,
|
||||
user *gtsmodel.User,
|
||||
) (*apimodel.Token, gtserror.WithCode) {
|
||||
// Generate access token.
|
||||
accessToken, err := p.oauthServer.GenerateUserAccessToken(
|
||||
ctx,
|
||||
appToken,
|
||||
app.ClientSecret,
|
||||
user.ID,
|
||||
)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("error creating new access token for user %s: %w", user.ID, err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
return &apimodel.Token{
|
||||
AccessToken: accessToken.GetAccess(),
|
||||
TokenType: "Bearer",
|
||||
Scope: accessToken.GetScope(),
|
||||
CreatedAt: accessToken.GetAccessCreateAt().Unix(),
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -95,23 +95,6 @@ func (p *Processor) Delete(
|
|||
return nil
|
||||
}
|
||||
|
||||
// DeleteSelf is like Delete, but specifically for local accounts deleting themselves.
|
||||
//
|
||||
// Calling DeleteSelf results in a delete message being enqueued in the processor,
|
||||
// which causes side effects to occur: delete will be federated out to other instances,
|
||||
// and the above Delete function will be called afterwards from the processor, to clear
|
||||
// out the account's bits and bobs, and stubbify it.
|
||||
func (p *Processor) DeleteSelf(ctx context.Context, account *gtsmodel.Account) gtserror.WithCode {
|
||||
// Process the delete side effects asynchronously.
|
||||
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
|
||||
APObjectType: ap.ActorPerson,
|
||||
APActivityType: ap.ActivityDelete,
|
||||
Origin: account,
|
||||
Target: account,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteUserAndTokensForAccount deletes the gtsmodel.User and
|
||||
// any OAuth tokens and applications for the given account.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -297,7 +297,7 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
|
|||
}
|
||||
|
||||
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
|
||||
APObjectType: ap.ObjectProfile,
|
||||
APObjectType: ap.ActorPerson,
|
||||
APActivityType: ap.ActivityUpdate,
|
||||
GTSModel: account,
|
||||
Origin: account,
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateSimple() {
|
|||
|
||||
// Profile update.
|
||||
suite.Equal(ap.ActivityUpdate, msg.APActivityType)
|
||||
suite.Equal(ap.ObjectProfile, msg.APObjectType)
|
||||
suite.Equal(ap.ActorPerson, msg.APObjectType)
|
||||
|
||||
// Correct account updated.
|
||||
if msg.Origin == nil {
|
||||
|
|
@ -114,7 +114,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateWithMention() {
|
|||
|
||||
// Profile update.
|
||||
suite.Equal(ap.ActivityUpdate, msg.APActivityType)
|
||||
suite.Equal(ap.ObjectProfile, msg.APObjectType)
|
||||
suite.Equal(ap.ActorPerson, msg.APObjectType)
|
||||
|
||||
// Correct account updated.
|
||||
if msg.Origin == nil {
|
||||
|
|
@ -170,7 +170,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateWithMarkdownNote() {
|
|||
|
||||
// Profile update.
|
||||
suite.Equal(ap.ActivityUpdate, msg.APActivityType)
|
||||
suite.Equal(ap.ObjectProfile, msg.APObjectType)
|
||||
suite.Equal(ap.ActorPerson, msg.APObjectType)
|
||||
|
||||
// Correct account updated.
|
||||
if msg.Origin == nil {
|
||||
|
|
@ -255,7 +255,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateWithFields() {
|
|||
|
||||
// Profile update.
|
||||
suite.Equal(ap.ActivityUpdate, msg.APActivityType)
|
||||
suite.Equal(ap.ObjectProfile, msg.APObjectType)
|
||||
suite.Equal(ap.ActorPerson, msg.APObjectType)
|
||||
|
||||
// Correct account updated.
|
||||
if msg.Origin == nil {
|
||||
|
|
@ -312,7 +312,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateNoteNotFields() {
|
|||
|
||||
// Profile update.
|
||||
suite.Equal(ap.ActivityUpdate, msg.APActivityType)
|
||||
suite.Equal(ap.ObjectProfile, msg.APObjectType)
|
||||
suite.Equal(ap.ActorPerson, msg.APObjectType)
|
||||
|
||||
// Correct account updated.
|
||||
if msg.Origin == nil {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue