mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-22 11:46:40 +00: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
|
@ -2713,6 +2713,77 @@ definitions:
|
||||||
type: object
|
type: object
|
||||||
x-go-name: Theme
|
x-go-name: Theme
|
||||||
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
|
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
|
||||||
|
user:
|
||||||
|
properties:
|
||||||
|
admin:
|
||||||
|
description: User is an admin.
|
||||||
|
example: false
|
||||||
|
type: boolean
|
||||||
|
x-go-name: Admin
|
||||||
|
approved:
|
||||||
|
description: User was approved by an admin.
|
||||||
|
example: true
|
||||||
|
type: boolean
|
||||||
|
x-go-name: Approved
|
||||||
|
confirmation_sent_at:
|
||||||
|
description: Time when the last "please confirm your email address" email was sent, if at all. (ISO 8601 Datetime)
|
||||||
|
example: "2021-07-30T09:20:25+00:00"
|
||||||
|
type: string
|
||||||
|
x-go-name: ConfirmationSentAt
|
||||||
|
confirmed_at:
|
||||||
|
description: Time at which the email given in the `email` field was confirmed, if at all. (ISO 8601 Datetime)
|
||||||
|
example: "2021-07-30T09:20:25+00:00"
|
||||||
|
type: string
|
||||||
|
x-go-name: ConfirmedAt
|
||||||
|
created_at:
|
||||||
|
description: Time this user was created. (ISO 8601 Datetime)
|
||||||
|
example: "2021-07-30T09:20:25+00:00"
|
||||||
|
type: string
|
||||||
|
x-go-name: CreatedAt
|
||||||
|
disabled:
|
||||||
|
description: User's account is disabled.
|
||||||
|
example: false
|
||||||
|
type: boolean
|
||||||
|
x-go-name: Disabled
|
||||||
|
email:
|
||||||
|
description: Confirmed email address of this user, if set.
|
||||||
|
example: someone@example.org
|
||||||
|
type: string
|
||||||
|
x-go-name: Email
|
||||||
|
id:
|
||||||
|
description: Database ID of this user.
|
||||||
|
example: 01FBVD42CQ3ZEEVMW180SBX03B
|
||||||
|
type: string
|
||||||
|
x-go-name: ID
|
||||||
|
last_emailed_at:
|
||||||
|
description: Time at which this user was last emailed, if at all. (ISO 8601 Datetime)
|
||||||
|
example: "2021-07-30T09:20:25+00:00"
|
||||||
|
type: string
|
||||||
|
x-go-name: LastEmailedAt
|
||||||
|
moderator:
|
||||||
|
description: User is a moderator.
|
||||||
|
example: false
|
||||||
|
type: boolean
|
||||||
|
x-go-name: Moderator
|
||||||
|
reason:
|
||||||
|
description: Reason for sign-up, if provided.
|
||||||
|
example: Please! Pretty please!
|
||||||
|
type: string
|
||||||
|
x-go-name: Reason
|
||||||
|
reset_password_sent_at:
|
||||||
|
description: Time when the last "please reset your password" email was sent, if at all. (ISO 8601 Datetime)
|
||||||
|
example: "2021-07-30T09:20:25+00:00"
|
||||||
|
type: string
|
||||||
|
x-go-name: ResetPasswordSentAt
|
||||||
|
unconfirmed_email:
|
||||||
|
description: Unconfirmed email address of this user, if set.
|
||||||
|
example: someone.else@somewhere.else.example.org
|
||||||
|
type: string
|
||||||
|
x-go-name: UnconfirmedEmail
|
||||||
|
title: User models fields relevant to one user.
|
||||||
|
type: object
|
||||||
|
x-go-name: User
|
||||||
|
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
|
||||||
wellKnownResponse:
|
wellKnownResponse:
|
||||||
description: See https://webfinger.net/
|
description: See https://webfinger.net/
|
||||||
properties:
|
properties:
|
||||||
|
@ -8636,6 +8707,77 @@ paths:
|
||||||
summary: See public statuses that use the given hashtag (case insensitive).
|
summary: See public statuses that use the given hashtag (case insensitive).
|
||||||
tags:
|
tags:
|
||||||
- timelines
|
- timelines
|
||||||
|
/api/v1/user:
|
||||||
|
get:
|
||||||
|
operationId: getUser
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: The requested user.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/user'
|
||||||
|
"400":
|
||||||
|
description: bad request
|
||||||
|
"401":
|
||||||
|
description: unauthorized
|
||||||
|
"403":
|
||||||
|
description: forbidden
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal error
|
||||||
|
security:
|
||||||
|
- OAuth2 Bearer:
|
||||||
|
- read:user
|
||||||
|
summary: Get your own user model.
|
||||||
|
tags:
|
||||||
|
- user
|
||||||
|
/api/v1/user/email_change:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
- application/xml
|
||||||
|
- application/x-www-form-urlencoded
|
||||||
|
operationId: userEmailChange
|
||||||
|
parameters:
|
||||||
|
- description: User's current password, for verification.
|
||||||
|
in: formData
|
||||||
|
name: password
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
x-go-name: Password
|
||||||
|
- description: Desired new email address.
|
||||||
|
in: formData
|
||||||
|
name: new_email
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
x-go-name: NewEmail
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"202":
|
||||||
|
description: 'Accepted: email change is processing; check your inbox to confirm new address.'
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/user'
|
||||||
|
"400":
|
||||||
|
description: bad request
|
||||||
|
"401":
|
||||||
|
description: unauthorized
|
||||||
|
"403":
|
||||||
|
description: forbidden
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"409":
|
||||||
|
description: 'Conflict: desired email address already in use'
|
||||||
|
"500":
|
||||||
|
description: internal error
|
||||||
|
security:
|
||||||
|
- OAuth2 Bearer:
|
||||||
|
- write:user
|
||||||
|
summary: Request changing the email address of authenticated user.
|
||||||
|
tags:
|
||||||
|
- user
|
||||||
/api/v1/user/password_change:
|
/api/v1/user/password_change:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 104 KiB |
BIN
docs/assets/user-settings-settings.png
Normal file
BIN
docs/assets/user-settings-settings.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 108 KiB |
|
@ -133,11 +133,13 @@ See the [Custom CSS](./custom_css.md) page for some tips on writing custom CSS f
|
||||||
!!! tip
|
!!! tip
|
||||||
Any custom CSS you add in this box will be applied *after* your selected theme, so you can pick a preset theme that you like and then make your own tweaks!
|
Any custom CSS you add in this box will be applied *after* your selected theme, so you can pick a preset theme that you like and then make your own tweaks!
|
||||||
|
|
||||||
## Post Settings
|
## Settings
|
||||||
|
|
||||||
![Screenshot of the user settings section, providing drop-down menu's to select default post settings, and form fields to change your password](../assets/user-settings-post-settings.png)
|
![Screenshot of the settings section](../assets/user-settings-settings.png)
|
||||||
|
|
||||||
In the 'Settings' section, you can set various defaults for new posts.
|
In the 'Settings' section, you can set various defaults for new posts, and change your password / email address.
|
||||||
|
|
||||||
|
### Post Settings
|
||||||
|
|
||||||
The default post language setting allows you to indicate to other fediverse users which language your posts are usually written in. This is helpful for fediverse users who speak (for example) Korean, and would prefer to filter out posts written in other languages.
|
The default post language setting allows you to indicate to other fediverse users which language your posts are usually written in. This is helpful for fediverse users who speak (for example) Korean, and would prefer to filter out posts written in other languages.
|
||||||
|
|
||||||
|
@ -151,12 +153,18 @@ The markdown setting indicates that your posts should be parsed as Markdown, whi
|
||||||
|
|
||||||
When you are finished updating your post settings, remember to click the `Save post settings` button at the bottom of the section to save your changes.
|
When you are finished updating your post settings, remember to click the `Save post settings` button at the bottom of the section to save your changes.
|
||||||
|
|
||||||
## Password Change
|
### Password Change
|
||||||
|
|
||||||
You can use the Password Change section of the User Settings Panel to set a new password for your account.
|
You can use the Password Change section of the panel to set a new password for your account. For security reasons, you must provide your current password to validate the change.
|
||||||
|
|
||||||
For more information on the way GoToSocial manages passwords, please see the [Password management document](./password_management.md).
|
For more information on the way GoToSocial manages passwords, please see the [Password management document](./password_management.md).
|
||||||
|
|
||||||
|
### Email Change
|
||||||
|
|
||||||
|
You can use the Email Change section of the panel to change the email address for your account. For security reasons, you must provide your current password to validate the change.
|
||||||
|
|
||||||
|
Once a new email address has been entered, and you have clicked "Change email address", you must open the inbox of the new email address and confirm your address via the link provided. Once you've done that, your email address change will be confirmed, and you should use the new email address to log in.
|
||||||
|
|
||||||
## Migration
|
## Migration
|
||||||
|
|
||||||
In the migration section you can manage settings related to aliasing and/or migrating your account to another account.
|
In the migration section you can manage settings related to aliasing and/or migrating your account to another account.
|
||||||
|
|
|
@ -97,7 +97,7 @@ func (suite *UserGetTestSuite) TestGetUserPublicKeyDeleted() {
|
||||||
userModule := users.New(suite.processor)
|
userModule := users.New(suite.processor)
|
||||||
targetAccount := suite.testAccounts["local_account_1"]
|
targetAccount := suite.testAccounts["local_account_1"]
|
||||||
|
|
||||||
suite.processor.Account().DeleteSelf(context.Background(), suite.testAccounts["local_account_1"])
|
suite.processor.User().DeleteSelf(context.Background(), suite.testAccounts["local_account_1"])
|
||||||
|
|
||||||
// wait for the account delete to be processed
|
// wait for the account delete to be processed
|
||||||
if !testrig.WaitFor(func() bool {
|
if !testrig.WaitFor(func() bool {
|
||||||
|
|
|
@ -105,9 +105,9 @@ func (m *Module) AccountCreatePOSTHandler(c *gin.Context) {
|
||||||
}
|
}
|
||||||
form.IP = signUpIP
|
form.IP = signUpIP
|
||||||
|
|
||||||
// Create the new account + user.
|
// Create the new user+account.
|
||||||
ctx := c.Request.Context()
|
ctx := c.Request.Context()
|
||||||
user, errWithCode := m.processor.Account().Create(
|
user, errWithCode := m.processor.User().Create(
|
||||||
ctx,
|
ctx,
|
||||||
authed.Application,
|
authed.Application,
|
||||||
form,
|
form,
|
||||||
|
@ -118,7 +118,7 @@ func (m *Module) AccountCreatePOSTHandler(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a token for the new user.
|
// Get a token for the new user.
|
||||||
ti, errWithCode := m.processor.Account().TokenForNewUser(
|
ti, errWithCode := m.processor.User().TokenForNewUser(
|
||||||
ctx,
|
ctx,
|
||||||
authed.Token,
|
authed.Token,
|
||||||
authed.Application,
|
authed.Application,
|
||||||
|
|
|
@ -91,7 +91,7 @@ func (m *Module) AccountDeletePOSTHandler(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if errWithCode := m.processor.Account().DeleteSelf(c.Request.Context(), authed.Account); errWithCode != nil {
|
if errWithCode := m.processor.User().DeleteSelf(c.Request.Context(), authed.Account); errWithCode != nil {
|
||||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,7 +91,7 @@ func (m *Module) AccountApprovePOSTHandler(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
account, errWithCode := m.processor.Admin().AccountApprove(
|
account, errWithCode := m.processor.Admin().SignupApprove(
|
||||||
c.Request.Context(),
|
c.Request.Context(),
|
||||||
authed.Account,
|
authed.Account,
|
||||||
targetAcctID,
|
targetAcctID,
|
||||||
|
|
|
@ -119,7 +119,7 @@ func (m *Module) AccountRejectPOSTHandler(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
account, errWithCode := m.processor.Admin().AccountReject(
|
account, errWithCode := m.processor.Admin().SignupReject(
|
||||||
c.Request.Context(),
|
c.Request.Context(),
|
||||||
authed.Account,
|
authed.Account,
|
||||||
targetAcctID,
|
targetAcctID,
|
||||||
|
|
104
internal/api/client/user/emailchange.go
Normal file
104
internal/api/client/user/emailchange.go
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
// 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 (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EmailChangePOSTHandler swagger:operation POST /api/v1/user/email_change userEmailChange
|
||||||
|
//
|
||||||
|
// Request changing the email address of authenticated user.
|
||||||
|
//
|
||||||
|
// ---
|
||||||
|
// tags:
|
||||||
|
// - user
|
||||||
|
//
|
||||||
|
// consumes:
|
||||||
|
// - application/json
|
||||||
|
// - application/xml
|
||||||
|
// - application/x-www-form-urlencoded
|
||||||
|
//
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
//
|
||||||
|
// security:
|
||||||
|
// - OAuth2 Bearer:
|
||||||
|
// - write:user
|
||||||
|
//
|
||||||
|
// responses:
|
||||||
|
// '202':
|
||||||
|
// description: "Accepted: email change is processing; check your inbox to confirm new address."
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/user"
|
||||||
|
// '400':
|
||||||
|
// description: bad request
|
||||||
|
// '401':
|
||||||
|
// description: unauthorized
|
||||||
|
// '403':
|
||||||
|
// description: forbidden
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
|
// '409':
|
||||||
|
// description: "Conflict: desired email address already in use"
|
||||||
|
// '500':
|
||||||
|
// description: internal error
|
||||||
|
func (m *Module) EmailChangePOSTHandler(c *gin.Context) {
|
||||||
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
|
if err != nil {
|
||||||
|
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||||
|
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
form := &apimodel.EmailChangeRequest{}
|
||||||
|
if err := c.ShouldBind(form); err != nil {
|
||||||
|
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if form.Password == "" {
|
||||||
|
err := errors.New("email change request missing field password")
|
||||||
|
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, errWithCode := m.processor.User().EmailChange(
|
||||||
|
c.Request.Context(),
|
||||||
|
authed.User,
|
||||||
|
form.Password,
|
||||||
|
form.NewEmail,
|
||||||
|
)
|
||||||
|
if errWithCode != nil {
|
||||||
|
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiutil.JSON(c, http.StatusAccepted, user)
|
||||||
|
}
|
142
internal/api/client/user/emailchange_test.go
Normal file
142
internal/api/client/user/emailchange_test.go
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
// 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_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/api/client/user"
|
||||||
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EmailChangeTestSuite struct {
|
||||||
|
UserStandardTestSuite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *EmailChangeTestSuite) TestEmailChangePOST() {
|
||||||
|
// Get a new processor for this test, as
|
||||||
|
// we're expecting an email, and we don't
|
||||||
|
// want the other tests interfering if
|
||||||
|
// we're running them at the same time.
|
||||||
|
state := new(state.State)
|
||||||
|
state.DB = testrig.NewTestDB(&suite.state)
|
||||||
|
storage := testrig.NewInMemoryStorage()
|
||||||
|
sentEmails := make(map[string]string)
|
||||||
|
emailSender := testrig.NewEmailSender("../../../../web/template/", sentEmails)
|
||||||
|
processor := testrig.NewTestProcessor(state, suite.federator, emailSender, suite.mediaManager)
|
||||||
|
testrig.StartWorkers(state, processor.Workers())
|
||||||
|
userModule := user.New(processor)
|
||||||
|
testrig.StandardDBSetup(state.DB, suite.testAccounts)
|
||||||
|
testrig.StandardStorageSetup(storage, "../../../../testrig/media")
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
testrig.StandardDBTeardown(state.DB)
|
||||||
|
testrig.StandardStorageTeardown(storage)
|
||||||
|
testrig.StopWorkers(state)
|
||||||
|
}()
|
||||||
|
|
||||||
|
response, code := suite.POST(user.EmailChangePath, map[string][]string{
|
||||||
|
"password": {"password"},
|
||||||
|
"new_email": {"someone@example.org"},
|
||||||
|
}, userModule.EmailChangePOSTHandler)
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
// Check response
|
||||||
|
suite.EqualValues(http.StatusAccepted, code)
|
||||||
|
b, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
apiUser := new(apimodel.User)
|
||||||
|
if err := json.Unmarshal(b, apiUser); err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unconfirmed email should be set now.
|
||||||
|
suite.Equal("someone@example.org", apiUser.UnconfirmedEmail)
|
||||||
|
|
||||||
|
// Ensure unconfirmed address gets an email.
|
||||||
|
if !testrig.WaitFor(func() bool {
|
||||||
|
_, ok := sentEmails["someone@example.org"]
|
||||||
|
return ok
|
||||||
|
}) {
|
||||||
|
suite.FailNow("no email received")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *EmailChangeTestSuite) TestEmailChangePOSTAddressInUse() {
|
||||||
|
response, code := suite.POST(user.EmailChangePath, map[string][]string{
|
||||||
|
"password": {"password"},
|
||||||
|
"new_email": {"admin@example.org"},
|
||||||
|
}, suite.userModule.EmailChangePOSTHandler)
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
// Check response
|
||||||
|
suite.EqualValues(http.StatusConflict, code)
|
||||||
|
b, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.Equal(`{"error":"Conflict: new email address is already in use on this instance"}`, string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *EmailChangeTestSuite) TestEmailChangePOSTSameEmail() {
|
||||||
|
response, code := suite.POST(user.EmailChangePath, map[string][]string{
|
||||||
|
"password": {"password"},
|
||||||
|
"new_email": {"zork@example.org"},
|
||||||
|
}, suite.userModule.EmailChangePOSTHandler)
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
// Check response
|
||||||
|
suite.EqualValues(http.StatusBadRequest, code)
|
||||||
|
b, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.Equal(`{"error":"Bad Request: new email address cannot be the same as current email address"}`, string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *EmailChangeTestSuite) TestEmailChangePOSTBadPassword() {
|
||||||
|
response, code := suite.POST(user.EmailChangePath, map[string][]string{
|
||||||
|
"password": {"notmypassword"},
|
||||||
|
"new_email": {"someone@example.org"},
|
||||||
|
}, suite.userModule.EmailChangePOSTHandler)
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
// Check response
|
||||||
|
suite.EqualValues(http.StatusUnauthorized, code)
|
||||||
|
b, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.Equal(`{"error":"Unauthorized: password was incorrect"}`, string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEmailChangeTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, &EmailChangeTestSuite{})
|
||||||
|
}
|
|
@ -19,18 +19,13 @@
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
|
||||||
"net/url"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/user"
|
"github.com/superseriousbusiness/gotosocial/internal/api/client/user"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -39,29 +34,20 @@ type PasswordChangeTestSuite struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *PasswordChangeTestSuite) TestPasswordChangePOST() {
|
func (suite *PasswordChangeTestSuite) TestPasswordChangePOST() {
|
||||||
t := suite.testTokens["local_account_1"]
|
response, code := suite.POST(user.PasswordChangePath, map[string][]string{
|
||||||
oauthToken := oauth.DBTokenToToken(t)
|
|
||||||
|
|
||||||
recorder := httptest.NewRecorder()
|
|
||||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
|
||||||
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
|
||||||
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
|
||||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
|
||||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
|
||||||
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", user.PasswordChangePath), nil)
|
|
||||||
ctx.Request.Header.Set("accept", "application/json")
|
|
||||||
ctx.Request.Form = url.Values{
|
|
||||||
"old_password": {"password"},
|
"old_password": {"password"},
|
||||||
"new_password": {"peepeepoopoopassword"},
|
"new_password": {"peepeepoopoopassword"},
|
||||||
}
|
}, suite.userModule.PasswordChangePOSTHandler)
|
||||||
suite.userModule.PasswordChangePOSTHandler(ctx)
|
defer response.Body.Close()
|
||||||
|
|
||||||
// check response
|
// Check response
|
||||||
suite.EqualValues(http.StatusOK, recorder.Code)
|
suite.EqualValues(http.StatusOK, code)
|
||||||
|
|
||||||
dbUser := >smodel.User{}
|
dbUser := >smodel.User{}
|
||||||
err := suite.db.GetByID(context.Background(), suite.testUsers["local_account_1"].ID, dbUser)
|
err := suite.db.GetByID(context.Background(), suite.testUsers["local_account_1"].ID, dbUser)
|
||||||
suite.NoError(err)
|
if err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
// new password should pass
|
// new password should pass
|
||||||
err = bcrypt.CompareHashAndPassword([]byte(dbUser.EncryptedPassword), []byte("peepeepoopoopassword"))
|
err = bcrypt.CompareHashAndPassword([]byte(dbUser.EncryptedPassword), []byte("peepeepoopoopassword"))
|
||||||
|
@ -73,85 +59,49 @@ func (suite *PasswordChangeTestSuite) TestPasswordChangePOST() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *PasswordChangeTestSuite) TestPasswordMissingOldPassword() {
|
func (suite *PasswordChangeTestSuite) TestPasswordMissingOldPassword() {
|
||||||
t := suite.testTokens["local_account_1"]
|
response, code := suite.POST(user.PasswordChangePath, map[string][]string{
|
||||||
oauthToken := oauth.DBTokenToToken(t)
|
|
||||||
|
|
||||||
recorder := httptest.NewRecorder()
|
|
||||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
|
||||||
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
|
||||||
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
|
||||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
|
||||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
|
||||||
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", user.PasswordChangePath), nil)
|
|
||||||
ctx.Request.Header.Set("accept", "application/json")
|
|
||||||
ctx.Request.Form = url.Values{
|
|
||||||
"new_password": {"peepeepoopoopassword"},
|
"new_password": {"peepeepoopoopassword"},
|
||||||
|
}, suite.userModule.PasswordChangePOSTHandler)
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
// Check response
|
||||||
|
suite.EqualValues(http.StatusBadRequest, code)
|
||||||
|
b, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
}
|
}
|
||||||
suite.userModule.PasswordChangePOSTHandler(ctx)
|
|
||||||
|
|
||||||
// check response
|
|
||||||
suite.EqualValues(http.StatusBadRequest, recorder.Code)
|
|
||||||
|
|
||||||
result := recorder.Result()
|
|
||||||
defer result.Body.Close()
|
|
||||||
b, err := ioutil.ReadAll(result.Body)
|
|
||||||
suite.NoError(err)
|
|
||||||
suite.Equal(`{"error":"Bad Request: password change request missing field old_password"}`, string(b))
|
suite.Equal(`{"error":"Bad Request: password change request missing field old_password"}`, string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *PasswordChangeTestSuite) TestPasswordIncorrectOldPassword() {
|
func (suite *PasswordChangeTestSuite) TestPasswordIncorrectOldPassword() {
|
||||||
t := suite.testTokens["local_account_1"]
|
response, code := suite.POST(user.PasswordChangePath, map[string][]string{
|
||||||
oauthToken := oauth.DBTokenToToken(t)
|
|
||||||
|
|
||||||
recorder := httptest.NewRecorder()
|
|
||||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
|
||||||
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
|
||||||
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
|
||||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
|
||||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
|
||||||
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", user.PasswordChangePath), nil)
|
|
||||||
ctx.Request.Header.Set("accept", "application/json")
|
|
||||||
ctx.Request.Form = url.Values{
|
|
||||||
"old_password": {"notright"},
|
"old_password": {"notright"},
|
||||||
"new_password": {"peepeepoopoopassword"},
|
"new_password": {"peepeepoopoopassword"},
|
||||||
|
}, suite.userModule.PasswordChangePOSTHandler)
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
// Check response
|
||||||
|
suite.EqualValues(http.StatusUnauthorized, code)
|
||||||
|
b, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
}
|
}
|
||||||
suite.userModule.PasswordChangePOSTHandler(ctx)
|
|
||||||
|
|
||||||
// check response
|
|
||||||
suite.EqualValues(http.StatusUnauthorized, recorder.Code)
|
|
||||||
|
|
||||||
result := recorder.Result()
|
|
||||||
defer result.Body.Close()
|
|
||||||
b, err := ioutil.ReadAll(result.Body)
|
|
||||||
suite.NoError(err)
|
|
||||||
suite.Equal(`{"error":"Unauthorized: old password was incorrect"}`, string(b))
|
suite.Equal(`{"error":"Unauthorized: old password was incorrect"}`, string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *PasswordChangeTestSuite) TestPasswordWeakNewPassword() {
|
func (suite *PasswordChangeTestSuite) TestPasswordWeakNewPassword() {
|
||||||
t := suite.testTokens["local_account_1"]
|
response, code := suite.POST(user.PasswordChangePath, map[string][]string{
|
||||||
oauthToken := oauth.DBTokenToToken(t)
|
|
||||||
|
|
||||||
recorder := httptest.NewRecorder()
|
|
||||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
|
||||||
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
|
||||||
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
|
||||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
|
||||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
|
||||||
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", user.PasswordChangePath), nil)
|
|
||||||
ctx.Request.Header.Set("accept", "application/json")
|
|
||||||
ctx.Request.Form = url.Values{
|
|
||||||
"old_password": {"password"},
|
"old_password": {"password"},
|
||||||
"new_password": {"peepeepoopoo"},
|
"new_password": {"peepeepoopoo"},
|
||||||
|
}, suite.userModule.PasswordChangePOSTHandler)
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
// Check response
|
||||||
|
suite.EqualValues(http.StatusBadRequest, code)
|
||||||
|
b, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
}
|
}
|
||||||
suite.userModule.PasswordChangePOSTHandler(ctx)
|
|
||||||
|
|
||||||
// check response
|
|
||||||
suite.EqualValues(http.StatusBadRequest, recorder.Code)
|
|
||||||
|
|
||||||
result := recorder.Result()
|
|
||||||
defer result.Body.Close()
|
|
||||||
b, err := ioutil.ReadAll(result.Body)
|
|
||||||
suite.NoError(err)
|
|
||||||
suite.Equal(`{"error":"Bad Request: password is only 94% strength, try including more special characters, using uppercase letters, using numbers or using a longer password"}`, string(b))
|
suite.Equal(`{"error":"Bad Request: password is only 94% strength, try including more special characters, using uppercase letters, using numbers or using a longer password"}`, string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,8 @@
|
||||||
BasePath = "/v1/user"
|
BasePath = "/v1/user"
|
||||||
// PasswordChangePath is the path for POSTing a password change request.
|
// PasswordChangePath is the path for POSTing a password change request.
|
||||||
PasswordChangePath = BasePath + "/password_change"
|
PasswordChangePath = BasePath + "/password_change"
|
||||||
|
// EmailChangePath is the path for POSTing an email address change request.
|
||||||
|
EmailChangePath = BasePath + "/email_change"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Module struct {
|
type Module struct {
|
||||||
|
@ -42,5 +44,7 @@ func New(processor *processing.Processor) *Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) {
|
func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) {
|
||||||
|
attachHandler(http.MethodGet, BasePath, m.UserGETHandler)
|
||||||
attachHandler(http.MethodPost, PasswordChangePath, m.PasswordChangePOSTHandler)
|
attachHandler(http.MethodPost, PasswordChangePath, m.PasswordChangePOSTHandler)
|
||||||
|
attachHandler(http.MethodPost, EmailChangePath, m.EmailChangePOSTHandler)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,14 +18,19 @@
|
||||||
package user_test
|
package user_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/user"
|
"github.com/superseriousbusiness/gotosocial/internal/api/client/user"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/email"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/storage"
|
"github.com/superseriousbusiness/gotosocial/internal/storage"
|
||||||
|
@ -39,7 +44,6 @@ type UserStandardTestSuite struct {
|
||||||
tc *typeutils.Converter
|
tc *typeutils.Converter
|
||||||
mediaManager *media.Manager
|
mediaManager *media.Manager
|
||||||
federator *federation.Federator
|
federator *federation.Federator
|
||||||
emailSender email.Sender
|
|
||||||
processor *processing.Processor
|
processor *processing.Processor
|
||||||
storage *storage.Driver
|
storage *storage.Driver
|
||||||
state state.State
|
state state.State
|
||||||
|
@ -50,8 +54,6 @@ type UserStandardTestSuite struct {
|
||||||
testUsers map[string]*gtsmodel.User
|
testUsers map[string]*gtsmodel.User
|
||||||
testAccounts map[string]*gtsmodel.Account
|
testAccounts map[string]*gtsmodel.Account
|
||||||
|
|
||||||
sentEmails map[string]string
|
|
||||||
|
|
||||||
userModule *user.Module
|
userModule *user.Module
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,9 +85,7 @@ func (suite *UserStandardTestSuite) SetupTest() {
|
||||||
|
|
||||||
suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
|
suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
|
||||||
suite.federator = testrig.NewTestFederator(&suite.state, testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../../testrig/media")), suite.mediaManager)
|
suite.federator = testrig.NewTestFederator(&suite.state, testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../../testrig/media")), suite.mediaManager)
|
||||||
suite.sentEmails = make(map[string]string)
|
suite.processor = testrig.NewTestProcessor(&suite.state, suite.federator, testrig.NewEmailSender("../../../../web/template/", nil), suite.mediaManager)
|
||||||
suite.emailSender = testrig.NewEmailSender("../../../../web/template/", suite.sentEmails)
|
|
||||||
suite.processor = testrig.NewTestProcessor(&suite.state, suite.federator, suite.emailSender, suite.mediaManager)
|
|
||||||
suite.userModule = user.New(suite.processor)
|
suite.userModule = user.New(suite.processor)
|
||||||
testrig.StandardDBSetup(suite.db, suite.testAccounts)
|
testrig.StandardDBSetup(suite.db, suite.testAccounts)
|
||||||
testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
|
testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
|
||||||
|
@ -96,3 +96,32 @@ func (suite *UserStandardTestSuite) TearDownTest() {
|
||||||
testrig.StandardStorageTeardown(suite.storage)
|
testrig.StandardStorageTeardown(suite.storage)
|
||||||
testrig.StopWorkers(&suite.state)
|
testrig.StopWorkers(&suite.state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *UserStandardTestSuite) POST(path string, formValues map[string][]string, handler gin.HandlerFunc) (*http.Response, int) {
|
||||||
|
var (
|
||||||
|
oauthToken = oauth.DBTokenToToken(suite.testTokens["local_account_1"])
|
||||||
|
app = suite.testApplications["application_1"]
|
||||||
|
user = suite.testUsers["local_account_1"]
|
||||||
|
account = suite.testAccounts["local_account_1"]
|
||||||
|
target = "http://localhost:8080" + path
|
||||||
|
)
|
||||||
|
|
||||||
|
// Prepare context.
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||||
|
ctx.Set(oauth.SessionAuthorizedApplication, app)
|
||||||
|
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
||||||
|
ctx.Set(oauth.SessionAuthorizedUser, user)
|
||||||
|
ctx.Set(oauth.SessionAuthorizedAccount, account)
|
||||||
|
|
||||||
|
// Prepare request.
|
||||||
|
ctx.Request = httptest.NewRequest(http.MethodPost, target, nil)
|
||||||
|
ctx.Request.Header.Set("accept", "application/json")
|
||||||
|
ctx.Request.Form = url.Values(formValues)
|
||||||
|
|
||||||
|
// Call the handler.
|
||||||
|
handler(ctx)
|
||||||
|
|
||||||
|
// Return response.
|
||||||
|
return recorder.Result(), recorder.Code
|
||||||
|
}
|
||||||
|
|
78
internal/api/client/user/userget.go
Normal file
78
internal/api/client/user/userget.go
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
// 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 (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserGETHandler swagger:operation GET /api/v1/user getUser
|
||||||
|
//
|
||||||
|
// Get your own user model.
|
||||||
|
//
|
||||||
|
// ---
|
||||||
|
// tags:
|
||||||
|
// - user
|
||||||
|
//
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
//
|
||||||
|
// security:
|
||||||
|
// - OAuth2 Bearer:
|
||||||
|
// - read:user
|
||||||
|
//
|
||||||
|
// responses:
|
||||||
|
// '200':
|
||||||
|
// description: The requested user.
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/user"
|
||||||
|
// '400':
|
||||||
|
// description: bad request
|
||||||
|
// '401':
|
||||||
|
// description: unauthorized
|
||||||
|
// '403':
|
||||||
|
// description: forbidden
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
|
// '500':
|
||||||
|
// description: internal error
|
||||||
|
func (m *Module) UserGETHandler(c *gin.Context) {
|
||||||
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
|
if err != nil {
|
||||||
|
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||||
|
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, errWithCode := m.processor.User().Get(c.Request.Context(), authed.User)
|
||||||
|
if errWithCode != nil {
|
||||||
|
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiutil.JSON(c, http.StatusOK, user)
|
||||||
|
}
|
|
@ -17,6 +17,51 @@
|
||||||
|
|
||||||
package model
|
package model
|
||||||
|
|
||||||
|
// User models fields relevant to one user.
|
||||||
|
//
|
||||||
|
// swagger:model user
|
||||||
|
type User struct {
|
||||||
|
// Database ID of this user.
|
||||||
|
// example: 01FBVD42CQ3ZEEVMW180SBX03B
|
||||||
|
ID string `json:"id"`
|
||||||
|
// Time this user was created. (ISO 8601 Datetime)
|
||||||
|
// example: 2021-07-30T09:20:25+00:00
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
// Confirmed email address of this user, if set.
|
||||||
|
// example: someone@example.org
|
||||||
|
Email string `json:"email,omitempty"`
|
||||||
|
// Unconfirmed email address of this user, if set.
|
||||||
|
// example: someone.else@somewhere.else.example.org
|
||||||
|
UnconfirmedEmail string `json:"unconfirmed_email,omitempty"`
|
||||||
|
// Reason for sign-up, if provided.
|
||||||
|
// example: Please! Pretty please!
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
// Time at which this user was last emailed, if at all. (ISO 8601 Datetime)
|
||||||
|
// example: 2021-07-30T09:20:25+00:00
|
||||||
|
LastEmailedAt string `json:"last_emailed_at,omitempty"`
|
||||||
|
// Time at which the email given in the `email` field was confirmed, if at all. (ISO 8601 Datetime)
|
||||||
|
// example: 2021-07-30T09:20:25+00:00
|
||||||
|
ConfirmedAt string `json:"confirmed_at,omitempty"`
|
||||||
|
// Time when the last "please confirm your email address" email was sent, if at all. (ISO 8601 Datetime)
|
||||||
|
// example: 2021-07-30T09:20:25+00:00
|
||||||
|
ConfirmationSentAt string `json:"confirmation_sent_at,omitempty"`
|
||||||
|
// User is a moderator.
|
||||||
|
// example: false
|
||||||
|
Moderator bool `json:"moderator"`
|
||||||
|
// User is an admin.
|
||||||
|
// example: false
|
||||||
|
Admin bool `json:"admin"`
|
||||||
|
// User's account is disabled.
|
||||||
|
// example: false
|
||||||
|
Disabled bool `json:"disabled"`
|
||||||
|
// User was approved by an admin.
|
||||||
|
// example: true
|
||||||
|
Approved bool `json:"approved"`
|
||||||
|
// Time when the last "please reset your password" email was sent, if at all. (ISO 8601 Datetime)
|
||||||
|
// example: 2021-07-30T09:20:25+00:00
|
||||||
|
ResetPasswordSentAt string `json:"reset_password_sent_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// PasswordChangeRequest models user password change parameters.
|
// PasswordChangeRequest models user password change parameters.
|
||||||
//
|
//
|
||||||
// swagger:parameters userPasswordChange
|
// swagger:parameters userPasswordChange
|
||||||
|
@ -34,3 +79,19 @@ type PasswordChangeRequest struct {
|
||||||
// required: true
|
// required: true
|
||||||
NewPassword string `form:"new_password" json:"new_password" xml:"new_password" validation:"required"`
|
NewPassword string `form:"new_password" json:"new_password" xml:"new_password" validation:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EmailChangeRequest models user email change parameters.
|
||||||
|
//
|
||||||
|
// swagger:parameters userEmailChange
|
||||||
|
type EmailChangeRequest struct {
|
||||||
|
// User's current password, for verification.
|
||||||
|
//
|
||||||
|
// in: formData
|
||||||
|
// required: true
|
||||||
|
Password string `form:"password" json:"password" xml:"password" validation:"required"`
|
||||||
|
// Desired new email address.
|
||||||
|
//
|
||||||
|
// in: formData
|
||||||
|
// required: true
|
||||||
|
NewEmail string `form:"new_email" json:"new_email" xml:"new_email" validation:"required"`
|
||||||
|
}
|
||||||
|
|
|
@ -26,13 +26,20 @@
|
||||||
type ConfirmData struct {
|
type ConfirmData struct {
|
||||||
// Username to be addressed.
|
// Username to be addressed.
|
||||||
Username string
|
Username string
|
||||||
// URL of the instance to present to the receiver.
|
// URL of the instance to
|
||||||
|
// present to the receiver.
|
||||||
InstanceURL string
|
InstanceURL string
|
||||||
// Name of the instance to present to the receiver.
|
// Name of the instance to
|
||||||
|
// present to the receiver.
|
||||||
InstanceName string
|
InstanceName string
|
||||||
// Link to present to the receiver to click on and do the confirmation.
|
// Link to present to the receiver to
|
||||||
// Should be a full link with protocol eg., https://example.org/confirm_email?token=some-long-token
|
// click on and do the confirmation.
|
||||||
|
// Should be a full link with protocol
|
||||||
|
// eg., https://example.org/confirm_email?token=some-long-token
|
||||||
ConfirmLink string
|
ConfirmLink string
|
||||||
|
// Is this confirm email being sent
|
||||||
|
// because this is a new sign-up?
|
||||||
|
NewSignup bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sender) SendConfirmEmail(toAddress string, data ConfirmData) error {
|
func (s *sender) SendConfirmEmail(toAddress string, data ConfirmData) error {
|
||||||
|
|
|
@ -40,17 +40,32 @@ func (suite *EmailTestSuite) SetupTest() {
|
||||||
suite.sender = testrig.NewEmailSender("../../web/template/", suite.sentEmails)
|
suite.sender = testrig.NewEmailSender("../../web/template/", suite.sentEmails)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *EmailTestSuite) TestTemplateConfirmNewSignup() {
|
||||||
|
confirmData := email.ConfirmData{
|
||||||
|
Username: "test",
|
||||||
|
InstanceURL: "https://example.org",
|
||||||
|
InstanceName: "Test Instance",
|
||||||
|
ConfirmLink: "https://example.org/confirm_email?token=ee24f71d-e615-43f9-afae-385c0799b7fa",
|
||||||
|
NewSignup: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.sender.SendConfirmEmail("user@example.org", confirmData)
|
||||||
|
suite.Len(suite.sentEmails, 1)
|
||||||
|
suite.Equal("To: user@example.org\r\nFrom: test@example.org\r\nSubject: GoToSocial Email Confirmation\r\nMIME-Version: 1.0\r\nContent-Transfer-Encoding: 8bit\r\nContent-Type: text/plain; charset=\"UTF-8\"\r\n\r\nHello test!\r\n\r\nYou are receiving this mail because you've requested an account on https://example.org.\r\n\r\nTo use your account, you must confirm that this is your email address.\r\n\r\nTo confirm your email, paste the following in your browser's address bar:\r\n\r\nhttps://example.org/confirm_email?token=ee24f71d-e615-43f9-afae-385c0799b7fa\r\n\r\n---\r\n\r\nIf you believe you've been sent this email in error, feel free to ignore it, or contact the administrator of https://example.org.\r\n\r\n", suite.sentEmails["user@example.org"])
|
||||||
|
}
|
||||||
|
|
||||||
func (suite *EmailTestSuite) TestTemplateConfirm() {
|
func (suite *EmailTestSuite) TestTemplateConfirm() {
|
||||||
confirmData := email.ConfirmData{
|
confirmData := email.ConfirmData{
|
||||||
Username: "test",
|
Username: "test",
|
||||||
InstanceURL: "https://example.org",
|
InstanceURL: "https://example.org",
|
||||||
InstanceName: "Test Instance",
|
InstanceName: "Test Instance",
|
||||||
ConfirmLink: "https://example.org/confirm_email?token=ee24f71d-e615-43f9-afae-385c0799b7fa",
|
ConfirmLink: "https://example.org/confirm_email?token=ee24f71d-e615-43f9-afae-385c0799b7fa",
|
||||||
|
NewSignup: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
suite.sender.SendConfirmEmail("user@example.org", confirmData)
|
suite.sender.SendConfirmEmail("user@example.org", confirmData)
|
||||||
suite.Len(suite.sentEmails, 1)
|
suite.Len(suite.sentEmails, 1)
|
||||||
suite.Equal("To: user@example.org\r\nFrom: test@example.org\r\nSubject: GoToSocial Email Confirmation\r\nMIME-Version: 1.0\r\nContent-Transfer-Encoding: 8bit\r\nContent-Type: text/plain; charset=\"UTF-8\"\r\n\r\nHello test!\r\n\r\nYou are receiving this mail because you've requested an account on https://example.org.\r\n\r\nTo use your account, you must confirm that this is your email address.\r\n\r\nTo confirm your email, paste the following in your browser's address bar:\r\n\r\nhttps://example.org/confirm_email?token=ee24f71d-e615-43f9-afae-385c0799b7fa\r\n\r\n---\r\n\r\nIf you believe you've been sent this email in error, feel free to ignore it, or contact the administrator of https://example.org.\r\n\r\n", suite.sentEmails["user@example.org"])
|
suite.Equal("To: user@example.org\r\nFrom: test@example.org\r\nSubject: GoToSocial Email Confirmation\r\nMIME-Version: 1.0\r\nContent-Transfer-Encoding: 8bit\r\nContent-Type: text/plain; charset=\"UTF-8\"\r\n\r\nHello test!\r\n\r\nYou are receiving this mail because you've requested an email address change on https://example.org.\r\n\r\nTo complete the change, you must confirm that this is your email address.\r\n\r\nTo confirm your email, paste the following in your browser's address bar:\r\n\r\nhttps://example.org/confirm_email?token=ee24f71d-e615-43f9-afae-385c0799b7fa\r\n\r\n---\r\n\r\nIf you believe you've been sent this email in error, feel free to ignore it, or contact the administrator of https://example.org.\r\n\r\n", suite.sentEmails["user@example.org"])
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *EmailTestSuite) TestTemplateReset() {
|
func (suite *EmailTestSuite) TestTemplateReset() {
|
||||||
|
|
|
@ -113,7 +113,7 @@ func (f *federatingDB) deleteAccount(
|
||||||
|
|
||||||
log.Debugf(ctx, "deleting account: %s", account.URI)
|
log.Debugf(ctx, "deleting account: %s", account.URI)
|
||||||
f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
|
f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
|
||||||
APObjectType: ap.ObjectProfile,
|
APObjectType: ap.ActorPerson,
|
||||||
APActivityType: ap.ActivityDelete,
|
APActivityType: ap.ActivityDelete,
|
||||||
GTSModel: account,
|
GTSModel: account,
|
||||||
Receiving: receiving,
|
Receiving: receiving,
|
||||||
|
|
|
@ -171,7 +171,7 @@ func (f *federatingDB) Move(ctx context.Context, move vocab.ActivityStreamsMove)
|
||||||
// We had a Move already or stored a new Move.
|
// We had a Move already or stored a new Move.
|
||||||
// Pass back to a worker for async processing.
|
// Pass back to a worker for async processing.
|
||||||
f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
|
f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
|
||||||
APObjectType: ap.ObjectProfile,
|
APObjectType: ap.ActorPerson,
|
||||||
APActivityType: ap.ActivityMove,
|
APActivityType: ap.ActivityMove,
|
||||||
GTSModel: stubMove,
|
GTSModel: stubMove,
|
||||||
Requesting: requestingAcct,
|
Requesting: requestingAcct,
|
||||||
|
|
|
@ -78,7 +78,7 @@ func (suite *MoveTestSuite) TestMove() {
|
||||||
|
|
||||||
// Should be a message heading to the processor.
|
// Should be a message heading to the processor.
|
||||||
msg, _ := suite.getFederatorMsg(5 * time.Second)
|
msg, _ := suite.getFederatorMsg(5 * time.Second)
|
||||||
suite.Equal(ap.ObjectProfile, msg.APObjectType)
|
suite.Equal(ap.ActorPerson, msg.APObjectType)
|
||||||
suite.Equal(ap.ActivityMove, msg.APActivityType)
|
suite.Equal(ap.ActivityMove, msg.APActivityType)
|
||||||
|
|
||||||
// Stub Move should be on the message.
|
// Stub Move should be on the message.
|
||||||
|
@ -95,7 +95,7 @@ func (suite *MoveTestSuite) TestMove() {
|
||||||
// Should be a message heading to the processor
|
// Should be a message heading to the processor
|
||||||
// since this is just a straight up retry.
|
// since this is just a straight up retry.
|
||||||
msg, _ = suite.getFederatorMsg(5 * time.Second)
|
msg, _ = suite.getFederatorMsg(5 * time.Second)
|
||||||
suite.Equal(ap.ObjectProfile, msg.APObjectType)
|
suite.Equal(ap.ActorPerson, msg.APObjectType)
|
||||||
suite.Equal(ap.ActivityMove, msg.APActivityType)
|
suite.Equal(ap.ActivityMove, msg.APActivityType)
|
||||||
|
|
||||||
// Same as the first Move, but with a different ID.
|
// Same as the first Move, but with a different ID.
|
||||||
|
@ -115,7 +115,7 @@ func (suite *MoveTestSuite) TestMove() {
|
||||||
// Should be a message heading to the processor
|
// Should be a message heading to the processor
|
||||||
// since this is just a retry with a different ID.
|
// since this is just a retry with a different ID.
|
||||||
msg, _ = suite.getFederatorMsg(5 * time.Second)
|
msg, _ = suite.getFederatorMsg(5 * time.Second)
|
||||||
suite.Equal(ap.ObjectProfile, msg.APObjectType)
|
suite.Equal(ap.ActorPerson, msg.APObjectType)
|
||||||
suite.Equal(ap.ActivityMove, msg.APActivityType)
|
suite.Equal(ap.ActivityMove, msg.APActivityType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -99,7 +99,7 @@ func (f *federatingDB) updateAccountable(ctx context.Context, receivingAcct *gts
|
||||||
// updating of eg., avatar/header, emojis, etc. The actual db
|
// updating of eg., avatar/header, emojis, etc. The actual db
|
||||||
// inserts/updates will take place there.
|
// inserts/updates will take place there.
|
||||||
f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
|
f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
|
||||||
APObjectType: ap.ObjectProfile,
|
APObjectType: ap.ActorPerson,
|
||||||
APActivityType: ap.ActivityUpdate,
|
APActivityType: ap.ActivityUpdate,
|
||||||
GTSModel: requestingAcct,
|
GTSModel: requestingAcct,
|
||||||
APObject: accountable,
|
APObject: accountable,
|
||||||
|
|
|
@ -22,7 +22,6 @@
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing/common"
|
"github.com/superseriousbusiness/gotosocial/internal/processing/common"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/text"
|
"github.com/superseriousbusiness/gotosocial/internal/text"
|
||||||
|
@ -39,7 +38,6 @@ type Processor struct {
|
||||||
state *state.State
|
state *state.State
|
||||||
converter *typeutils.Converter
|
converter *typeutils.Converter
|
||||||
mediaManager *media.Manager
|
mediaManager *media.Manager
|
||||||
oauthServer oauth.Server
|
|
||||||
filter *visibility.Filter
|
filter *visibility.Filter
|
||||||
formatter *text.Formatter
|
formatter *text.Formatter
|
||||||
federator *federation.Federator
|
federator *federation.Federator
|
||||||
|
@ -53,7 +51,6 @@ func New(
|
||||||
state *state.State,
|
state *state.State,
|
||||||
converter *typeutils.Converter,
|
converter *typeutils.Converter,
|
||||||
mediaManager *media.Manager,
|
mediaManager *media.Manager,
|
||||||
oauthServer oauth.Server,
|
|
||||||
federator *federation.Federator,
|
federator *federation.Federator,
|
||||||
filter *visibility.Filter,
|
filter *visibility.Filter,
|
||||||
parseMention gtsmodel.ParseMentionFunc,
|
parseMention gtsmodel.ParseMentionFunc,
|
||||||
|
@ -63,7 +60,6 @@ func New(
|
||||||
state: state,
|
state: state,
|
||||||
converter: converter,
|
converter: converter,
|
||||||
mediaManager: mediaManager,
|
mediaManager: mediaManager,
|
||||||
oauthServer: oauthServer,
|
|
||||||
filter: filter,
|
filter: filter,
|
||||||
formatter: text.NewFormatter(state.DB),
|
formatter: text.NewFormatter(state.DB),
|
||||||
federator: federator,
|
federator: federator,
|
||||||
|
|
|
@ -29,7 +29,6 @@
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing/account"
|
"github.com/superseriousbusiness/gotosocial/internal/processing/account"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing/common"
|
"github.com/superseriousbusiness/gotosocial/internal/processing/common"
|
||||||
|
@ -48,7 +47,6 @@ type AccountStandardTestSuite struct {
|
||||||
storage *storage.Driver
|
storage *storage.Driver
|
||||||
state state.State
|
state state.State
|
||||||
mediaManager *media.Manager
|
mediaManager *media.Manager
|
||||||
oauthServer oauth.Server
|
|
||||||
transportController transport.Controller
|
transportController transport.Controller
|
||||||
federator *federation.Federator
|
federator *federation.Federator
|
||||||
emailSender email.Sender
|
emailSender email.Sender
|
||||||
|
@ -106,7 +104,6 @@ func (suite *AccountStandardTestSuite) SetupTest() {
|
||||||
suite.storage = testrig.NewInMemoryStorage()
|
suite.storage = testrig.NewInMemoryStorage()
|
||||||
suite.state.Storage = suite.storage
|
suite.state.Storage = suite.storage
|
||||||
suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
|
suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
|
||||||
suite.oauthServer = testrig.NewTestOauthServer(suite.db)
|
|
||||||
|
|
||||||
suite.transportController = testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../testrig/media"))
|
suite.transportController = testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../testrig/media"))
|
||||||
suite.federator = testrig.NewTestFederator(&suite.state, suite.transportController, suite.mediaManager)
|
suite.federator = testrig.NewTestFederator(&suite.state, suite.transportController, suite.mediaManager)
|
||||||
|
@ -115,7 +112,7 @@ func (suite *AccountStandardTestSuite) SetupTest() {
|
||||||
|
|
||||||
filter := visibility.NewFilter(&suite.state)
|
filter := visibility.NewFilter(&suite.state)
|
||||||
common := common.New(&suite.state, suite.tc, suite.federator, filter)
|
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.StandardDBSetup(suite.db, nil)
|
||||||
testrig.StandardStorageSetup(suite.storage, "../../../testrig/media")
|
testrig.StandardStorageSetup(suite.storage, "../../../testrig/media")
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,23 +95,6 @@ func (p *Processor) Delete(
|
||||||
return nil
|
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
|
// deleteUserAndTokensForAccount deletes the gtsmodel.User and
|
||||||
// any OAuth tokens and applications for the given account.
|
// 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{
|
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
|
||||||
APObjectType: ap.ObjectProfile,
|
APObjectType: ap.ActorPerson,
|
||||||
APActivityType: ap.ActivityUpdate,
|
APActivityType: ap.ActivityUpdate,
|
||||||
GTSModel: account,
|
GTSModel: account,
|
||||||
Origin: account,
|
Origin: account,
|
||||||
|
|
|
@ -64,7 +64,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateSimple() {
|
||||||
|
|
||||||
// Profile update.
|
// Profile update.
|
||||||
suite.Equal(ap.ActivityUpdate, msg.APActivityType)
|
suite.Equal(ap.ActivityUpdate, msg.APActivityType)
|
||||||
suite.Equal(ap.ObjectProfile, msg.APObjectType)
|
suite.Equal(ap.ActorPerson, msg.APObjectType)
|
||||||
|
|
||||||
// Correct account updated.
|
// Correct account updated.
|
||||||
if msg.Origin == nil {
|
if msg.Origin == nil {
|
||||||
|
@ -114,7 +114,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateWithMention() {
|
||||||
|
|
||||||
// Profile update.
|
// Profile update.
|
||||||
suite.Equal(ap.ActivityUpdate, msg.APActivityType)
|
suite.Equal(ap.ActivityUpdate, msg.APActivityType)
|
||||||
suite.Equal(ap.ObjectProfile, msg.APObjectType)
|
suite.Equal(ap.ActorPerson, msg.APObjectType)
|
||||||
|
|
||||||
// Correct account updated.
|
// Correct account updated.
|
||||||
if msg.Origin == nil {
|
if msg.Origin == nil {
|
||||||
|
@ -170,7 +170,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateWithMarkdownNote() {
|
||||||
|
|
||||||
// Profile update.
|
// Profile update.
|
||||||
suite.Equal(ap.ActivityUpdate, msg.APActivityType)
|
suite.Equal(ap.ActivityUpdate, msg.APActivityType)
|
||||||
suite.Equal(ap.ObjectProfile, msg.APObjectType)
|
suite.Equal(ap.ActorPerson, msg.APObjectType)
|
||||||
|
|
||||||
// Correct account updated.
|
// Correct account updated.
|
||||||
if msg.Origin == nil {
|
if msg.Origin == nil {
|
||||||
|
@ -255,7 +255,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateWithFields() {
|
||||||
|
|
||||||
// Profile update.
|
// Profile update.
|
||||||
suite.Equal(ap.ActivityUpdate, msg.APActivityType)
|
suite.Equal(ap.ActivityUpdate, msg.APActivityType)
|
||||||
suite.Equal(ap.ObjectProfile, msg.APObjectType)
|
suite.Equal(ap.ActorPerson, msg.APObjectType)
|
||||||
|
|
||||||
// Correct account updated.
|
// Correct account updated.
|
||||||
if msg.Origin == nil {
|
if msg.Origin == nil {
|
||||||
|
@ -312,7 +312,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateNoteNotFields() {
|
||||||
|
|
||||||
// Profile update.
|
// Profile update.
|
||||||
suite.Equal(ap.ActivityUpdate, msg.APActivityType)
|
suite.Equal(ap.ActivityUpdate, msg.APActivityType)
|
||||||
suite.Equal(ap.ObjectProfile, msg.APObjectType)
|
suite.Equal(ap.ActorPerson, msg.APObjectType)
|
||||||
|
|
||||||
// Correct account updated.
|
// Correct account updated.
|
||||||
if msg.Origin == nil {
|
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 @@
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *Processor) AccountApprove(
|
func (p *Processor) SignupApprove(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
adminAcct *gtsmodel.Account,
|
adminAcct *gtsmodel.Account,
|
||||||
accountID string,
|
accountID string,
|
||||||
|
@ -55,7 +55,10 @@ func (p *Processor) AccountApprove(
|
||||||
if !*user.Approved {
|
if !*user.Approved {
|
||||||
// Process approval side effects asynschronously.
|
// Process approval side effects asynschronously.
|
||||||
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
|
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,
|
APActivityType: ap.ActivityAccept,
|
||||||
GTSModel: user,
|
GTSModel: user,
|
||||||
Origin: adminAcct,
|
Origin: adminAcct,
|
|
@ -42,7 +42,7 @@ func (suite *AdminApproveTestSuite) TestApprove() {
|
||||||
*targetUser = *suite.testUsers["unconfirmed_account"]
|
*targetUser = *suite.testUsers["unconfirmed_account"]
|
||||||
|
|
||||||
// Approve the sign-up.
|
// Approve the sign-up.
|
||||||
acct, errWithCode := suite.adminProcessor.AccountApprove(
|
acct, errWithCode := suite.adminProcessor.SignupApprove(
|
||||||
ctx,
|
ctx,
|
||||||
adminAcct,
|
adminAcct,
|
||||||
targetAcct.ID,
|
targetAcct.ID,
|
|
@ -30,7 +30,7 @@
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *Processor) AccountReject(
|
func (p *Processor) SignupReject(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
adminAcct *gtsmodel.Account,
|
adminAcct *gtsmodel.Account,
|
||||||
accountID string,
|
accountID string,
|
||||||
|
@ -102,7 +102,10 @@ func (p *Processor) AccountReject(
|
||||||
|
|
||||||
// Process rejection side effects asynschronously.
|
// Process rejection side effects asynschronously.
|
||||||
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
|
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,
|
APActivityType: ap.ActivityReject,
|
||||||
GTSModel: deniedUser,
|
GTSModel: deniedUser,
|
||||||
Origin: adminAcct,
|
Origin: adminAcct,
|
|
@ -42,7 +42,7 @@ func (suite *AdminRejectTestSuite) TestReject() {
|
||||||
message = "Too stinky."
|
message = "Too stinky."
|
||||||
)
|
)
|
||||||
|
|
||||||
acct, errWithCode := suite.adminProcessor.AccountReject(
|
acct, errWithCode := suite.adminProcessor.SignupReject(
|
||||||
ctx,
|
ctx,
|
||||||
adminAcct,
|
adminAcct,
|
||||||
targetAcct.ID,
|
targetAcct.ID,
|
||||||
|
@ -104,7 +104,7 @@ func (suite *AdminRejectTestSuite) TestRejectRemote() {
|
||||||
)
|
)
|
||||||
|
|
||||||
// Try to reject a remote account.
|
// Try to reject a remote account.
|
||||||
_, err := suite.adminProcessor.AccountReject(
|
_, err := suite.adminProcessor.SignupReject(
|
||||||
ctx,
|
ctx,
|
||||||
adminAcct,
|
adminAcct,
|
||||||
targetAcct.ID,
|
targetAcct.ID,
|
||||||
|
@ -126,7 +126,7 @@ func (suite *AdminRejectTestSuite) TestRejectApproved() {
|
||||||
)
|
)
|
||||||
|
|
||||||
// Try to reject an already-approved account.
|
// Try to reject an already-approved account.
|
||||||
_, err := suite.adminProcessor.AccountReject(
|
_, err := suite.adminProcessor.SignupReject(
|
||||||
ctx,
|
ctx,
|
||||||
adminAcct,
|
adminAcct,
|
||||||
targetAcct.ID,
|
targetAcct.ID,
|
|
@ -180,13 +180,13 @@ func NewProcessor(
|
||||||
// Start with sub processors that will
|
// Start with sub processors that will
|
||||||
// be required by the workers processor.
|
// be required by the workers processor.
|
||||||
common := common.New(state, converter, federator, filter)
|
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.media = media.New(state, converter, mediaManager, federator.TransportController())
|
||||||
processor.stream = stream.New(state, oauthServer)
|
processor.stream = stream.New(state, oauthServer)
|
||||||
|
|
||||||
// Instantiate the rest of the sub
|
// Instantiate the rest of the sub
|
||||||
// processors + pin them to this struct.
|
// 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.admin = admin.New(state, cleaner, converter, mediaManager, federator.TransportController(), emailSender)
|
||||||
processor.fedi = fedi.New(state, &common, converter, federator, filter)
|
processor.fedi = fedi.New(state, &common, converter, federator, filter)
|
||||||
processor.filtersv1 = filtersv1.New(state, converter)
|
processor.filtersv1 = filtersv1.New(state, converter)
|
||||||
|
@ -198,7 +198,7 @@ func NewProcessor(
|
||||||
processor.timeline = timeline.New(state, converter, filter)
|
processor.timeline = timeline.New(state, converter, filter)
|
||||||
processor.search = search.New(state, federator, converter, filter)
|
processor.search = search.New(state, federator, converter, filter)
|
||||||
processor.status = status.New(state, &common, &processor.polls, federator, converter, filter, parseMentionFunc)
|
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
|
// Workers processor handles asynchronous
|
||||||
// worker jobs; instantiate it separately
|
// 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{
|
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
|
||||||
APObjectType: ap.ObjectProfile,
|
APObjectType: ap.ActorPerson,
|
||||||
APActivityType: ap.ActivityFlag,
|
APActivityType: ap.ActivityFlag,
|
||||||
GTSModel: report,
|
GTSModel: report,
|
||||||
Origin: account,
|
Origin: account,
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
package account
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
@ -32,10 +32,9 @@
|
||||||
"github.com/superseriousbusiness/oauth2/v4"
|
"github.com/superseriousbusiness/oauth2/v4"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create processes the given form for creating a new account,
|
// Create processes the given form for creating a new user+account.
|
||||||
// returning a new user (with attached account) if successful.
|
|
||||||
//
|
//
|
||||||
// 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.
|
// If nil, the instance app will be used.
|
||||||
//
|
//
|
||||||
// Precondition: the form's fields should have already been
|
// Precondition: the form's fields should have already been
|
||||||
|
@ -124,9 +123,12 @@ func (p *Processor) Create(
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
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.
|
// (confirmation emails etc), perform these async.
|
||||||
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
|
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,
|
APObjectType: ap.ObjectProfile,
|
||||||
APActivityType: ap.ActivityCreate,
|
APActivityType: ap.ActivityCreate,
|
||||||
GTSModel: user,
|
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 @@
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"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/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"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
|
// EmailGetUserForConfirmToken retrieves the user (with account) from
|
||||||
// the database for the given "confirm your email" token string.
|
// the database for the given "confirm your email" token string.
|
||||||
func (p *Processor) EmailGetUserForConfirmToken(ctx context.Context, token string) (*gtsmodel.User, gtserror.WithCode) {
|
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 @@
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/email"
|
"github.com/superseriousbusiness/gotosocial/internal/email"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Processor struct {
|
type Processor struct {
|
||||||
state *state.State
|
state *state.State
|
||||||
|
converter *typeutils.Converter
|
||||||
|
oauthServer oauth.Server
|
||||||
emailSender email.Sender
|
emailSender email.Sender
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new user processor
|
// New returns a new user processor.
|
||||||
func New(state *state.State, emailSender email.Sender) Processor {
|
func New(
|
||||||
|
state *state.State,
|
||||||
|
converter *typeutils.Converter,
|
||||||
|
oauthServer oauth.Server,
|
||||||
|
emailSender email.Sender,
|
||||||
|
) Processor {
|
||||||
return Processor{
|
return Processor{
|
||||||
state: state,
|
state: state,
|
||||||
|
converter: converter,
|
||||||
emailSender: emailSender,
|
emailSender: emailSender,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing/user"
|
"github.com/superseriousbusiness/gotosocial/internal/processing/user"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -53,7 +54,7 @@ func (suite *UserStandardTestSuite) SetupTest() {
|
||||||
suite.emailSender = testrig.NewEmailSender("../../../web/template/", suite.sentEmails)
|
suite.emailSender = testrig.NewEmailSender("../../../web/template/", suite.sentEmails)
|
||||||
suite.testUsers = testrig.NewTestUsers()
|
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)
|
testrig.StandardDBSetup(suite.db, nil)
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,9 +71,9 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg *messages.Fro
|
||||||
case ap.ActivityCreate:
|
case ap.ActivityCreate:
|
||||||
switch cMsg.APObjectType {
|
switch cMsg.APObjectType {
|
||||||
|
|
||||||
// CREATE PROFILE/ACCOUNT
|
// CREATE USER (ie., new user+account sign-up)
|
||||||
case ap.ObjectProfile, ap.ActorPerson:
|
case ap.ObjectProfile:
|
||||||
return p.clientAPI.CreateAccount(ctx, cMsg)
|
return p.clientAPI.CreateUser(ctx, cMsg)
|
||||||
|
|
||||||
// CREATE NOTE/STATUS
|
// CREATE NOTE/STATUS
|
||||||
case ap.ObjectNote:
|
case ap.ObjectNote:
|
||||||
|
@ -111,13 +111,17 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg *messages.Fro
|
||||||
case ap.ObjectNote:
|
case ap.ObjectNote:
|
||||||
return p.clientAPI.UpdateStatus(ctx, cMsg)
|
return p.clientAPI.UpdateStatus(ctx, cMsg)
|
||||||
|
|
||||||
// UPDATE PROFILE/ACCOUNT
|
// UPDATE ACCOUNT (ie., bio, settings, etc)
|
||||||
case ap.ObjectProfile, ap.ActorPerson:
|
case ap.ActorPerson:
|
||||||
return p.clientAPI.UpdateAccount(ctx, cMsg)
|
return p.clientAPI.UpdateAccount(ctx, cMsg)
|
||||||
|
|
||||||
// UPDATE A FLAG/REPORT (mark as resolved/closed)
|
// UPDATE A FLAG/REPORT (mark as resolved/closed)
|
||||||
case ap.ActivityFlag:
|
case ap.ActivityFlag:
|
||||||
return p.clientAPI.UpdateReport(ctx, cMsg)
|
return p.clientAPI.UpdateReport(ctx, cMsg)
|
||||||
|
|
||||||
|
// UPDATE USER (ie., email address)
|
||||||
|
case ap.ObjectProfile:
|
||||||
|
return p.clientAPI.UpdateUser(ctx, cMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ACCEPT SOMETHING
|
// ACCEPT SOMETHING
|
||||||
|
@ -128,9 +132,9 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg *messages.Fro
|
||||||
case ap.ActivityFollow:
|
case ap.ActivityFollow:
|
||||||
return p.clientAPI.AcceptFollow(ctx, cMsg)
|
return p.clientAPI.AcceptFollow(ctx, cMsg)
|
||||||
|
|
||||||
// ACCEPT PROFILE/ACCOUNT (sign-up)
|
// ACCEPT USER (ie., new user+account sign-up)
|
||||||
case ap.ObjectProfile, ap.ActorPerson:
|
case ap.ObjectProfile:
|
||||||
return p.clientAPI.AcceptAccount(ctx, cMsg)
|
return p.clientAPI.AcceptUser(ctx, cMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// REJECT SOMETHING
|
// REJECT SOMETHING
|
||||||
|
@ -141,9 +145,9 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg *messages.Fro
|
||||||
case ap.ActivityFollow:
|
case ap.ActivityFollow:
|
||||||
return p.clientAPI.RejectFollowRequest(ctx, cMsg)
|
return p.clientAPI.RejectFollowRequest(ctx, cMsg)
|
||||||
|
|
||||||
// REJECT PROFILE/ACCOUNT (sign-up)
|
// REJECT USER (ie., new user+account sign-up)
|
||||||
case ap.ObjectProfile, ap.ActorPerson:
|
case ap.ObjectProfile:
|
||||||
return p.clientAPI.RejectAccount(ctx, cMsg)
|
return p.clientAPI.RejectUser(ctx, cMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UNDO SOMETHING
|
// UNDO SOMETHING
|
||||||
|
@ -175,17 +179,17 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg *messages.Fro
|
||||||
case ap.ObjectNote:
|
case ap.ObjectNote:
|
||||||
return p.clientAPI.DeleteStatus(ctx, cMsg)
|
return p.clientAPI.DeleteStatus(ctx, cMsg)
|
||||||
|
|
||||||
// DELETE PROFILE/ACCOUNT
|
// DELETE REMOTE ACCOUNT or LOCAL USER+ACCOUNT
|
||||||
case ap.ObjectProfile, ap.ActorPerson:
|
case ap.ActorPerson, ap.ObjectProfile:
|
||||||
return p.clientAPI.DeleteAccount(ctx, cMsg)
|
return p.clientAPI.DeleteAccountOrUser(ctx, cMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FLAG/REPORT SOMETHING
|
// FLAG/REPORT SOMETHING
|
||||||
case ap.ActivityFlag:
|
case ap.ActivityFlag:
|
||||||
switch cMsg.APObjectType { //nolint:gocritic
|
switch cMsg.APObjectType { //nolint:gocritic
|
||||||
|
|
||||||
// FLAG/REPORT A PROFILE
|
// FLAG/REPORT ACCOUNT
|
||||||
case ap.ObjectProfile:
|
case ap.ActorPerson:
|
||||||
return p.clientAPI.ReportAccount(ctx, cMsg)
|
return p.clientAPI.ReportAccount(ctx, cMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,8 +197,8 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg *messages.Fro
|
||||||
case ap.ActivityMove:
|
case ap.ActivityMove:
|
||||||
switch cMsg.APObjectType { //nolint:gocritic
|
switch cMsg.APObjectType { //nolint:gocritic
|
||||||
|
|
||||||
// MOVE PROFILE/ACCOUNT
|
// MOVE ACCOUNT
|
||||||
case ap.ObjectProfile, ap.ActorPerson:
|
case ap.ActorPerson:
|
||||||
return p.clientAPI.MoveAccount(ctx, cMsg)
|
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)
|
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)
|
newUser, ok := cMsg.GTSModel.(*gtsmodel.User)
|
||||||
if !ok {
|
if !ok {
|
||||||
return gtserror.Newf("%T not parseable as *gtsmodel.User", cMsg.GTSModel)
|
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.
|
// 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)
|
log.Errorf(ctx, "error emailing confirm: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -479,6 +483,22 @@ func (p *clientAPI) UpdateReport(ctx context.Context, cMsg *messages.FromClientA
|
||||||
return nil
|
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 {
|
func (p *clientAPI) AcceptFollow(ctx context.Context, cMsg *messages.FromClientAPI) error {
|
||||||
follow, ok := cMsg.GTSModel.(*gtsmodel.Follow)
|
follow, ok := cMsg.GTSModel.(*gtsmodel.Follow)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -669,7 +689,7 @@ func (p *clientAPI) DeleteStatus(ctx context.Context, cMsg *messages.FromClientA
|
||||||
return nil
|
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:
|
// The originID of the delete, one of:
|
||||||
// - ID of a domain block, for which
|
// - ID of a domain block, for which
|
||||||
// this account delete is a side effect.
|
// this account delete is a side effect.
|
||||||
|
@ -768,7 +788,7 @@ func (p *clientAPI) MoveAccount(ctx context.Context, cMsg *messages.FromClientAP
|
||||||
return nil
|
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)
|
newUser, ok := cMsg.GTSModel.(*gtsmodel.User)
|
||||||
if !ok {
|
if !ok {
|
||||||
return gtserror.Newf("%T not parseable as *gtsmodel.User", cMsg.GTSModel)
|
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
|
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)
|
deniedUser, ok := cMsg.GTSModel.(*gtsmodel.DeniedUser)
|
||||||
if !ok {
|
if !ok {
|
||||||
return gtserror.Newf("%T not parseable as *gtsmodel.DeniedUser", cMsg.GTSModel)
|
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:
|
case ap.ObjectNote:
|
||||||
return p.fediAPI.UpdateStatus(ctx, fMsg)
|
return p.fediAPI.UpdateStatus(ctx, fMsg)
|
||||||
|
|
||||||
// UPDATE PROFILE/ACCOUNT
|
// UPDATE ACCOUNT
|
||||||
case ap.ObjectProfile:
|
case ap.ActorPerson:
|
||||||
return p.fediAPI.UpdateAccount(ctx, fMsg)
|
return p.fediAPI.UpdateAccount(ctx, fMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,17 +137,17 @@ func (p *Processor) ProcessFromFediAPI(ctx context.Context, fMsg *messages.FromF
|
||||||
case ap.ObjectNote:
|
case ap.ObjectNote:
|
||||||
return p.fediAPI.DeleteStatus(ctx, fMsg)
|
return p.fediAPI.DeleteStatus(ctx, fMsg)
|
||||||
|
|
||||||
// DELETE PROFILE/ACCOUNT
|
// DELETE ACCOUNT
|
||||||
case ap.ObjectProfile:
|
case ap.ActorPerson:
|
||||||
return p.fediAPI.DeleteAccount(ctx, fMsg)
|
return p.fediAPI.DeleteAccount(ctx, fMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MOVE SOMETHING
|
// MOVE SOMETHING
|
||||||
case ap.ActivityMove:
|
case ap.ActivityMove:
|
||||||
|
|
||||||
// MOVE PROFILE/ACCOUNT
|
// MOVE ACCOUNT
|
||||||
// fromfediapi_move.go.
|
// fromfediapi_move.go.
|
||||||
if fMsg.APObjectType == ap.ObjectProfile {
|
if fMsg.APObjectType == ap.ActorPerson {
|
||||||
return p.fediAPI.MoveAccount(ctx, fMsg)
|
return p.fediAPI.MoveAccount(ctx, fMsg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -337,7 +337,7 @@ func (suite *FromFediAPITestSuite) TestProcessAccountDelete() {
|
||||||
|
|
||||||
// now they are mufos!
|
// now they are mufos!
|
||||||
err = testStructs.Processor.Workers().ProcessFromFediAPI(ctx, &messages.FromFediAPI{
|
err = testStructs.Processor.Workers().ProcessFromFediAPI(ctx, &messages.FromFediAPI{
|
||||||
APObjectType: ap.ObjectProfile,
|
APObjectType: ap.ActorPerson,
|
||||||
APActivityType: ap.ActivityDelete,
|
APActivityType: ap.ActivityDelete,
|
||||||
GTSModel: deletedAccount,
|
GTSModel: deletedAccount,
|
||||||
Receiving: receivingAccount,
|
Receiving: receivingAccount,
|
||||||
|
@ -613,7 +613,7 @@ func (suite *FromFediAPITestSuite) TestMoveAccount() {
|
||||||
|
|
||||||
// Process the Move.
|
// Process the Move.
|
||||||
err := testStructs.Processor.Workers().ProcessFromFediAPI(ctx, &messages.FromFediAPI{
|
err := testStructs.Processor.Workers().ProcessFromFediAPI(ctx, &messages.FromFediAPI{
|
||||||
APObjectType: ap.ObjectProfile,
|
APObjectType: ap.ActorPerson,
|
||||||
APActivityType: ap.ActivityMove,
|
APActivityType: ap.ActivityMove,
|
||||||
GTSModel: >smodel.Move{
|
GTSModel: >smodel.Move{
|
||||||
OriginURI: requestingAcct.URI,
|
OriginURI: requestingAcct.URI,
|
||||||
|
|
|
@ -74,7 +74,10 @@ func (s *Surface) emailUserReportClosed(ctx context.Context, report *gtsmodel.Re
|
||||||
|
|
||||||
// emailUserPleaseConfirm emails the given user
|
// emailUserPleaseConfirm emails the given user
|
||||||
// to ask them to confirm their email address.
|
// 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 == "" ||
|
if user.UnconfirmedEmail == "" ||
|
||||||
user.UnconfirmedEmail == user.Email {
|
user.UnconfirmedEmail == user.Email {
|
||||||
// User has already confirmed this
|
// User has already confirmed this
|
||||||
|
@ -104,6 +107,7 @@ func (s *Surface) emailUserPleaseConfirm(ctx context.Context, user *gtsmodel.Use
|
||||||
InstanceURL: instance.URI,
|
InstanceURL: instance.URI,
|
||||||
InstanceName: instance.Title,
|
InstanceName: instance.Title,
|
||||||
ConfirmLink: confirmLink,
|
ConfirmLink: confirmLink,
|
||||||
|
NewSignup: newSignup,
|
||||||
},
|
},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -63,6 +63,44 @@ func toMastodonVersion(in string) string {
|
||||||
return instanceMastodonVersion + "+" + strings.ReplaceAll(in, " ", "-")
|
return instanceMastodonVersion + "+" + strings.ReplaceAll(in, " ", "-")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserToAPIUser converts a *gtsmodel.User to an API
|
||||||
|
// representation suitable for serving to that user.
|
||||||
|
//
|
||||||
|
// Contains sensitive info so should only
|
||||||
|
// ever be served to the user themself.
|
||||||
|
func (c *Converter) UserToAPIUser(ctx context.Context, u *gtsmodel.User) *apimodel.User {
|
||||||
|
user := &apimodel.User{
|
||||||
|
ID: u.ID,
|
||||||
|
CreatedAt: util.FormatISO8601(u.CreatedAt),
|
||||||
|
Email: u.Email,
|
||||||
|
UnconfirmedEmail: u.UnconfirmedEmail,
|
||||||
|
Reason: u.Reason,
|
||||||
|
Moderator: *u.Moderator,
|
||||||
|
Admin: *u.Admin,
|
||||||
|
Disabled: *u.Disabled,
|
||||||
|
Approved: *u.Approved,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zero-able dates.
|
||||||
|
if !u.LastEmailedAt.IsZero() {
|
||||||
|
user.LastEmailedAt = util.FormatISO8601(u.LastEmailedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !u.ConfirmedAt.IsZero() {
|
||||||
|
user.ConfirmedAt = util.FormatISO8601(u.ConfirmedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !u.ConfirmationSentAt.IsZero() {
|
||||||
|
user.ConfirmationSentAt = util.FormatISO8601(u.ConfirmationSentAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !u.ResetPasswordSentAt.IsZero() {
|
||||||
|
user.ResetPasswordSentAt = util.FormatISO8601(u.ResetPasswordSentAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
// AppToAPIAppSensitive takes a db model application as a param, and returns a populated apitype application, or an error
|
// AppToAPIAppSensitive takes a db model application as a param, and returns a populated apitype application, or an error
|
||||||
// if something goes wrong. The returned application should be ready to serialize on an API level, and may have sensitive fields
|
// if something goes wrong. The returned application should be ready to serialize on an API level, and may have sensitive fields
|
||||||
// (such as client id and client secret), so serve it only to an authorized user who should have permission to see it.
|
// (such as client id and client secret), so serve it only to an authorized user who should have permission to see it.
|
||||||
|
|
|
@ -108,9 +108,9 @@ func (m *Module) signupPOSTHandler(c *gin.Context) {
|
||||||
}
|
}
|
||||||
form.IP = signUpIP
|
form.IP = signUpIP
|
||||||
|
|
||||||
// We have all the info we need, call account create
|
// We have all the info we need, call user+account create
|
||||||
// (this will also trigger side effects like sending emails etc).
|
// (this will also trigger side effects like sending emails etc).
|
||||||
user, errWithCode := m.processor.Account().Create(
|
user, errWithCode := m.processor.User().Create(
|
||||||
c.Request.Context(),
|
c.Request.Context(),
|
||||||
// nil to use
|
// nil to use
|
||||||
// instance app.
|
// instance app.
|
||||||
|
|
|
@ -24,6 +24,7 @@ import type {
|
||||||
UpdateAliasesFormData
|
UpdateAliasesFormData
|
||||||
} from "../../types/migration";
|
} from "../../types/migration";
|
||||||
import type { Theme } from "../../types/theme";
|
import type { Theme } from "../../types/theme";
|
||||||
|
import { User } from "../../types/user";
|
||||||
|
|
||||||
const extended = gtsApi.injectEndpoints({
|
const extended = gtsApi.injectEndpoints({
|
||||||
endpoints: (build) => ({
|
endpoints: (build) => ({
|
||||||
|
@ -37,6 +38,9 @@ const extended = gtsApi.injectEndpoints({
|
||||||
}),
|
}),
|
||||||
...replaceCacheOnMutation("verifyCredentials")
|
...replaceCacheOnMutation("verifyCredentials")
|
||||||
}),
|
}),
|
||||||
|
user: build.query<User, void>({
|
||||||
|
query: () => ({url: `/api/v1/user`})
|
||||||
|
}),
|
||||||
passwordChange: build.mutation({
|
passwordChange: build.mutation({
|
||||||
query: (data) => ({
|
query: (data) => ({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
@ -44,6 +48,14 @@ const extended = gtsApi.injectEndpoints({
|
||||||
body: data
|
body: data
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
|
emailChange: build.mutation<User, { password: string, new_email: string }>({
|
||||||
|
query: (data) => ({
|
||||||
|
method: "POST",
|
||||||
|
url: `/api/v1/user/email_change`,
|
||||||
|
body: data
|
||||||
|
}),
|
||||||
|
...replaceCacheOnMutation("user")
|
||||||
|
}),
|
||||||
aliasAccount: build.mutation<any, UpdateAliasesFormData>({
|
aliasAccount: build.mutation<any, UpdateAliasesFormData>({
|
||||||
async queryFn(formData, _api, _extraOpts, fetchWithBQ) {
|
async queryFn(formData, _api, _extraOpts, fetchWithBQ) {
|
||||||
// Pull entries out from the hooked form.
|
// Pull entries out from the hooked form.
|
||||||
|
@ -78,7 +90,9 @@ const extended = gtsApi.injectEndpoints({
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
useUpdateCredentialsMutation,
|
useUpdateCredentialsMutation,
|
||||||
|
useUserQuery,
|
||||||
usePasswordChangeMutation,
|
usePasswordChangeMutation,
|
||||||
|
useEmailChangeMutation,
|
||||||
useAliasAccountMutation,
|
useAliasAccountMutation,
|
||||||
useMoveAccountMutation,
|
useMoveAccountMutation,
|
||||||
useAccountThemesQuery,
|
useAccountThemesQuery,
|
||||||
|
|
34
web/source/settings/lib/types/user.ts
Normal file
34
web/source/settings/lib/types/user.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
created_at: string;
|
||||||
|
email?: string;
|
||||||
|
unconfirmed_email?: string;
|
||||||
|
reason?: string;
|
||||||
|
last_emailed_at?: string;
|
||||||
|
confirmed_at?: string;
|
||||||
|
confirmation_sent_at?: string;
|
||||||
|
moderator: boolean;
|
||||||
|
admin: boolean;
|
||||||
|
disabled: boolean;
|
||||||
|
approved: boolean;
|
||||||
|
reset_password_sent_at?: string;
|
||||||
|
}
|
|
@ -25,7 +25,9 @@ import FormWithData from "../../lib/form/form-with-data";
|
||||||
import Languages from "../../components/languages";
|
import Languages from "../../components/languages";
|
||||||
import MutationButton from "../../components/form/mutation-button";
|
import MutationButton from "../../components/form/mutation-button";
|
||||||
import { useVerifyCredentialsQuery } from "../../lib/query/oauth";
|
import { useVerifyCredentialsQuery } from "../../lib/query/oauth";
|
||||||
import { usePasswordChangeMutation, useUpdateCredentialsMutation } from "../../lib/query/user";
|
import { useEmailChangeMutation, usePasswordChangeMutation, useUpdateCredentialsMutation, useUserQuery } from "../../lib/query/user";
|
||||||
|
import Loading from "../../components/loading";
|
||||||
|
import { User } from "../../lib/types/user";
|
||||||
|
|
||||||
export default function UserSettings() {
|
export default function UserSettings() {
|
||||||
return (
|
return (
|
||||||
|
@ -98,6 +100,7 @@ function UserSettingsForm({ data }) {
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
<PasswordChange />
|
<PasswordChange />
|
||||||
|
<EmailChange />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -168,3 +171,105 @@ function PasswordChange() {
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function EmailChange() {
|
||||||
|
// Load existing user data.
|
||||||
|
const { data: user, isFetching, isLoading } = useUserQuery();
|
||||||
|
if (isFetching || isLoading) {
|
||||||
|
return <Loading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user === undefined) {
|
||||||
|
throw "could not fetch user";
|
||||||
|
}
|
||||||
|
|
||||||
|
return <EmailChangeForm user={user} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmailChangeForm({user}: {user: User}) {
|
||||||
|
const form = {
|
||||||
|
currentEmail: useTextInput("current_email", {
|
||||||
|
defaultValue: user.email,
|
||||||
|
nosubmit: true
|
||||||
|
}),
|
||||||
|
newEmail: useTextInput("new_email", {
|
||||||
|
validator: (value: string | undefined) => {
|
||||||
|
if (!value) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.toLowerCase() === user.email?.toLowerCase()) {
|
||||||
|
return "cannot change to your existing address";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.toLowerCase() === user.unconfirmed_email?.toLowerCase()) {
|
||||||
|
return "you already have a pending email address change to this address";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
password: useTextInput("password"),
|
||||||
|
};
|
||||||
|
const [submitForm, result] = useFormSubmit(form, useEmailChangeMutation());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className="change-email" onSubmit={submitForm}>
|
||||||
|
<div className="form-section-docs">
|
||||||
|
<h3>Change Email</h3>
|
||||||
|
<a
|
||||||
|
href="https://docs.gotosocial.org/en/latest/user_guide/settings/#email-change"
|
||||||
|
target="_blank"
|
||||||
|
className="docslink"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Learn more about this (opens in a new tab)
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ user.unconfirmed_email && <>
|
||||||
|
<div className="info">
|
||||||
|
<i className="fa fa-fw fa-info-circle" aria-hidden="true"></i>
|
||||||
|
<b>
|
||||||
|
You currently have a pending email address
|
||||||
|
change to the address: {user.unconfirmed_email}
|
||||||
|
<br />
|
||||||
|
To confirm {user.unconfirmed_email} as your new
|
||||||
|
address for this account, please check your email inbox.
|
||||||
|
</b>
|
||||||
|
</div>
|
||||||
|
</> }
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
type="email"
|
||||||
|
name="current-email"
|
||||||
|
field={form.currentEmail}
|
||||||
|
label="Current email address"
|
||||||
|
autoComplete="none"
|
||||||
|
disabled={true}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
field={form.password}
|
||||||
|
label="Current password"
|
||||||
|
autoComplete="current-password"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
type="email"
|
||||||
|
name="new-email"
|
||||||
|
field={form.newEmail}
|
||||||
|
label="New email address"
|
||||||
|
autoComplete="none"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MutationButton
|
||||||
|
disabled={!form.password || !form.newEmail || !form.newEmail.valid}
|
||||||
|
label="Change email address"
|
||||||
|
result={result}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -18,11 +18,15 @@
|
||||||
*/ -}}
|
*/ -}}
|
||||||
|
|
||||||
Hello {{ .Username -}}!
|
Hello {{ .Username -}}!
|
||||||
|
{{ if .NewSignup }}
|
||||||
You are receiving this mail because you've requested an account on {{ .InstanceURL -}}.
|
You are receiving this mail because you've requested an account on {{ .InstanceURL -}}.
|
||||||
|
|
||||||
To use your account, you must confirm that this is your email address.
|
To use your account, you must confirm that this is your email address.
|
||||||
|
{{ else }}
|
||||||
|
You are receiving this mail because you've requested an email address change on {{ .InstanceURL -}}.
|
||||||
|
|
||||||
|
To complete the change, you must confirm that this is your email address.
|
||||||
|
{{ end }}
|
||||||
To confirm your email, paste the following in your browser's address bar:
|
To confirm your email, paste the following in your browser's address bar:
|
||||||
|
|
||||||
{{ .ConfirmLink }}
|
{{ .ConfirmLink }}
|
||||||
|
|
Loading…
Reference in a new issue