mirror of
https://github.com/superseriousbusiness/gotosocial
synced 2025-06-05 21:59:39 +02: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:
@@ -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")
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -1,106 +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 processing_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/activity/pub"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
type AccountTestSuite struct {
|
||||
ProcessingStandardTestSuite
|
||||
}
|
||||
|
||||
func (suite *AccountTestSuite) TestAccountDeleteLocal() {
|
||||
ctx := context.Background()
|
||||
deletingAccount := suite.testAccounts["local_account_1"]
|
||||
followingAccount := suite.testAccounts["remote_account_1"]
|
||||
|
||||
// make the following account follow the deleting account so that a delete message will be sent to it via the federating API
|
||||
follow := >smodel.Follow{
|
||||
ID: "01FJ1S8DX3STJJ6CEYPMZ1M0R3",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
URI: fmt.Sprintf("%s/follow/01FJ1S8DX3STJJ6CEYPMZ1M0R3", followingAccount.URI),
|
||||
AccountID: followingAccount.ID,
|
||||
TargetAccountID: deletingAccount.ID,
|
||||
}
|
||||
err := suite.db.Put(ctx, follow)
|
||||
suite.NoError(err)
|
||||
|
||||
errWithCode := suite.processor.Account().DeleteSelf(ctx, suite.testAccounts["local_account_1"])
|
||||
suite.NoError(errWithCode)
|
||||
|
||||
// the delete should be federated outwards to the following account's inbox
|
||||
var sent []byte
|
||||
delete := new(struct {
|
||||
Actor string `json:"actor"`
|
||||
ID string `json:"id"`
|
||||
Object string `json:"object"`
|
||||
To string `json:"to"`
|
||||
CC string `json:"cc"`
|
||||
Type string `json:"type"`
|
||||
})
|
||||
|
||||
if !testrig.WaitFor(func() bool {
|
||||
delivery, ok := suite.state.Workers.Delivery.Queue.Pop()
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !testrig.EqualRequestURIs(delivery.Request.URL, *followingAccount.SharedInboxURI) {
|
||||
panic("differing request uris")
|
||||
}
|
||||
sent, err = io.ReadAll(delivery.Request.Body)
|
||||
if err != nil {
|
||||
panic("error reading body: " + err.Error())
|
||||
}
|
||||
err = json.Unmarshal(sent, delete)
|
||||
if err != nil {
|
||||
panic("error unmarshaling json: " + err.Error())
|
||||
}
|
||||
return true
|
||||
}) {
|
||||
suite.FailNow("timed out waiting for message")
|
||||
}
|
||||
|
||||
suite.Equal(deletingAccount.URI, delete.Actor)
|
||||
suite.Equal(deletingAccount.URI, delete.Object)
|
||||
suite.Equal(deletingAccount.FollowersURI, delete.To)
|
||||
suite.Equal(pub.PublicActivityPubIRI, delete.CC)
|
||||
suite.Equal("Delete", delete.Type)
|
||||
|
||||
if !testrig.WaitFor(func() bool {
|
||||
dbAccount, _ := suite.db.GetAccountByID(ctx, deletingAccount.ID)
|
||||
return !dbAccount.SuspendedAt.IsZero()
|
||||
}) {
|
||||
suite.FailNow("timed out waiting for account to be deleted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountTestSuite(t *testing.T) {
|
||||
suite.Run(t, &AccountTestSuite{})
|
||||
}
|
@@ -30,7 +30,7 @@ import (
|
||||
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||||
)
|
||||
|
||||
func (p *Processor) AccountApprove(
|
||||
func (p *Processor) SignupApprove(
|
||||
ctx context.Context,
|
||||
adminAcct *gtsmodel.Account,
|
||||
accountID string,
|
||||
@@ -55,7 +55,10 @@ func (p *Processor) AccountApprove(
|
||||
if !*user.Approved {
|
||||
// Process approval side effects asynschronously.
|
||||
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
|
||||
APObjectType: ap.ActorPerson,
|
||||
// Use ap.ObjectProfile here to
|
||||
// distinguish this message (user model)
|
||||
// from ap.ActorPerson (account model).
|
||||
APObjectType: ap.ObjectProfile,
|
||||
APActivityType: ap.ActivityAccept,
|
||||
GTSModel: user,
|
||||
Origin: adminAcct,
|
@@ -42,7 +42,7 @@ func (suite *AdminApproveTestSuite) TestApprove() {
|
||||
*targetUser = *suite.testUsers["unconfirmed_account"]
|
||||
|
||||
// Approve the sign-up.
|
||||
acct, errWithCode := suite.adminProcessor.AccountApprove(
|
||||
acct, errWithCode := suite.adminProcessor.SignupApprove(
|
||||
ctx,
|
||||
adminAcct,
|
||||
targetAcct.ID,
|
@@ -30,7 +30,7 @@ import (
|
||||
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||||
)
|
||||
|
||||
func (p *Processor) AccountReject(
|
||||
func (p *Processor) SignupReject(
|
||||
ctx context.Context,
|
||||
adminAcct *gtsmodel.Account,
|
||||
accountID string,
|
||||
@@ -102,7 +102,10 @@ func (p *Processor) AccountReject(
|
||||
|
||||
// Process rejection side effects asynschronously.
|
||||
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
|
||||
APObjectType: ap.ActorPerson,
|
||||
// Use ap.ObjectProfile here to
|
||||
// distinguish this message (user model)
|
||||
// from ap.ActorPerson (account model).
|
||||
APObjectType: ap.ObjectProfile,
|
||||
APActivityType: ap.ActivityReject,
|
||||
GTSModel: deniedUser,
|
||||
Origin: adminAcct,
|
@@ -42,7 +42,7 @@ func (suite *AdminRejectTestSuite) TestReject() {
|
||||
message = "Too stinky."
|
||||
)
|
||||
|
||||
acct, errWithCode := suite.adminProcessor.AccountReject(
|
||||
acct, errWithCode := suite.adminProcessor.SignupReject(
|
||||
ctx,
|
||||
adminAcct,
|
||||
targetAcct.ID,
|
||||
@@ -104,7 +104,7 @@ func (suite *AdminRejectTestSuite) TestRejectRemote() {
|
||||
)
|
||||
|
||||
// Try to reject a remote account.
|
||||
_, err := suite.adminProcessor.AccountReject(
|
||||
_, err := suite.adminProcessor.SignupReject(
|
||||
ctx,
|
||||
adminAcct,
|
||||
targetAcct.ID,
|
||||
@@ -126,7 +126,7 @@ func (suite *AdminRejectTestSuite) TestRejectApproved() {
|
||||
)
|
||||
|
||||
// Try to reject an already-approved account.
|
||||
_, err := suite.adminProcessor.AccountReject(
|
||||
_, err := suite.adminProcessor.SignupReject(
|
||||
ctx,
|
||||
adminAcct,
|
||||
targetAcct.ID,
|
@@ -180,13 +180,13 @@ func NewProcessor(
|
||||
// Start with sub processors that will
|
||||
// be required by the workers processor.
|
||||
common := common.New(state, converter, federator, filter)
|
||||
processor.account = account.New(&common, state, converter, mediaManager, oauthServer, federator, filter, parseMentionFunc)
|
||||
processor.account = account.New(&common, state, converter, mediaManager, federator, filter, parseMentionFunc)
|
||||
processor.media = media.New(state, converter, mediaManager, federator.TransportController())
|
||||
processor.stream = stream.New(state, oauthServer)
|
||||
|
||||
// Instantiate the rest of the sub
|
||||
// processors + pin them to this struct.
|
||||
processor.account = account.New(&common, state, converter, mediaManager, oauthServer, federator, filter, parseMentionFunc)
|
||||
processor.account = account.New(&common, state, converter, mediaManager, federator, filter, parseMentionFunc)
|
||||
processor.admin = admin.New(state, cleaner, converter, mediaManager, federator.TransportController(), emailSender)
|
||||
processor.fedi = fedi.New(state, &common, converter, federator, filter)
|
||||
processor.filtersv1 = filtersv1.New(state, converter)
|
||||
@@ -198,7 +198,7 @@ func NewProcessor(
|
||||
processor.timeline = timeline.New(state, converter, filter)
|
||||
processor.search = search.New(state, federator, converter, filter)
|
||||
processor.status = status.New(state, &common, &processor.polls, federator, converter, filter, parseMentionFunc)
|
||||
processor.user = user.New(state, emailSender)
|
||||
processor.user = user.New(state, converter, oauthServer, emailSender)
|
||||
|
||||
// Workers processor handles asynchronous
|
||||
// worker jobs; instantiate it separately
|
||||
|
@@ -92,7 +92,7 @@ func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form
|
||||
}
|
||||
|
||||
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
|
||||
APObjectType: ap.ObjectProfile,
|
||||
APObjectType: ap.ActorPerson,
|
||||
APActivityType: ap.ActivityFlag,
|
||||
GTSModel: report,
|
||||
Origin: account,
|
||||
|
@@ -15,7 +15,7 @@
|
||||
// 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
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -32,10 +32,9 @@ import (
|
||||
"github.com/superseriousbusiness/oauth2/v4"
|
||||
)
|
||||
|
||||
// Create processes the given form for creating a new account,
|
||||
// returning a new user (with attached account) if successful.
|
||||
// Create processes the given form for creating a new user+account.
|
||||
//
|
||||
// App should be the app used to create the account.
|
||||
// App should be the app used to create the user+account.
|
||||
// If nil, the instance app will be used.
|
||||
//
|
||||
// Precondition: the form's fields should have already been
|
||||
@@ -124,9 +123,12 @@ func (p *Processor) Create(
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
// There are side effects for creating a new account
|
||||
// There are side effects for creating a new user+account
|
||||
// (confirmation emails etc), perform these async.
|
||||
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
|
||||
// Use ap.ObjectProfile here to
|
||||
// distinguish this message (user model)
|
||||
// from ap.ActorPerson (account model).
|
||||
APObjectType: ap.ObjectProfile,
|
||||
APActivityType: ap.ActivityCreate,
|
||||
GTSModel: user,
|
48
internal/processing/user/delete.go
Normal file
48
internal/processing/user/delete.go
Normal file
@@ -0,0 +1,48 @@
|
||||
// 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 user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||||
)
|
||||
|
||||
// DeleteSelf is like Account.Delete, but specifically
|
||||
// for local user+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{
|
||||
// Use ap.ObjectProfile here to
|
||||
// distinguish this message (user model)
|
||||
// from ap.ActorPerson (account model).
|
||||
APObjectType: ap.ObjectProfile,
|
||||
APActivityType: ap.ActivityDelete,
|
||||
Origin: account,
|
||||
Target: account,
|
||||
})
|
||||
return nil
|
||||
}
|
@@ -23,11 +23,92 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/validate"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// EmailChange processes an email address change request for the given user.
|
||||
func (p *Processor) EmailChange(
|
||||
ctx context.Context,
|
||||
user *gtsmodel.User,
|
||||
password string,
|
||||
newEmail string,
|
||||
) (*apimodel.User, gtserror.WithCode) {
|
||||
// Ensure provided password is correct.
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.EncryptedPassword), []byte(password)); err != nil {
|
||||
err := gtserror.Newf("%w", err)
|
||||
return nil, gtserror.NewErrorUnauthorized(err, "password was incorrect")
|
||||
}
|
||||
|
||||
// Ensure new email address is valid.
|
||||
if err := validate.Email(newEmail); err != nil {
|
||||
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
||||
}
|
||||
|
||||
// Ensure new email address is different
|
||||
// from current email address.
|
||||
if newEmail == user.Email {
|
||||
const help = "new email address cannot be the same as current email address"
|
||||
err := gtserror.New(help)
|
||||
return nil, gtserror.NewErrorBadRequest(err, help)
|
||||
}
|
||||
|
||||
if newEmail == user.UnconfirmedEmail {
|
||||
const help = "you already have an email change request pending for given email address"
|
||||
err := gtserror.New(help)
|
||||
return nil, gtserror.NewErrorBadRequest(err, help)
|
||||
}
|
||||
|
||||
// Ensure this address isn't already used by another account.
|
||||
emailAvailable, err := p.state.DB.IsEmailAvailable(ctx, newEmail)
|
||||
if err != nil {
|
||||
err := gtserror.Newf("db error checking email availability: %w", err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
if !emailAvailable {
|
||||
const help = "new email address is already in use on this instance"
|
||||
err := gtserror.New(help)
|
||||
return nil, gtserror.NewErrorConflict(err, help)
|
||||
}
|
||||
|
||||
// Set new email address on user.
|
||||
user.UnconfirmedEmail = newEmail
|
||||
if err := p.state.DB.UpdateUser(
|
||||
ctx, user,
|
||||
"unconfirmed_email",
|
||||
); err != nil {
|
||||
err := gtserror.Newf("db error updating user: %w", err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
// Ensure user populated (we need account).
|
||||
if err := p.state.DB.PopulateUser(ctx, user); err != nil {
|
||||
err := gtserror.Newf("db error populating user: %w", err)
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
// Add email sending job to the queue.
|
||||
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
|
||||
// Use ap.ObjectProfile here to
|
||||
// distinguish this message (user model)
|
||||
// from ap.ActorPerson (account model).
|
||||
APObjectType: ap.ObjectProfile,
|
||||
APActivityType: ap.ActivityUpdate,
|
||||
GTSModel: user,
|
||||
Origin: user.Account,
|
||||
Target: user.Account,
|
||||
})
|
||||
|
||||
return p.converter.UserToAPIUser(ctx, user), nil
|
||||
}
|
||||
|
||||
// EmailGetUserForConfirmToken retrieves the user (with account) from
|
||||
// the database for the given "confirm your email" token string.
|
||||
func (p *Processor) EmailGetUserForConfirmToken(ctx context.Context, token string) (*gtsmodel.User, gtserror.WithCode) {
|
||||
|
32
internal/processing/user/get.go
Normal file
32
internal/processing/user/get.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// 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 user
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
// Get returns the API model of the given user.
|
||||
// Should only be served if user == the user doing the request.
|
||||
func (p *Processor) Get(ctx context.Context, user *gtsmodel.User) (*apimodel.User, gtserror.WithCode) {
|
||||
return p.converter.UserToAPIUser(ctx, user), nil
|
||||
}
|
@@ -19,18 +19,28 @@ package user
|
||||
|
||||
import (
|
||||
"github.com/superseriousbusiness/gotosocial/internal/email"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||
)
|
||||
|
||||
type Processor struct {
|
||||
state *state.State
|
||||
converter *typeutils.Converter
|
||||
oauthServer oauth.Server
|
||||
emailSender email.Sender
|
||||
}
|
||||
|
||||
// New returns a new user processor
|
||||
func New(state *state.State, emailSender email.Sender) Processor {
|
||||
// New returns a new user processor.
|
||||
func New(
|
||||
state *state.State,
|
||||
converter *typeutils.Converter,
|
||||
oauthServer oauth.Server,
|
||||
emailSender email.Sender,
|
||||
) Processor {
|
||||
return Processor{
|
||||
state: state,
|
||||
converter: converter,
|
||||
emailSender: emailSender,
|
||||
}
|
||||
}
|
||||
|
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing/user"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
@@ -53,7 +54,7 @@ func (suite *UserStandardTestSuite) SetupTest() {
|
||||
suite.emailSender = testrig.NewEmailSender("../../../web/template/", suite.sentEmails)
|
||||
suite.testUsers = testrig.NewTestUsers()
|
||||
|
||||
suite.user = user.New(&suite.state, suite.emailSender)
|
||||
suite.user = user.New(&suite.state, typeutils.NewConverter(&suite.state), testrig.NewTestOauthServer(suite.db), suite.emailSender)
|
||||
|
||||
testrig.StandardDBSetup(suite.db, nil)
|
||||
}
|
||||
|
@@ -71,9 +71,9 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg *messages.Fro
|
||||
case ap.ActivityCreate:
|
||||
switch cMsg.APObjectType {
|
||||
|
||||
// CREATE PROFILE/ACCOUNT
|
||||
case ap.ObjectProfile, ap.ActorPerson:
|
||||
return p.clientAPI.CreateAccount(ctx, cMsg)
|
||||
// CREATE USER (ie., new user+account sign-up)
|
||||
case ap.ObjectProfile:
|
||||
return p.clientAPI.CreateUser(ctx, cMsg)
|
||||
|
||||
// CREATE NOTE/STATUS
|
||||
case ap.ObjectNote:
|
||||
@@ -111,13 +111,17 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg *messages.Fro
|
||||
case ap.ObjectNote:
|
||||
return p.clientAPI.UpdateStatus(ctx, cMsg)
|
||||
|
||||
// UPDATE PROFILE/ACCOUNT
|
||||
case ap.ObjectProfile, ap.ActorPerson:
|
||||
// UPDATE ACCOUNT (ie., bio, settings, etc)
|
||||
case ap.ActorPerson:
|
||||
return p.clientAPI.UpdateAccount(ctx, cMsg)
|
||||
|
||||
// UPDATE A FLAG/REPORT (mark as resolved/closed)
|
||||
case ap.ActivityFlag:
|
||||
return p.clientAPI.UpdateReport(ctx, cMsg)
|
||||
|
||||
// UPDATE USER (ie., email address)
|
||||
case ap.ObjectProfile:
|
||||
return p.clientAPI.UpdateUser(ctx, cMsg)
|
||||
}
|
||||
|
||||
// ACCEPT SOMETHING
|
||||
@@ -128,9 +132,9 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg *messages.Fro
|
||||
case ap.ActivityFollow:
|
||||
return p.clientAPI.AcceptFollow(ctx, cMsg)
|
||||
|
||||
// ACCEPT PROFILE/ACCOUNT (sign-up)
|
||||
case ap.ObjectProfile, ap.ActorPerson:
|
||||
return p.clientAPI.AcceptAccount(ctx, cMsg)
|
||||
// ACCEPT USER (ie., new user+account sign-up)
|
||||
case ap.ObjectProfile:
|
||||
return p.clientAPI.AcceptUser(ctx, cMsg)
|
||||
}
|
||||
|
||||
// REJECT SOMETHING
|
||||
@@ -141,9 +145,9 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg *messages.Fro
|
||||
case ap.ActivityFollow:
|
||||
return p.clientAPI.RejectFollowRequest(ctx, cMsg)
|
||||
|
||||
// REJECT PROFILE/ACCOUNT (sign-up)
|
||||
case ap.ObjectProfile, ap.ActorPerson:
|
||||
return p.clientAPI.RejectAccount(ctx, cMsg)
|
||||
// REJECT USER (ie., new user+account sign-up)
|
||||
case ap.ObjectProfile:
|
||||
return p.clientAPI.RejectUser(ctx, cMsg)
|
||||
}
|
||||
|
||||
// UNDO SOMETHING
|
||||
@@ -175,17 +179,17 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg *messages.Fro
|
||||
case ap.ObjectNote:
|
||||
return p.clientAPI.DeleteStatus(ctx, cMsg)
|
||||
|
||||
// DELETE PROFILE/ACCOUNT
|
||||
case ap.ObjectProfile, ap.ActorPerson:
|
||||
return p.clientAPI.DeleteAccount(ctx, cMsg)
|
||||
// DELETE REMOTE ACCOUNT or LOCAL USER+ACCOUNT
|
||||
case ap.ActorPerson, ap.ObjectProfile:
|
||||
return p.clientAPI.DeleteAccountOrUser(ctx, cMsg)
|
||||
}
|
||||
|
||||
// FLAG/REPORT SOMETHING
|
||||
case ap.ActivityFlag:
|
||||
switch cMsg.APObjectType { //nolint:gocritic
|
||||
|
||||
// FLAG/REPORT A PROFILE
|
||||
case ap.ObjectProfile:
|
||||
// FLAG/REPORT ACCOUNT
|
||||
case ap.ActorPerson:
|
||||
return p.clientAPI.ReportAccount(ctx, cMsg)
|
||||
}
|
||||
|
||||
@@ -193,8 +197,8 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg *messages.Fro
|
||||
case ap.ActivityMove:
|
||||
switch cMsg.APObjectType { //nolint:gocritic
|
||||
|
||||
// MOVE PROFILE/ACCOUNT
|
||||
case ap.ObjectProfile, ap.ActorPerson:
|
||||
// MOVE ACCOUNT
|
||||
case ap.ActorPerson:
|
||||
return p.clientAPI.MoveAccount(ctx, cMsg)
|
||||
}
|
||||
}
|
||||
@@ -202,7 +206,7 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg *messages.Fro
|
||||
return gtserror.Newf("unhandled: %s %s", cMsg.APActivityType, cMsg.APObjectType)
|
||||
}
|
||||
|
||||
func (p *clientAPI) CreateAccount(ctx context.Context, cMsg *messages.FromClientAPI) error {
|
||||
func (p *clientAPI) CreateUser(ctx context.Context, cMsg *messages.FromClientAPI) error {
|
||||
newUser, ok := cMsg.GTSModel.(*gtsmodel.User)
|
||||
if !ok {
|
||||
return gtserror.Newf("%T not parseable as *gtsmodel.User", cMsg.GTSModel)
|
||||
@@ -219,7 +223,7 @@ func (p *clientAPI) CreateAccount(ctx context.Context, cMsg *messages.FromClient
|
||||
}
|
||||
|
||||
// Send "please confirm your address" email to the new user.
|
||||
if err := p.surface.emailUserPleaseConfirm(ctx, newUser); err != nil {
|
||||
if err := p.surface.emailUserPleaseConfirm(ctx, newUser, true); err != nil {
|
||||
log.Errorf(ctx, "error emailing confirm: %v", err)
|
||||
}
|
||||
|
||||
@@ -479,6 +483,22 @@ func (p *clientAPI) UpdateReport(ctx context.Context, cMsg *messages.FromClientA
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *clientAPI) UpdateUser(ctx context.Context, cMsg *messages.FromClientAPI) error {
|
||||
user, ok := cMsg.GTSModel.(*gtsmodel.User)
|
||||
if !ok {
|
||||
return gtserror.Newf("cannot cast %T -> *gtsmodel.User", cMsg.GTSModel)
|
||||
}
|
||||
|
||||
// The only possible "UpdateUser" action is to update the
|
||||
// user's email address, so we can safely assume by this
|
||||
// point that a new unconfirmed email address has been set.
|
||||
if err := p.surface.emailUserPleaseConfirm(ctx, user, false); err != nil {
|
||||
log.Errorf(ctx, "error emailing report closed: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *clientAPI) AcceptFollow(ctx context.Context, cMsg *messages.FromClientAPI) error {
|
||||
follow, ok := cMsg.GTSModel.(*gtsmodel.Follow)
|
||||
if !ok {
|
||||
@@ -669,7 +689,7 @@ func (p *clientAPI) DeleteStatus(ctx context.Context, cMsg *messages.FromClientA
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *clientAPI) DeleteAccount(ctx context.Context, cMsg *messages.FromClientAPI) error {
|
||||
func (p *clientAPI) DeleteAccountOrUser(ctx context.Context, cMsg *messages.FromClientAPI) error {
|
||||
// The originID of the delete, one of:
|
||||
// - ID of a domain block, for which
|
||||
// this account delete is a side effect.
|
||||
@@ -768,7 +788,7 @@ func (p *clientAPI) MoveAccount(ctx context.Context, cMsg *messages.FromClientAP
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *clientAPI) AcceptAccount(ctx context.Context, cMsg *messages.FromClientAPI) error {
|
||||
func (p *clientAPI) AcceptUser(ctx context.Context, cMsg *messages.FromClientAPI) error {
|
||||
newUser, ok := cMsg.GTSModel.(*gtsmodel.User)
|
||||
if !ok {
|
||||
return gtserror.Newf("%T not parseable as *gtsmodel.User", cMsg.GTSModel)
|
||||
@@ -791,7 +811,7 @@ func (p *clientAPI) AcceptAccount(ctx context.Context, cMsg *messages.FromClient
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *clientAPI) RejectAccount(ctx context.Context, cMsg *messages.FromClientAPI) error {
|
||||
func (p *clientAPI) RejectUser(ctx context.Context, cMsg *messages.FromClientAPI) error {
|
||||
deniedUser, ok := cMsg.GTSModel.(*gtsmodel.DeniedUser)
|
||||
if !ok {
|
||||
return gtserror.Newf("%T not parseable as *gtsmodel.DeniedUser", cMsg.GTSModel)
|
||||
|
@@ -115,8 +115,8 @@ func (p *Processor) ProcessFromFediAPI(ctx context.Context, fMsg *messages.FromF
|
||||
case ap.ObjectNote:
|
||||
return p.fediAPI.UpdateStatus(ctx, fMsg)
|
||||
|
||||
// UPDATE PROFILE/ACCOUNT
|
||||
case ap.ObjectProfile:
|
||||
// UPDATE ACCOUNT
|
||||
case ap.ActorPerson:
|
||||
return p.fediAPI.UpdateAccount(ctx, fMsg)
|
||||
}
|
||||
|
||||
@@ -137,17 +137,17 @@ func (p *Processor) ProcessFromFediAPI(ctx context.Context, fMsg *messages.FromF
|
||||
case ap.ObjectNote:
|
||||
return p.fediAPI.DeleteStatus(ctx, fMsg)
|
||||
|
||||
// DELETE PROFILE/ACCOUNT
|
||||
case ap.ObjectProfile:
|
||||
// DELETE ACCOUNT
|
||||
case ap.ActorPerson:
|
||||
return p.fediAPI.DeleteAccount(ctx, fMsg)
|
||||
}
|
||||
|
||||
// MOVE SOMETHING
|
||||
case ap.ActivityMove:
|
||||
|
||||
// MOVE PROFILE/ACCOUNT
|
||||
// MOVE ACCOUNT
|
||||
// fromfediapi_move.go.
|
||||
if fMsg.APObjectType == ap.ObjectProfile {
|
||||
if fMsg.APObjectType == ap.ActorPerson {
|
||||
return p.fediAPI.MoveAccount(ctx, fMsg)
|
||||
}
|
||||
}
|
||||
|
@@ -337,7 +337,7 @@ func (suite *FromFediAPITestSuite) TestProcessAccountDelete() {
|
||||
|
||||
// now they are mufos!
|
||||
err = testStructs.Processor.Workers().ProcessFromFediAPI(ctx, &messages.FromFediAPI{
|
||||
APObjectType: ap.ObjectProfile,
|
||||
APObjectType: ap.ActorPerson,
|
||||
APActivityType: ap.ActivityDelete,
|
||||
GTSModel: deletedAccount,
|
||||
Receiving: receivingAccount,
|
||||
@@ -613,7 +613,7 @@ func (suite *FromFediAPITestSuite) TestMoveAccount() {
|
||||
|
||||
// Process the Move.
|
||||
err := testStructs.Processor.Workers().ProcessFromFediAPI(ctx, &messages.FromFediAPI{
|
||||
APObjectType: ap.ObjectProfile,
|
||||
APObjectType: ap.ActorPerson,
|
||||
APActivityType: ap.ActivityMove,
|
||||
GTSModel: >smodel.Move{
|
||||
OriginURI: requestingAcct.URI,
|
||||
|
@@ -74,7 +74,10 @@ func (s *Surface) emailUserReportClosed(ctx context.Context, report *gtsmodel.Re
|
||||
|
||||
// emailUserPleaseConfirm emails the given user
|
||||
// to ask them to confirm their email address.
|
||||
func (s *Surface) emailUserPleaseConfirm(ctx context.Context, user *gtsmodel.User) error {
|
||||
//
|
||||
// If newSignup is true, template will be geared
|
||||
// towards someone who just created an account.
|
||||
func (s *Surface) emailUserPleaseConfirm(ctx context.Context, user *gtsmodel.User, newSignup bool) error {
|
||||
if user.UnconfirmedEmail == "" ||
|
||||
user.UnconfirmedEmail == user.Email {
|
||||
// User has already confirmed this
|
||||
@@ -104,6 +107,7 @@ func (s *Surface) emailUserPleaseConfirm(ctx context.Context, user *gtsmodel.Use
|
||||
InstanceURL: instance.URI,
|
||||
InstanceName: instance.Title,
|
||||
ConfirmLink: confirmLink,
|
||||
NewSignup: newSignup,
|
||||
},
|
||||
); err != nil {
|
||||
return err
|
||||
|
Reference in New Issue
Block a user