From bcda048eab799284fc46d74706334bf9ef76dc83 Mon Sep 17 00:00:00 2001
From: tobi <31960611+tsmethurst@users.noreply.github.com>
Date: Thu, 6 Jun 2024 15:43:25 +0200
Subject: [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
---
internal/api/activitypub/users/userget_test.go | 2 +-
internal/api/client/accounts/accountcreate.go | 6 +-
internal/api/client/accounts/accountdelete.go | 2 +-
internal/api/client/admin/accountapprove.go | 2 +-
internal/api/client/admin/accountreject.go | 2 +-
internal/api/client/user/emailchange.go | 104 ++++++++++++++
internal/api/client/user/emailchange_test.go | 142 +++++++++++++++++++
internal/api/client/user/passwordchange_test.go | 122 +++++------------
internal/api/client/user/user.go | 4 +
internal/api/client/user/user_test.go | 43 +++++-
internal/api/client/user/userget.go | 78 +++++++++++
internal/api/model/user.go | 61 +++++++++
internal/email/confirm.go | 15 +-
internal/email/email_test.go | 17 ++-
internal/federation/federatingdb/delete.go | 2 +-
internal/federation/federatingdb/move.go | 2 +-
internal/federation/federatingdb/move_test.go | 6 +-
internal/federation/federatingdb/update.go | 2 +-
internal/processing/account/account.go | 4 -
internal/processing/account/account_test.go | 5 +-
internal/processing/account/create.go | 165 ----------------------
internal/processing/account/delete.go | 17 ---
internal/processing/account/update.go | 2 +-
internal/processing/account/update_test.go | 10 +-
internal/processing/account_test.go | 106 --------------
internal/processing/admin/accountapprove.go | 79 -----------
internal/processing/admin/accountapprove_test.go | 75 ----------
internal/processing/admin/accountreject.go | 113 ---------------
internal/processing/admin/accountreject_test.go | 142 -------------------
internal/processing/admin/signupapprove.go | 82 +++++++++++
internal/processing/admin/signupapprove_test.go | 75 ++++++++++
internal/processing/admin/signupreject.go | 116 ++++++++++++++++
internal/processing/admin/signupreject_test.go | 142 +++++++++++++++++++
internal/processing/processor.go | 6 +-
internal/processing/report/create.go | 2 +-
internal/processing/user/create.go | 167 +++++++++++++++++++++++
internal/processing/user/delete.go | 48 +++++++
internal/processing/user/email.go | 81 +++++++++++
internal/processing/user/get.go | 32 +++++
internal/processing/user/user.go | 14 +-
internal/processing/user/user_test.go | 3 +-
internal/processing/workers/fromclientapi.go | 66 +++++----
internal/processing/workers/fromfediapi.go | 12 +-
internal/processing/workers/fromfediapi_test.go | 4 +-
internal/processing/workers/surfaceemail.go | 6 +-
internal/typeutils/internaltofrontend.go | 38 ++++++
internal/web/signup.go | 4 +-
47 files changed, 1365 insertions(+), 863 deletions(-)
create mode 100644 internal/api/client/user/emailchange.go
create mode 100644 internal/api/client/user/emailchange_test.go
create mode 100644 internal/api/client/user/userget.go
delete mode 100644 internal/processing/account/create.go
delete mode 100644 internal/processing/account_test.go
delete mode 100644 internal/processing/admin/accountapprove.go
delete mode 100644 internal/processing/admin/accountapprove_test.go
delete mode 100644 internal/processing/admin/accountreject.go
delete mode 100644 internal/processing/admin/accountreject_test.go
create mode 100644 internal/processing/admin/signupapprove.go
create mode 100644 internal/processing/admin/signupapprove_test.go
create mode 100644 internal/processing/admin/signupreject.go
create mode 100644 internal/processing/admin/signupreject_test.go
create mode 100644 internal/processing/user/create.go
create mode 100644 internal/processing/user/delete.go
create mode 100644 internal/processing/user/get.go
(limited to 'internal')
diff --git a/internal/api/activitypub/users/userget_test.go b/internal/api/activitypub/users/userget_test.go
index ac8b2c0eb..2a68b309c 100644
--- a/internal/api/activitypub/users/userget_test.go
+++ b/internal/api/activitypub/users/userget_test.go
@@ -97,7 +97,7 @@ func (suite *UserGetTestSuite) TestGetUserPublicKeyDeleted() {
userModule := users.New(suite.processor)
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
if !testrig.WaitFor(func() bool {
diff --git a/internal/api/client/accounts/accountcreate.go b/internal/api/client/accounts/accountcreate.go
index 920b6d4d8..33d743791 100644
--- a/internal/api/client/accounts/accountcreate.go
+++ b/internal/api/client/accounts/accountcreate.go
@@ -105,9 +105,9 @@ func (m *Module) AccountCreatePOSTHandler(c *gin.Context) {
}
form.IP = signUpIP
- // Create the new account + user.
+ // Create the new user+account.
ctx := c.Request.Context()
- user, errWithCode := m.processor.Account().Create(
+ user, errWithCode := m.processor.User().Create(
ctx,
authed.Application,
form,
@@ -118,7 +118,7 @@ func (m *Module) AccountCreatePOSTHandler(c *gin.Context) {
}
// Get a token for the new user.
- ti, errWithCode := m.processor.Account().TokenForNewUser(
+ ti, errWithCode := m.processor.User().TokenForNewUser(
ctx,
authed.Token,
authed.Application,
diff --git a/internal/api/client/accounts/accountdelete.go b/internal/api/client/accounts/accountdelete.go
index 947634f70..9a1ef7931 100644
--- a/internal/api/client/accounts/accountdelete.go
+++ b/internal/api/client/accounts/accountdelete.go
@@ -91,7 +91,7 @@ func (m *Module) AccountDeletePOSTHandler(c *gin.Context) {
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)
return
}
diff --git a/internal/api/client/admin/accountapprove.go b/internal/api/client/admin/accountapprove.go
index ff6474adb..7aaa48509 100644
--- a/internal/api/client/admin/accountapprove.go
+++ b/internal/api/client/admin/accountapprove.go
@@ -91,7 +91,7 @@ func (m *Module) AccountApprovePOSTHandler(c *gin.Context) {
return
}
- account, errWithCode := m.processor.Admin().AccountApprove(
+ account, errWithCode := m.processor.Admin().SignupApprove(
c.Request.Context(),
authed.Account,
targetAcctID,
diff --git a/internal/api/client/admin/accountreject.go b/internal/api/client/admin/accountreject.go
index 1e909b508..a4653985d 100644
--- a/internal/api/client/admin/accountreject.go
+++ b/internal/api/client/admin/accountreject.go
@@ -119,7 +119,7 @@ func (m *Module) AccountRejectPOSTHandler(c *gin.Context) {
return
}
- account, errWithCode := m.processor.Admin().AccountReject(
+ account, errWithCode := m.processor.Admin().SignupReject(
c.Request.Context(),
authed.Account,
targetAcctID,
diff --git a/internal/api/client/user/emailchange.go b/internal/api/client/user/emailchange.go
new file mode 100644
index 000000000..b2e25343f
--- /dev/null
+++ b/internal/api/client/user/emailchange.go
@@ -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 .
+
+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)
+}
diff --git a/internal/api/client/user/emailchange_test.go b/internal/api/client/user/emailchange_test.go
new file mode 100644
index 000000000..fce96c144
--- /dev/null
+++ b/internal/api/client/user/emailchange_test.go
@@ -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 .
+
+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{})
+}
diff --git a/internal/api/client/user/passwordchange_test.go b/internal/api/client/user/passwordchange_test.go
index b820696b5..8a741f96c 100644
--- a/internal/api/client/user/passwordchange_test.go
+++ b/internal/api/client/user/passwordchange_test.go
@@ -19,18 +19,13 @@ package user_test
import (
"context"
- "fmt"
- "io/ioutil"
+ "io"
"net/http"
- "net/http/httptest"
- "net/url"
"testing"
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/api/client/user"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
- "github.com/superseriousbusiness/gotosocial/testrig"
"golang.org/x/crypto/bcrypt"
)
@@ -39,29 +34,20 @@ type PasswordChangeTestSuite struct {
}
func (suite *PasswordChangeTestSuite) TestPasswordChangePOST() {
- t := suite.testTokens["local_account_1"]
- 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{
+ response, code := suite.POST(user.PasswordChangePath, map[string][]string{
"old_password": {"password"},
"new_password": {"peepeepoopoopassword"},
- }
- suite.userModule.PasswordChangePOSTHandler(ctx)
+ }, suite.userModule.PasswordChangePOSTHandler)
+ defer response.Body.Close()
- // check response
- suite.EqualValues(http.StatusOK, recorder.Code)
+ // Check response
+ suite.EqualValues(http.StatusOK, code)
dbUser := >smodel.User{}
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
err = bcrypt.CompareHashAndPassword([]byte(dbUser.EncryptedPassword), []byte("peepeepoopoopassword"))
@@ -73,85 +59,49 @@ func (suite *PasswordChangeTestSuite) TestPasswordChangePOST() {
}
func (suite *PasswordChangeTestSuite) TestPasswordMissingOldPassword() {
- t := suite.testTokens["local_account_1"]
- 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{
+ response, code := suite.POST(user.PasswordChangePath, map[string][]string{
"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))
}
func (suite *PasswordChangeTestSuite) TestPasswordIncorrectOldPassword() {
- t := suite.testTokens["local_account_1"]
- 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{
+ response, code := suite.POST(user.PasswordChangePath, map[string][]string{
"old_password": {"notright"},
"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))
}
func (suite *PasswordChangeTestSuite) TestPasswordWeakNewPassword() {
- t := suite.testTokens["local_account_1"]
- 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{
+ response, code := suite.POST(user.PasswordChangePath, map[string][]string{
"old_password": {"password"},
"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))
}
diff --git a/internal/api/client/user/user.go b/internal/api/client/user/user.go
index 487b9684c..6ad176a2e 100644
--- a/internal/api/client/user/user.go
+++ b/internal/api/client/user/user.go
@@ -29,6 +29,8 @@ const (
BasePath = "/v1/user"
// PasswordChangePath is the path for POSTing a password change request.
PasswordChangePath = BasePath + "/password_change"
+ // EmailChangePath is the path for POSTing an email address change request.
+ EmailChangePath = BasePath + "/email_change"
)
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) {
+ attachHandler(http.MethodGet, BasePath, m.UserGETHandler)
attachHandler(http.MethodPost, PasswordChangePath, m.PasswordChangePOSTHandler)
+ attachHandler(http.MethodPost, EmailChangePath, m.EmailChangePOSTHandler)
}
diff --git a/internal/api/client/user/user_test.go b/internal/api/client/user/user_test.go
index efff89b13..808daf1a3 100644
--- a/internal/api/client/user/user_test.go
+++ b/internal/api/client/user/user_test.go
@@ -18,14 +18,19 @@
package user_test
import (
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+
+ "github.com/gin-gonic/gin"
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/api/client/user"
"github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/email"
"github.com/superseriousbusiness/gotosocial/internal/federation"
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/media"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/processing"
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/storage"
@@ -39,7 +44,6 @@ type UserStandardTestSuite struct {
tc *typeutils.Converter
mediaManager *media.Manager
federator *federation.Federator
- emailSender email.Sender
processor *processing.Processor
storage *storage.Driver
state state.State
@@ -50,8 +54,6 @@ type UserStandardTestSuite struct {
testUsers map[string]*gtsmodel.User
testAccounts map[string]*gtsmodel.Account
- sentEmails map[string]string
-
userModule *user.Module
}
@@ -83,9 +85,7 @@ func (suite *UserStandardTestSuite) SetupTest() {
suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
suite.federator = testrig.NewTestFederator(&suite.state, testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../../testrig/media")), suite.mediaManager)
- suite.sentEmails = make(map[string]string)
- suite.emailSender = testrig.NewEmailSender("../../../../web/template/", suite.sentEmails)
- suite.processor = testrig.NewTestProcessor(&suite.state, suite.federator, suite.emailSender, suite.mediaManager)
+ suite.processor = testrig.NewTestProcessor(&suite.state, suite.federator, testrig.NewEmailSender("../../../../web/template/", nil), suite.mediaManager)
suite.userModule = user.New(suite.processor)
testrig.StandardDBSetup(suite.db, suite.testAccounts)
testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media")
@@ -96,3 +96,32 @@ func (suite *UserStandardTestSuite) TearDownTest() {
testrig.StandardStorageTeardown(suite.storage)
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
+}
diff --git a/internal/api/client/user/userget.go b/internal/api/client/user/userget.go
new file mode 100644
index 000000000..147c1dbd5
--- /dev/null
+++ b/internal/api/client/user/userget.go
@@ -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 .
+
+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)
+}
diff --git a/internal/api/model/user.go b/internal/api/model/user.go
index 1a70a90d7..9226406d6 100644
--- a/internal/api/model/user.go
+++ b/internal/api/model/user.go
@@ -17,6 +17,51 @@
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.
//
// swagger:parameters userPasswordChange
@@ -34,3 +79,19 @@ type PasswordChangeRequest struct {
// required: true
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"`
+}
diff --git a/internal/email/confirm.go b/internal/email/confirm.go
index 9f05a4f71..4fbe2a98f 100644
--- a/internal/email/confirm.go
+++ b/internal/email/confirm.go
@@ -26,13 +26,20 @@ const (
type ConfirmData struct {
// Username to be addressed.
Username string
- // URL of the instance to present to the receiver.
+ // URL of the instance to
+ // present to the receiver.
InstanceURL string
- // Name of the instance to present to the receiver.
+ // Name of the instance to
+ // present to the receiver.
InstanceName string
- // Link to present to the receiver to click on and do the confirmation.
- // Should be a full link with protocol eg., https://example.org/confirm_email?token=some-long-token
+ // Link to present to the receiver to
+ // 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
+ // Is this confirm email being sent
+ // because this is a new sign-up?
+ NewSignup bool
}
func (s *sender) SendConfirmEmail(toAddress string, data ConfirmData) error {
diff --git a/internal/email/email_test.go b/internal/email/email_test.go
index b57562cb5..aacca1b3d 100644
--- a/internal/email/email_test.go
+++ b/internal/email/email_test.go
@@ -40,12 +40,13 @@ func (suite *EmailTestSuite) SetupTest() {
suite.sender = testrig.NewEmailSender("../../web/template/", suite.sentEmails)
}
-func (suite *EmailTestSuite) TestTemplateConfirm() {
+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)
@@ -53,6 +54,20 @@ func (suite *EmailTestSuite) TestTemplateConfirm() {
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() {
+ 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: false,
+ }
+
+ 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 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() {
resetData := email.ResetData{
Username: "test",
diff --git a/internal/federation/federatingdb/delete.go b/internal/federation/federatingdb/delete.go
index 7e9b66c5a..931320940 100644
--- a/internal/federation/federatingdb/delete.go
+++ b/internal/federation/federatingdb/delete.go
@@ -113,7 +113,7 @@ func (f *federatingDB) deleteAccount(
log.Debugf(ctx, "deleting account: %s", account.URI)
f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
- APObjectType: ap.ObjectProfile,
+ APObjectType: ap.ActorPerson,
APActivityType: ap.ActivityDelete,
GTSModel: account,
Receiving: receiving,
diff --git a/internal/federation/federatingdb/move.go b/internal/federation/federatingdb/move.go
index 59dc2529c..681a9cff2 100644
--- a/internal/federation/federatingdb/move.go
+++ b/internal/federation/federatingdb/move.go
@@ -171,7 +171,7 @@ func (f *federatingDB) Move(ctx context.Context, move vocab.ActivityStreamsMove)
// We had a Move already or stored a new Move.
// Pass back to a worker for async processing.
f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
- APObjectType: ap.ObjectProfile,
+ APObjectType: ap.ActorPerson,
APActivityType: ap.ActivityMove,
GTSModel: stubMove,
Requesting: requestingAcct,
diff --git a/internal/federation/federatingdb/move_test.go b/internal/federation/federatingdb/move_test.go
index 3e35dc97a..e9689b1a7 100644
--- a/internal/federation/federatingdb/move_test.go
+++ b/internal/federation/federatingdb/move_test.go
@@ -78,7 +78,7 @@ func (suite *MoveTestSuite) TestMove() {
// Should be a message heading to the processor.
msg, _ := suite.getFederatorMsg(5 * time.Second)
- suite.Equal(ap.ObjectProfile, msg.APObjectType)
+ suite.Equal(ap.ActorPerson, msg.APObjectType)
suite.Equal(ap.ActivityMove, msg.APActivityType)
// Stub Move should be on the message.
@@ -95,7 +95,7 @@ func (suite *MoveTestSuite) TestMove() {
// Should be a message heading to the processor
// since this is just a straight up retry.
msg, _ = suite.getFederatorMsg(5 * time.Second)
- suite.Equal(ap.ObjectProfile, msg.APObjectType)
+ suite.Equal(ap.ActorPerson, msg.APObjectType)
suite.Equal(ap.ActivityMove, msg.APActivityType)
// 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
// since this is just a retry with a different ID.
msg, _ = suite.getFederatorMsg(5 * time.Second)
- suite.Equal(ap.ObjectProfile, msg.APObjectType)
+ suite.Equal(ap.ActorPerson, msg.APObjectType)
suite.Equal(ap.ActivityMove, msg.APActivityType)
}
diff --git a/internal/federation/federatingdb/update.go b/internal/federation/federatingdb/update.go
index 2f00e0867..16ecf3443 100644
--- a/internal/federation/federatingdb/update.go
+++ b/internal/federation/federatingdb/update.go
@@ -99,7 +99,7 @@ func (f *federatingDB) updateAccountable(ctx context.Context, receivingAcct *gts
// updating of eg., avatar/header, emojis, etc. The actual db
// inserts/updates will take place there.
f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
- APObjectType: ap.ObjectProfile,
+ APObjectType: ap.ActorPerson,
APActivityType: ap.ActivityUpdate,
GTSModel: requestingAcct,
APObject: accountable,
diff --git a/internal/processing/account/account.go b/internal/processing/account/account.go
index dbcecdb0a..65bb40292 100644
--- a/internal/processing/account/account.go
+++ b/internal/processing/account/account.go
@@ -22,7 +22,6 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/media"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/processing/common"
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/text"
@@ -39,7 +38,6 @@ type Processor struct {
state *state.State
converter *typeutils.Converter
mediaManager *media.Manager
- oauthServer oauth.Server
filter *visibility.Filter
formatter *text.Formatter
federator *federation.Federator
@@ -53,7 +51,6 @@ func New(
state *state.State,
converter *typeutils.Converter,
mediaManager *media.Manager,
- oauthServer oauth.Server,
federator *federation.Federator,
filter *visibility.Filter,
parseMention gtsmodel.ParseMentionFunc,
@@ -63,7 +60,6 @@ func New(
state: state,
converter: converter,
mediaManager: mediaManager,
- oauthServer: oauthServer,
filter: filter,
formatter: text.NewFormatter(state.DB),
federator: federator,
diff --git a/internal/processing/account/account_test.go b/internal/processing/account/account_test.go
index 10d5f91e1..556f4d91f 100644
--- a/internal/processing/account/account_test.go
+++ b/internal/processing/account/account_test.go
@@ -29,7 +29,6 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/messages"
- "github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/processing"
"github.com/superseriousbusiness/gotosocial/internal/processing/account"
"github.com/superseriousbusiness/gotosocial/internal/processing/common"
@@ -48,7 +47,6 @@ type AccountStandardTestSuite struct {
storage *storage.Driver
state state.State
mediaManager *media.Manager
- oauthServer oauth.Server
transportController transport.Controller
federator *federation.Federator
emailSender email.Sender
@@ -106,7 +104,6 @@ func (suite *AccountStandardTestSuite) SetupTest() {
suite.storage = testrig.NewInMemoryStorage()
suite.state.Storage = suite.storage
suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
- suite.oauthServer = testrig.NewTestOauthServer(suite.db)
suite.transportController = testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../testrig/media"))
suite.federator = testrig.NewTestFederator(&suite.state, suite.transportController, suite.mediaManager)
@@ -115,7 +112,7 @@ func (suite *AccountStandardTestSuite) SetupTest() {
filter := visibility.NewFilter(&suite.state)
common := common.New(&suite.state, suite.tc, suite.federator, filter)
- suite.accountProcessor = account.New(&common, &suite.state, suite.tc, suite.mediaManager, suite.oauthServer, suite.federator, filter, processing.GetParseMentionFunc(&suite.state, suite.federator))
+ suite.accountProcessor = account.New(&common, &suite.state, suite.tc, suite.mediaManager, suite.federator, filter, processing.GetParseMentionFunc(&suite.state, suite.federator))
testrig.StandardDBSetup(suite.db, nil)
testrig.StandardStorageSetup(suite.storage, "../../../testrig/media")
}
diff --git a/internal/processing/account/create.go b/internal/processing/account/create.go
deleted file mode 100644
index 761165356..000000000
--- a/internal/processing/account/create.go
+++ /dev/null
@@ -1,165 +0,0 @@
-// GoToSocial
-// Copyright (C) GoToSocial Authors admin@gotosocial.org
-// SPDX-License-Identifier: AGPL-3.0-or-later
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package account
-
-import (
- "context"
- "fmt"
- "time"
-
- "github.com/superseriousbusiness/gotosocial/internal/ap"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/messages"
- "github.com/superseriousbusiness/gotosocial/internal/text"
- "github.com/superseriousbusiness/oauth2/v4"
-)
-
-// Create processes the given form for creating a new account,
-// returning a new user (with attached account) if successful.
-//
-// App should be the app used to create the account.
-// If nil, the instance app will be used.
-//
-// Precondition: the form's fields should have already been
-// validated and normalized by the caller.
-func (p *Processor) Create(
- ctx context.Context,
- app *gtsmodel.Application,
- form *apimodel.AccountCreateRequest,
-) (*gtsmodel.User, gtserror.WithCode) {
- const (
- usersPerDay = 10
- regBacklog = 20
- )
-
- // Ensure no more than usersPerDay
- // have registered in the last 24h.
- newUsersCount, err := p.state.DB.CountApprovedSignupsSince(ctx, time.Now().Add(-24*time.Hour))
- if err != nil {
- err := fmt.Errorf("db error counting new users: %w", err)
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- if newUsersCount >= usersPerDay {
- err := fmt.Errorf("this instance has hit its limit of new sign-ups for today; you can try again tomorrow")
- return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
- }
-
- // Ensure the new users backlog isn't full.
- backlogLen, err := p.state.DB.CountUnhandledSignups(ctx)
- if err != nil {
- err := fmt.Errorf("db error counting registration backlog length: %w", err)
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- if backlogLen >= regBacklog {
- err := fmt.Errorf("this instance's sign-up backlog is currently full; you must wait until pending sign-ups are handled by the admin(s)")
- return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
- }
-
- emailAvailable, err := p.state.DB.IsEmailAvailable(ctx, form.Email)
- if err != nil {
- err := fmt.Errorf("db error checking email availability: %w", err)
- return nil, gtserror.NewErrorInternalError(err)
- }
- if !emailAvailable {
- err := fmt.Errorf("email address %s is not available", form.Email)
- return nil, gtserror.NewErrorConflict(err, err.Error())
- }
-
- usernameAvailable, err := p.state.DB.IsUsernameAvailable(ctx, form.Username)
- if err != nil {
- err := fmt.Errorf("db error checking username availability: %w", err)
- return nil, gtserror.NewErrorInternalError(err)
- }
- if !usernameAvailable {
- err := fmt.Errorf("username %s is not available", form.Username)
- return nil, gtserror.NewErrorConflict(err, err.Error())
- }
-
- // Only store reason if one is required.
- var reason string
- if config.GetAccountsReasonRequired() {
- reason = form.Reason
- }
-
- // Use instance app if no app provided.
- if app == nil {
- app, err = p.state.DB.GetInstanceApplication(ctx)
- if err != nil {
- err := fmt.Errorf("db error getting instance app: %w", err)
- return nil, gtserror.NewErrorInternalError(err)
- }
- }
-
- user, err := p.state.DB.NewSignup(ctx, gtsmodel.NewSignup{
- Username: form.Username,
- Email: form.Email,
- Password: form.Password,
- Reason: text.SanitizeToPlaintext(reason),
- SignUpIP: form.IP,
- Locale: form.Locale,
- AppID: app.ID,
- })
- if err != nil {
- err := fmt.Errorf("db error creating new signup: %w", err)
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- // There are side effects for creating a new account
- // (confirmation emails etc), perform these async.
- p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
- APObjectType: ap.ObjectProfile,
- APActivityType: ap.ActivityCreate,
- GTSModel: user,
- Origin: user.Account,
- })
-
- return user, nil
-}
-
-// TokenForNewUser generates an OAuth Bearer token
-// for a new user (with account) created by Create().
-func (p *Processor) TokenForNewUser(
- ctx context.Context,
- appToken oauth2.TokenInfo,
- app *gtsmodel.Application,
- user *gtsmodel.User,
-) (*apimodel.Token, gtserror.WithCode) {
- // Generate access token.
- accessToken, err := p.oauthServer.GenerateUserAccessToken(
- ctx,
- appToken,
- app.ClientSecret,
- user.ID,
- )
- if err != nil {
- err := fmt.Errorf("error creating new access token for user %s: %w", user.ID, err)
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- return &apimodel.Token{
- AccessToken: accessToken.GetAccess(),
- TokenType: "Bearer",
- Scope: accessToken.GetScope(),
- CreatedAt: accessToken.GetAccessCreateAt().Unix(),
- }, nil
-}
diff --git a/internal/processing/account/delete.go b/internal/processing/account/delete.go
index 3f051edf0..075e94544 100644
--- a/internal/processing/account/delete.go
+++ b/internal/processing/account/delete.go
@@ -95,23 +95,6 @@ func (p *Processor) Delete(
return nil
}
-// DeleteSelf is like Delete, but specifically for local accounts deleting themselves.
-//
-// Calling DeleteSelf results in a delete message being enqueued in the processor,
-// which causes side effects to occur: delete will be federated out to other instances,
-// and the above Delete function will be called afterwards from the processor, to clear
-// out the account's bits and bobs, and stubbify it.
-func (p *Processor) DeleteSelf(ctx context.Context, account *gtsmodel.Account) gtserror.WithCode {
- // Process the delete side effects asynchronously.
- p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
- APObjectType: ap.ActorPerson,
- APActivityType: ap.ActivityDelete,
- Origin: account,
- Target: account,
- })
- return nil
-}
-
// deleteUserAndTokensForAccount deletes the gtsmodel.User and
// any OAuth tokens and applications for the given account.
//
diff --git a/internal/processing/account/update.go b/internal/processing/account/update.go
index 7f2749503..ea6abed6e 100644
--- a/internal/processing/account/update.go
+++ b/internal/processing/account/update.go
@@ -297,7 +297,7 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
}
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
- APObjectType: ap.ObjectProfile,
+ APObjectType: ap.ActorPerson,
APActivityType: ap.ActivityUpdate,
GTSModel: account,
Origin: account,
diff --git a/internal/processing/account/update_test.go b/internal/processing/account/update_test.go
index ad09ff25c..a07562544 100644
--- a/internal/processing/account/update_test.go
+++ b/internal/processing/account/update_test.go
@@ -64,7 +64,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateSimple() {
// Profile update.
suite.Equal(ap.ActivityUpdate, msg.APActivityType)
- suite.Equal(ap.ObjectProfile, msg.APObjectType)
+ suite.Equal(ap.ActorPerson, msg.APObjectType)
// Correct account updated.
if msg.Origin == nil {
@@ -114,7 +114,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateWithMention() {
// Profile update.
suite.Equal(ap.ActivityUpdate, msg.APActivityType)
- suite.Equal(ap.ObjectProfile, msg.APObjectType)
+ suite.Equal(ap.ActorPerson, msg.APObjectType)
// Correct account updated.
if msg.Origin == nil {
@@ -170,7 +170,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateWithMarkdownNote() {
// Profile update.
suite.Equal(ap.ActivityUpdate, msg.APActivityType)
- suite.Equal(ap.ObjectProfile, msg.APObjectType)
+ suite.Equal(ap.ActorPerson, msg.APObjectType)
// Correct account updated.
if msg.Origin == nil {
@@ -255,7 +255,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateWithFields() {
// Profile update.
suite.Equal(ap.ActivityUpdate, msg.APActivityType)
- suite.Equal(ap.ObjectProfile, msg.APObjectType)
+ suite.Equal(ap.ActorPerson, msg.APObjectType)
// Correct account updated.
if msg.Origin == nil {
@@ -312,7 +312,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateNoteNotFields() {
// Profile update.
suite.Equal(ap.ActivityUpdate, msg.APActivityType)
- suite.Equal(ap.ObjectProfile, msg.APObjectType)
+ suite.Equal(ap.ActorPerson, msg.APObjectType)
// Correct account updated.
if msg.Origin == nil {
diff --git a/internal/processing/account_test.go b/internal/processing/account_test.go
deleted file mode 100644
index 82c28115e..000000000
--- a/internal/processing/account_test.go
+++ /dev/null
@@ -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 .
-
-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{})
-}
diff --git a/internal/processing/admin/accountapprove.go b/internal/processing/admin/accountapprove.go
deleted file mode 100644
index c3f6409c3..000000000
--- a/internal/processing/admin/accountapprove.go
+++ /dev/null
@@ -1,79 +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 .
-
-package admin
-
-import (
- "context"
- "errors"
- "fmt"
-
- "github.com/superseriousbusiness/gotosocial/internal/ap"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/messages"
-)
-
-func (p *Processor) AccountApprove(
- ctx context.Context,
- adminAcct *gtsmodel.Account,
- accountID string,
-) (*apimodel.AdminAccountInfo, gtserror.WithCode) {
- user, err := p.state.DB.GetUserByAccountID(ctx, accountID)
- if err != nil && !errors.Is(err, db.ErrNoEntries) {
- err := gtserror.Newf("db error getting user for account id %s: %w", accountID, err)
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- if user == nil {
- err := fmt.Errorf("user for account %s not found", accountID)
- return nil, gtserror.NewErrorNotFound(err, err.Error())
- }
-
- // Get a lock on the account URI,
- // to ensure it's not also being
- // rejected at the same time!
- unlock := p.state.ProcessingLocks.Lock(user.Account.URI)
- defer unlock()
-
- if !*user.Approved {
- // Process approval side effects asynschronously.
- p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
- APObjectType: ap.ActorPerson,
- APActivityType: ap.ActivityAccept,
- GTSModel: user,
- Origin: adminAcct,
- Target: user.Account,
- })
- }
-
- apiAccount, err := p.converter.AccountToAdminAPIAccount(ctx, user.Account)
- if err != nil {
- err := gtserror.Newf("error converting account %s to admin api model: %w", accountID, err)
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- // Optimistically set approved to true and
- // clear sign-up IP to reflect state that
- // will be produced by side effects.
- apiAccount.Approved = true
- apiAccount.IP = nil
-
- return apiAccount, nil
-}
diff --git a/internal/processing/admin/accountapprove_test.go b/internal/processing/admin/accountapprove_test.go
deleted file mode 100644
index b6ca1ed32..000000000
--- a/internal/processing/admin/accountapprove_test.go
+++ /dev/null
@@ -1,75 +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 .
-
-package admin_test
-
-import (
- "context"
- "testing"
-
- "github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/testrig"
-)
-
-type AdminApproveTestSuite struct {
- AdminStandardTestSuite
-}
-
-func (suite *AdminApproveTestSuite) TestApprove() {
- var (
- ctx = context.Background()
- adminAcct = suite.testAccounts["admin_account"]
- targetAcct = suite.testAccounts["unconfirmed_account"]
- targetUser = new(gtsmodel.User)
- )
-
- // Copy user since we're modifying it.
- *targetUser = *suite.testUsers["unconfirmed_account"]
-
- // Approve the sign-up.
- acct, errWithCode := suite.adminProcessor.AccountApprove(
- ctx,
- adminAcct,
- targetAcct.ID,
- )
- if errWithCode != nil {
- suite.FailNow(errWithCode.Error())
- }
-
- // Account should be approved.
- suite.NotNil(acct)
- suite.True(acct.Approved)
- suite.Nil(acct.IP)
-
- // Wait for processor to
- // handle side effects.
- var (
- dbUser *gtsmodel.User
- err error
- )
- if !testrig.WaitFor(func() bool {
- dbUser, err = suite.state.DB.GetUserByID(ctx, targetUser.ID)
- return err == nil && dbUser != nil && *dbUser.Approved
- }) {
- suite.FailNow("waiting for approved user")
- }
-}
-
-func TestAdminApproveTestSuite(t *testing.T) {
- suite.Run(t, new(AdminApproveTestSuite))
-}
diff --git a/internal/processing/admin/accountreject.go b/internal/processing/admin/accountreject.go
deleted file mode 100644
index 8cb54cad6..000000000
--- a/internal/processing/admin/accountreject.go
+++ /dev/null
@@ -1,113 +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 .
-
-package admin
-
-import (
- "context"
- "errors"
- "fmt"
-
- "github.com/superseriousbusiness/gotosocial/internal/ap"
- apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/messages"
-)
-
-func (p *Processor) AccountReject(
- ctx context.Context,
- adminAcct *gtsmodel.Account,
- accountID string,
- privateComment string,
- sendEmail bool,
- message string,
-) (*apimodel.AdminAccountInfo, gtserror.WithCode) {
- user, err := p.state.DB.GetUserByAccountID(ctx, accountID)
- if err != nil && !errors.Is(err, db.ErrNoEntries) {
- err := gtserror.Newf("db error getting user for account id %s: %w", accountID, err)
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- if user == nil {
- err := fmt.Errorf("user for account %s not found", accountID)
- return nil, gtserror.NewErrorNotFound(err, err.Error())
- }
-
- // Get a lock on the account URI,
- // since we're going to be deleting
- // it and its associated user.
- unlock := p.state.ProcessingLocks.Lock(user.Account.URI)
- defer unlock()
-
- // Can't reject an account with a
- // user that's already been approved.
- if *user.Approved {
- err := fmt.Errorf("account %s has already been approved", accountID)
- return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
- }
-
- // Convert to API account *before* doing the
- // rejection, since the rejection will cause
- // the user and account to be removed.
- apiAccount, err := p.converter.AccountToAdminAPIAccount(ctx, user.Account)
- if err != nil {
- err := gtserror.Newf("error converting account %s to admin api model: %w", accountID, err)
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- // Set approved to false on the API model, to
- // reflect the changes that will occur
- // asynchronously in the processor.
- apiAccount.Approved = false
-
- // Ensure we an email address.
- var email string
- if user.Email != "" {
- email = user.Email
- } else {
- email = user.UnconfirmedEmail
- }
-
- // Create a denied user entry for
- // the worker to process + store.
- deniedUser := >smodel.DeniedUser{
- ID: user.ID,
- Email: email,
- Username: user.Account.Username,
- SignUpIP: user.SignUpIP,
- InviteID: user.InviteID,
- Locale: user.Locale,
- CreatedByApplicationID: user.CreatedByApplicationID,
- SignUpReason: user.Reason,
- PrivateComment: privateComment,
- SendEmail: &sendEmail,
- Message: message,
- }
-
- // Process rejection side effects asynschronously.
- p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
- APObjectType: ap.ActorPerson,
- APActivityType: ap.ActivityReject,
- GTSModel: deniedUser,
- Origin: adminAcct,
- Target: user.Account,
- })
-
- return apiAccount, nil
-}
diff --git a/internal/processing/admin/accountreject_test.go b/internal/processing/admin/accountreject_test.go
deleted file mode 100644
index 071401afc..000000000
--- a/internal/processing/admin/accountreject_test.go
+++ /dev/null
@@ -1,142 +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 .
-
-package admin_test
-
-import (
- "context"
- "testing"
-
- "github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/testrig"
-)
-
-type AdminRejectTestSuite struct {
- AdminStandardTestSuite
-}
-
-func (suite *AdminRejectTestSuite) TestReject() {
- var (
- ctx = context.Background()
- adminAcct = suite.testAccounts["admin_account"]
- targetAcct = suite.testAccounts["unconfirmed_account"]
- targetUser = suite.testUsers["unconfirmed_account"]
- privateComment = "It's a no from me chief."
- sendEmail = true
- message = "Too stinky."
- )
-
- acct, errWithCode := suite.adminProcessor.AccountReject(
- ctx,
- adminAcct,
- targetAcct.ID,
- privateComment,
- sendEmail,
- message,
- )
- if errWithCode != nil {
- suite.FailNow(errWithCode.Error())
- }
- suite.NotNil(acct)
- suite.False(acct.Approved)
-
- // Wait for processor to
- // handle side effects.
- var (
- deniedUser *gtsmodel.DeniedUser
- err error
- )
- if !testrig.WaitFor(func() bool {
- deniedUser, err = suite.state.DB.GetDeniedUserByID(ctx, targetUser.ID)
- return deniedUser != nil && err == nil
- }) {
- suite.FailNow("waiting for denied user")
- }
-
- // Ensure fields as expected.
- suite.Equal(targetUser.ID, deniedUser.ID)
- suite.Equal(targetUser.UnconfirmedEmail, deniedUser.Email)
- suite.Equal(targetAcct.Username, deniedUser.Username)
- suite.Equal(targetUser.SignUpIP, deniedUser.SignUpIP)
- suite.Equal(targetUser.InviteID, deniedUser.InviteID)
- suite.Equal(targetUser.Locale, deniedUser.Locale)
- suite.Equal(targetUser.CreatedByApplicationID, deniedUser.CreatedByApplicationID)
- suite.Equal(targetUser.Reason, deniedUser.SignUpReason)
- suite.Equal(privateComment, deniedUser.PrivateComment)
- suite.Equal(sendEmail, *deniedUser.SendEmail)
- suite.Equal(message, deniedUser.Message)
-
- // Should be no user entry for
- // this denied request now.
- _, err = suite.state.DB.GetUserByID(ctx, targetUser.ID)
- suite.ErrorIs(db.ErrNoEntries, err)
-
- // Should be no account entry for
- // this denied request now.
- _, err = suite.state.DB.GetAccountByID(ctx, targetAcct.ID)
- suite.ErrorIs(db.ErrNoEntries, err)
-}
-
-func (suite *AdminRejectTestSuite) TestRejectRemote() {
- var (
- ctx = context.Background()
- adminAcct = suite.testAccounts["admin_account"]
- targetAcct = suite.testAccounts["remote_account_1"]
- privateComment = "It's a no from me chief."
- sendEmail = true
- message = "Too stinky."
- )
-
- // Try to reject a remote account.
- _, err := suite.adminProcessor.AccountReject(
- ctx,
- adminAcct,
- targetAcct.ID,
- privateComment,
- sendEmail,
- message,
- )
- suite.EqualError(err, "user for account 01F8MH5ZK5VRH73AKHQM6Y9VNX not found")
-}
-
-func (suite *AdminRejectTestSuite) TestRejectApproved() {
- var (
- ctx = context.Background()
- adminAcct = suite.testAccounts["admin_account"]
- targetAcct = suite.testAccounts["local_account_1"]
- privateComment = "It's a no from me chief."
- sendEmail = true
- message = "Too stinky."
- )
-
- // Try to reject an already-approved account.
- _, err := suite.adminProcessor.AccountReject(
- ctx,
- adminAcct,
- targetAcct.ID,
- privateComment,
- sendEmail,
- message,
- )
- suite.EqualError(err, "account 01F8MH1H7YV1Z7D2C8K2730QBF has already been approved")
-}
-
-func TestAdminRejectTestSuite(t *testing.T) {
- suite.Run(t, new(AdminRejectTestSuite))
-}
diff --git a/internal/processing/admin/signupapprove.go b/internal/processing/admin/signupapprove.go
new file mode 100644
index 000000000..84e04fa8d
--- /dev/null
+++ b/internal/processing/admin/signupapprove.go
@@ -0,0 +1,82 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package admin
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ "github.com/superseriousbusiness/gotosocial/internal/ap"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/messages"
+)
+
+func (p *Processor) SignupApprove(
+ ctx context.Context,
+ adminAcct *gtsmodel.Account,
+ accountID string,
+) (*apimodel.AdminAccountInfo, gtserror.WithCode) {
+ user, err := p.state.DB.GetUserByAccountID(ctx, accountID)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting user for account id %s: %w", accountID, err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if user == nil {
+ err := fmt.Errorf("user for account %s not found", accountID)
+ return nil, gtserror.NewErrorNotFound(err, err.Error())
+ }
+
+ // Get a lock on the account URI,
+ // to ensure it's not also being
+ // rejected at the same time!
+ unlock := p.state.ProcessingLocks.Lock(user.Account.URI)
+ defer unlock()
+
+ if !*user.Approved {
+ // Process approval side effects asynschronously.
+ 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.ActivityAccept,
+ GTSModel: user,
+ Origin: adminAcct,
+ Target: user.Account,
+ })
+ }
+
+ apiAccount, err := p.converter.AccountToAdminAPIAccount(ctx, user.Account)
+ if err != nil {
+ err := gtserror.Newf("error converting account %s to admin api model: %w", accountID, err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Optimistically set approved to true and
+ // clear sign-up IP to reflect state that
+ // will be produced by side effects.
+ apiAccount.Approved = true
+ apiAccount.IP = nil
+
+ return apiAccount, nil
+}
diff --git a/internal/processing/admin/signupapprove_test.go b/internal/processing/admin/signupapprove_test.go
new file mode 100644
index 000000000..58b8fdade
--- /dev/null
+++ b/internal/processing/admin/signupapprove_test.go
@@ -0,0 +1,75 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package admin_test
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/testrig"
+)
+
+type AdminApproveTestSuite struct {
+ AdminStandardTestSuite
+}
+
+func (suite *AdminApproveTestSuite) TestApprove() {
+ var (
+ ctx = context.Background()
+ adminAcct = suite.testAccounts["admin_account"]
+ targetAcct = suite.testAccounts["unconfirmed_account"]
+ targetUser = new(gtsmodel.User)
+ )
+
+ // Copy user since we're modifying it.
+ *targetUser = *suite.testUsers["unconfirmed_account"]
+
+ // Approve the sign-up.
+ acct, errWithCode := suite.adminProcessor.SignupApprove(
+ ctx,
+ adminAcct,
+ targetAcct.ID,
+ )
+ if errWithCode != nil {
+ suite.FailNow(errWithCode.Error())
+ }
+
+ // Account should be approved.
+ suite.NotNil(acct)
+ suite.True(acct.Approved)
+ suite.Nil(acct.IP)
+
+ // Wait for processor to
+ // handle side effects.
+ var (
+ dbUser *gtsmodel.User
+ err error
+ )
+ if !testrig.WaitFor(func() bool {
+ dbUser, err = suite.state.DB.GetUserByID(ctx, targetUser.ID)
+ return err == nil && dbUser != nil && *dbUser.Approved
+ }) {
+ suite.FailNow("waiting for approved user")
+ }
+}
+
+func TestAdminApproveTestSuite(t *testing.T) {
+ suite.Run(t, new(AdminApproveTestSuite))
+}
diff --git a/internal/processing/admin/signupreject.go b/internal/processing/admin/signupreject.go
new file mode 100644
index 000000000..39eff0b87
--- /dev/null
+++ b/internal/processing/admin/signupreject.go
@@ -0,0 +1,116 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package admin
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ "github.com/superseriousbusiness/gotosocial/internal/ap"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/messages"
+)
+
+func (p *Processor) SignupReject(
+ ctx context.Context,
+ adminAcct *gtsmodel.Account,
+ accountID string,
+ privateComment string,
+ sendEmail bool,
+ message string,
+) (*apimodel.AdminAccountInfo, gtserror.WithCode) {
+ user, err := p.state.DB.GetUserByAccountID(ctx, accountID)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting user for account id %s: %w", accountID, err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if user == nil {
+ err := fmt.Errorf("user for account %s not found", accountID)
+ return nil, gtserror.NewErrorNotFound(err, err.Error())
+ }
+
+ // Get a lock on the account URI,
+ // since we're going to be deleting
+ // it and its associated user.
+ unlock := p.state.ProcessingLocks.Lock(user.Account.URI)
+ defer unlock()
+
+ // Can't reject an account with a
+ // user that's already been approved.
+ if *user.Approved {
+ err := fmt.Errorf("account %s has already been approved", accountID)
+ return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
+ }
+
+ // Convert to API account *before* doing the
+ // rejection, since the rejection will cause
+ // the user and account to be removed.
+ apiAccount, err := p.converter.AccountToAdminAPIAccount(ctx, user.Account)
+ if err != nil {
+ err := gtserror.Newf("error converting account %s to admin api model: %w", accountID, err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Set approved to false on the API model, to
+ // reflect the changes that will occur
+ // asynchronously in the processor.
+ apiAccount.Approved = false
+
+ // Ensure we an email address.
+ var email string
+ if user.Email != "" {
+ email = user.Email
+ } else {
+ email = user.UnconfirmedEmail
+ }
+
+ // Create a denied user entry for
+ // the worker to process + store.
+ deniedUser := >smodel.DeniedUser{
+ ID: user.ID,
+ Email: email,
+ Username: user.Account.Username,
+ SignUpIP: user.SignUpIP,
+ InviteID: user.InviteID,
+ Locale: user.Locale,
+ CreatedByApplicationID: user.CreatedByApplicationID,
+ SignUpReason: user.Reason,
+ PrivateComment: privateComment,
+ SendEmail: &sendEmail,
+ Message: message,
+ }
+
+ // Process rejection side effects asynschronously.
+ 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.ActivityReject,
+ GTSModel: deniedUser,
+ Origin: adminAcct,
+ Target: user.Account,
+ })
+
+ return apiAccount, nil
+}
diff --git a/internal/processing/admin/signupreject_test.go b/internal/processing/admin/signupreject_test.go
new file mode 100644
index 000000000..cb6a25eb3
--- /dev/null
+++ b/internal/processing/admin/signupreject_test.go
@@ -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 .
+
+package admin_test
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/testrig"
+)
+
+type AdminRejectTestSuite struct {
+ AdminStandardTestSuite
+}
+
+func (suite *AdminRejectTestSuite) TestReject() {
+ var (
+ ctx = context.Background()
+ adminAcct = suite.testAccounts["admin_account"]
+ targetAcct = suite.testAccounts["unconfirmed_account"]
+ targetUser = suite.testUsers["unconfirmed_account"]
+ privateComment = "It's a no from me chief."
+ sendEmail = true
+ message = "Too stinky."
+ )
+
+ acct, errWithCode := suite.adminProcessor.SignupReject(
+ ctx,
+ adminAcct,
+ targetAcct.ID,
+ privateComment,
+ sendEmail,
+ message,
+ )
+ if errWithCode != nil {
+ suite.FailNow(errWithCode.Error())
+ }
+ suite.NotNil(acct)
+ suite.False(acct.Approved)
+
+ // Wait for processor to
+ // handle side effects.
+ var (
+ deniedUser *gtsmodel.DeniedUser
+ err error
+ )
+ if !testrig.WaitFor(func() bool {
+ deniedUser, err = suite.state.DB.GetDeniedUserByID(ctx, targetUser.ID)
+ return deniedUser != nil && err == nil
+ }) {
+ suite.FailNow("waiting for denied user")
+ }
+
+ // Ensure fields as expected.
+ suite.Equal(targetUser.ID, deniedUser.ID)
+ suite.Equal(targetUser.UnconfirmedEmail, deniedUser.Email)
+ suite.Equal(targetAcct.Username, deniedUser.Username)
+ suite.Equal(targetUser.SignUpIP, deniedUser.SignUpIP)
+ suite.Equal(targetUser.InviteID, deniedUser.InviteID)
+ suite.Equal(targetUser.Locale, deniedUser.Locale)
+ suite.Equal(targetUser.CreatedByApplicationID, deniedUser.CreatedByApplicationID)
+ suite.Equal(targetUser.Reason, deniedUser.SignUpReason)
+ suite.Equal(privateComment, deniedUser.PrivateComment)
+ suite.Equal(sendEmail, *deniedUser.SendEmail)
+ suite.Equal(message, deniedUser.Message)
+
+ // Should be no user entry for
+ // this denied request now.
+ _, err = suite.state.DB.GetUserByID(ctx, targetUser.ID)
+ suite.ErrorIs(db.ErrNoEntries, err)
+
+ // Should be no account entry for
+ // this denied request now.
+ _, err = suite.state.DB.GetAccountByID(ctx, targetAcct.ID)
+ suite.ErrorIs(db.ErrNoEntries, err)
+}
+
+func (suite *AdminRejectTestSuite) TestRejectRemote() {
+ var (
+ ctx = context.Background()
+ adminAcct = suite.testAccounts["admin_account"]
+ targetAcct = suite.testAccounts["remote_account_1"]
+ privateComment = "It's a no from me chief."
+ sendEmail = true
+ message = "Too stinky."
+ )
+
+ // Try to reject a remote account.
+ _, err := suite.adminProcessor.SignupReject(
+ ctx,
+ adminAcct,
+ targetAcct.ID,
+ privateComment,
+ sendEmail,
+ message,
+ )
+ suite.EqualError(err, "user for account 01F8MH5ZK5VRH73AKHQM6Y9VNX not found")
+}
+
+func (suite *AdminRejectTestSuite) TestRejectApproved() {
+ var (
+ ctx = context.Background()
+ adminAcct = suite.testAccounts["admin_account"]
+ targetAcct = suite.testAccounts["local_account_1"]
+ privateComment = "It's a no from me chief."
+ sendEmail = true
+ message = "Too stinky."
+ )
+
+ // Try to reject an already-approved account.
+ _, err := suite.adminProcessor.SignupReject(
+ ctx,
+ adminAcct,
+ targetAcct.ID,
+ privateComment,
+ sendEmail,
+ message,
+ )
+ suite.EqualError(err, "account 01F8MH1H7YV1Z7D2C8K2730QBF has already been approved")
+}
+
+func TestAdminRejectTestSuite(t *testing.T) {
+ suite.Run(t, new(AdminRejectTestSuite))
+}
diff --git a/internal/processing/processor.go b/internal/processing/processor.go
index 8a18bc45e..1e7997b8f 100644
--- a/internal/processing/processor.go
+++ b/internal/processing/processor.go
@@ -180,13 +180,13 @@ func NewProcessor(
// Start with sub processors that will
// be required by the workers processor.
common := common.New(state, converter, federator, filter)
- processor.account = account.New(&common, state, converter, mediaManager, oauthServer, federator, filter, parseMentionFunc)
+ processor.account = account.New(&common, state, converter, mediaManager, federator, filter, parseMentionFunc)
processor.media = media.New(state, converter, mediaManager, federator.TransportController())
processor.stream = stream.New(state, oauthServer)
// Instantiate the rest of the sub
// processors + pin them to this struct.
- processor.account = account.New(&common, state, converter, mediaManager, oauthServer, federator, filter, parseMentionFunc)
+ processor.account = account.New(&common, state, converter, mediaManager, federator, filter, parseMentionFunc)
processor.admin = admin.New(state, cleaner, converter, mediaManager, federator.TransportController(), emailSender)
processor.fedi = fedi.New(state, &common, converter, federator, filter)
processor.filtersv1 = filtersv1.New(state, converter)
@@ -198,7 +198,7 @@ func NewProcessor(
processor.timeline = timeline.New(state, converter, filter)
processor.search = search.New(state, federator, converter, filter)
processor.status = status.New(state, &common, &processor.polls, federator, converter, filter, parseMentionFunc)
- processor.user = user.New(state, emailSender)
+ processor.user = user.New(state, converter, oauthServer, emailSender)
// Workers processor handles asynchronous
// worker jobs; instantiate it separately
diff --git a/internal/processing/report/create.go b/internal/processing/report/create.go
index cac600006..dd31a8798 100644
--- a/internal/processing/report/create.go
+++ b/internal/processing/report/create.go
@@ -92,7 +92,7 @@ func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form
}
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
- APObjectType: ap.ObjectProfile,
+ APObjectType: ap.ActorPerson,
APActivityType: ap.ActivityFlag,
GTSModel: report,
Origin: account,
diff --git a/internal/processing/user/create.go b/internal/processing/user/create.go
new file mode 100644
index 000000000..0d848583e
--- /dev/null
+++ b/internal/processing/user/create.go
@@ -0,0 +1,167 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+package user
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "github.com/superseriousbusiness/gotosocial/internal/ap"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/messages"
+ "github.com/superseriousbusiness/gotosocial/internal/text"
+ "github.com/superseriousbusiness/oauth2/v4"
+)
+
+// Create processes the given form for creating a new user+account.
+//
+// App should be the app used to create the user+account.
+// If nil, the instance app will be used.
+//
+// Precondition: the form's fields should have already been
+// validated and normalized by the caller.
+func (p *Processor) Create(
+ ctx context.Context,
+ app *gtsmodel.Application,
+ form *apimodel.AccountCreateRequest,
+) (*gtsmodel.User, gtserror.WithCode) {
+ const (
+ usersPerDay = 10
+ regBacklog = 20
+ )
+
+ // Ensure no more than usersPerDay
+ // have registered in the last 24h.
+ newUsersCount, err := p.state.DB.CountApprovedSignupsSince(ctx, time.Now().Add(-24*time.Hour))
+ if err != nil {
+ err := fmt.Errorf("db error counting new users: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if newUsersCount >= usersPerDay {
+ err := fmt.Errorf("this instance has hit its limit of new sign-ups for today; you can try again tomorrow")
+ return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
+ }
+
+ // Ensure the new users backlog isn't full.
+ backlogLen, err := p.state.DB.CountUnhandledSignups(ctx)
+ if err != nil {
+ err := fmt.Errorf("db error counting registration backlog length: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if backlogLen >= regBacklog {
+ err := fmt.Errorf("this instance's sign-up backlog is currently full; you must wait until pending sign-ups are handled by the admin(s)")
+ return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
+ }
+
+ emailAvailable, err := p.state.DB.IsEmailAvailable(ctx, form.Email)
+ if err != nil {
+ err := fmt.Errorf("db error checking email availability: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ if !emailAvailable {
+ err := fmt.Errorf("email address %s is not available", form.Email)
+ return nil, gtserror.NewErrorConflict(err, err.Error())
+ }
+
+ usernameAvailable, err := p.state.DB.IsUsernameAvailable(ctx, form.Username)
+ if err != nil {
+ err := fmt.Errorf("db error checking username availability: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ if !usernameAvailable {
+ err := fmt.Errorf("username %s is not available", form.Username)
+ return nil, gtserror.NewErrorConflict(err, err.Error())
+ }
+
+ // Only store reason if one is required.
+ var reason string
+ if config.GetAccountsReasonRequired() {
+ reason = form.Reason
+ }
+
+ // Use instance app if no app provided.
+ if app == nil {
+ app, err = p.state.DB.GetInstanceApplication(ctx)
+ if err != nil {
+ err := fmt.Errorf("db error getting instance app: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+ }
+
+ user, err := p.state.DB.NewSignup(ctx, gtsmodel.NewSignup{
+ Username: form.Username,
+ Email: form.Email,
+ Password: form.Password,
+ Reason: text.SanitizeToPlaintext(reason),
+ SignUpIP: form.IP,
+ Locale: form.Locale,
+ AppID: app.ID,
+ })
+ if err != nil {
+ err := fmt.Errorf("db error creating new signup: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // There are side effects for creating a new user+account
+ // (confirmation emails etc), perform these async.
+ p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
+ // Use ap.ObjectProfile here to
+ // distinguish this message (user model)
+ // from ap.ActorPerson (account model).
+ APObjectType: ap.ObjectProfile,
+ APActivityType: ap.ActivityCreate,
+ GTSModel: user,
+ Origin: user.Account,
+ })
+
+ return user, nil
+}
+
+// TokenForNewUser generates an OAuth Bearer token
+// for a new user (with account) created by Create().
+func (p *Processor) TokenForNewUser(
+ ctx context.Context,
+ appToken oauth2.TokenInfo,
+ app *gtsmodel.Application,
+ user *gtsmodel.User,
+) (*apimodel.Token, gtserror.WithCode) {
+ // Generate access token.
+ accessToken, err := p.oauthServer.GenerateUserAccessToken(
+ ctx,
+ appToken,
+ app.ClientSecret,
+ user.ID,
+ )
+ if err != nil {
+ err := fmt.Errorf("error creating new access token for user %s: %w", user.ID, err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return &apimodel.Token{
+ AccessToken: accessToken.GetAccess(),
+ TokenType: "Bearer",
+ Scope: accessToken.GetScope(),
+ CreatedAt: accessToken.GetAccessCreateAt().Unix(),
+ }, nil
+}
diff --git a/internal/processing/user/delete.go b/internal/processing/user/delete.go
new file mode 100644
index 000000000..9783010ef
--- /dev/null
+++ b/internal/processing/user/delete.go
@@ -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 .
+
+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
+}
diff --git a/internal/processing/user/email.go b/internal/processing/user/email.go
index 2b27c6c92..ea9dbb64c 100644
--- a/internal/processing/user/email.go
+++ b/internal/processing/user/email.go
@@ -23,11 +23,92 @@ import (
"fmt"
"time"
+ "github.com/superseriousbusiness/gotosocial/internal/ap"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/messages"
+ "github.com/superseriousbusiness/gotosocial/internal/validate"
+ "golang.org/x/crypto/bcrypt"
)
+// EmailChange processes an email address change request for the given user.
+func (p *Processor) EmailChange(
+ ctx context.Context,
+ user *gtsmodel.User,
+ password string,
+ newEmail string,
+) (*apimodel.User, gtserror.WithCode) {
+ // Ensure provided password is correct.
+ if err := bcrypt.CompareHashAndPassword([]byte(user.EncryptedPassword), []byte(password)); err != nil {
+ err := gtserror.Newf("%w", err)
+ return nil, gtserror.NewErrorUnauthorized(err, "password was incorrect")
+ }
+
+ // Ensure new email address is valid.
+ if err := validate.Email(newEmail); err != nil {
+ return nil, gtserror.NewErrorBadRequest(err, err.Error())
+ }
+
+ // Ensure new email address is different
+ // from current email address.
+ if newEmail == user.Email {
+ const help = "new email address cannot be the same as current email address"
+ err := gtserror.New(help)
+ return nil, gtserror.NewErrorBadRequest(err, help)
+ }
+
+ if newEmail == user.UnconfirmedEmail {
+ const help = "you already have an email change request pending for given email address"
+ err := gtserror.New(help)
+ return nil, gtserror.NewErrorBadRequest(err, help)
+ }
+
+ // Ensure this address isn't already used by another account.
+ emailAvailable, err := p.state.DB.IsEmailAvailable(ctx, newEmail)
+ if err != nil {
+ err := gtserror.Newf("db error checking email availability: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if !emailAvailable {
+ const help = "new email address is already in use on this instance"
+ err := gtserror.New(help)
+ return nil, gtserror.NewErrorConflict(err, help)
+ }
+
+ // Set new email address on user.
+ user.UnconfirmedEmail = newEmail
+ if err := p.state.DB.UpdateUser(
+ ctx, user,
+ "unconfirmed_email",
+ ); err != nil {
+ err := gtserror.Newf("db error updating user: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Ensure user populated (we need account).
+ if err := p.state.DB.PopulateUser(ctx, user); err != nil {
+ err := gtserror.Newf("db error populating user: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Add email sending job to the queue.
+ p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
+ // Use ap.ObjectProfile here to
+ // distinguish this message (user model)
+ // from ap.ActorPerson (account model).
+ APObjectType: ap.ObjectProfile,
+ APActivityType: ap.ActivityUpdate,
+ GTSModel: user,
+ Origin: user.Account,
+ Target: user.Account,
+ })
+
+ return p.converter.UserToAPIUser(ctx, user), nil
+}
+
// EmailGetUserForConfirmToken retrieves the user (with account) from
// the database for the given "confirm your email" token string.
func (p *Processor) EmailGetUserForConfirmToken(ctx context.Context, token string) (*gtsmodel.User, gtserror.WithCode) {
diff --git a/internal/processing/user/get.go b/internal/processing/user/get.go
new file mode 100644
index 000000000..9b19189a8
--- /dev/null
+++ b/internal/processing/user/get.go
@@ -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 .
+
+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
+}
diff --git a/internal/processing/user/user.go b/internal/processing/user/user.go
index 2fbb9c888..cd8ab9900 100644
--- a/internal/processing/user/user.go
+++ b/internal/processing/user/user.go
@@ -19,18 +19,28 @@ package user
import (
"github.com/superseriousbusiness/gotosocial/internal/email"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/state"
+ "github.com/superseriousbusiness/gotosocial/internal/typeutils"
)
type Processor struct {
state *state.State
+ converter *typeutils.Converter
+ oauthServer oauth.Server
emailSender email.Sender
}
-// New returns a new user processor
-func New(state *state.State, emailSender email.Sender) Processor {
+// New returns a new user processor.
+func New(
+ state *state.State,
+ converter *typeutils.Converter,
+ oauthServer oauth.Server,
+ emailSender email.Sender,
+) Processor {
return Processor{
state: state,
+ converter: converter,
emailSender: emailSender,
}
}
diff --git a/internal/processing/user/user_test.go b/internal/processing/user/user_test.go
index 61e8f8b05..e473c5bb0 100644
--- a/internal/processing/user/user_test.go
+++ b/internal/processing/user/user_test.go
@@ -24,6 +24,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/processing/user"
"github.com/superseriousbusiness/gotosocial/internal/state"
+ "github.com/superseriousbusiness/gotosocial/internal/typeutils"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@@ -53,7 +54,7 @@ func (suite *UserStandardTestSuite) SetupTest() {
suite.emailSender = testrig.NewEmailSender("../../../web/template/", suite.sentEmails)
suite.testUsers = testrig.NewTestUsers()
- suite.user = user.New(&suite.state, suite.emailSender)
+ suite.user = user.New(&suite.state, typeutils.NewConverter(&suite.state), testrig.NewTestOauthServer(suite.db), suite.emailSender)
testrig.StandardDBSetup(suite.db, nil)
}
diff --git a/internal/processing/workers/fromclientapi.go b/internal/processing/workers/fromclientapi.go
index a9e33892f..89b8f546f 100644
--- a/internal/processing/workers/fromclientapi.go
+++ b/internal/processing/workers/fromclientapi.go
@@ -71,9 +71,9 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg *messages.Fro
case ap.ActivityCreate:
switch cMsg.APObjectType {
- // CREATE PROFILE/ACCOUNT
- case ap.ObjectProfile, ap.ActorPerson:
- return p.clientAPI.CreateAccount(ctx, cMsg)
+ // CREATE USER (ie., new user+account sign-up)
+ case ap.ObjectProfile:
+ return p.clientAPI.CreateUser(ctx, cMsg)
// CREATE NOTE/STATUS
case ap.ObjectNote:
@@ -111,13 +111,17 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg *messages.Fro
case ap.ObjectNote:
return p.clientAPI.UpdateStatus(ctx, cMsg)
- // UPDATE PROFILE/ACCOUNT
- case ap.ObjectProfile, ap.ActorPerson:
+ // UPDATE ACCOUNT (ie., bio, settings, etc)
+ case ap.ActorPerson:
return p.clientAPI.UpdateAccount(ctx, cMsg)
// UPDATE A FLAG/REPORT (mark as resolved/closed)
case ap.ActivityFlag:
return p.clientAPI.UpdateReport(ctx, cMsg)
+
+ // UPDATE USER (ie., email address)
+ case ap.ObjectProfile:
+ return p.clientAPI.UpdateUser(ctx, cMsg)
}
// ACCEPT SOMETHING
@@ -128,9 +132,9 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg *messages.Fro
case ap.ActivityFollow:
return p.clientAPI.AcceptFollow(ctx, cMsg)
- // ACCEPT PROFILE/ACCOUNT (sign-up)
- case ap.ObjectProfile, ap.ActorPerson:
- return p.clientAPI.AcceptAccount(ctx, cMsg)
+ // ACCEPT USER (ie., new user+account sign-up)
+ case ap.ObjectProfile:
+ return p.clientAPI.AcceptUser(ctx, cMsg)
}
// REJECT SOMETHING
@@ -141,9 +145,9 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg *messages.Fro
case ap.ActivityFollow:
return p.clientAPI.RejectFollowRequest(ctx, cMsg)
- // REJECT PROFILE/ACCOUNT (sign-up)
- case ap.ObjectProfile, ap.ActorPerson:
- return p.clientAPI.RejectAccount(ctx, cMsg)
+ // REJECT USER (ie., new user+account sign-up)
+ case ap.ObjectProfile:
+ return p.clientAPI.RejectUser(ctx, cMsg)
}
// UNDO SOMETHING
@@ -175,17 +179,17 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg *messages.Fro
case ap.ObjectNote:
return p.clientAPI.DeleteStatus(ctx, cMsg)
- // DELETE PROFILE/ACCOUNT
- case ap.ObjectProfile, ap.ActorPerson:
- return p.clientAPI.DeleteAccount(ctx, cMsg)
+ // DELETE REMOTE ACCOUNT or LOCAL USER+ACCOUNT
+ case ap.ActorPerson, ap.ObjectProfile:
+ return p.clientAPI.DeleteAccountOrUser(ctx, cMsg)
}
// FLAG/REPORT SOMETHING
case ap.ActivityFlag:
switch cMsg.APObjectType { //nolint:gocritic
- // FLAG/REPORT A PROFILE
- case ap.ObjectProfile:
+ // FLAG/REPORT ACCOUNT
+ case ap.ActorPerson:
return p.clientAPI.ReportAccount(ctx, cMsg)
}
@@ -193,8 +197,8 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg *messages.Fro
case ap.ActivityMove:
switch cMsg.APObjectType { //nolint:gocritic
- // MOVE PROFILE/ACCOUNT
- case ap.ObjectProfile, ap.ActorPerson:
+ // MOVE ACCOUNT
+ case ap.ActorPerson:
return p.clientAPI.MoveAccount(ctx, cMsg)
}
}
@@ -202,7 +206,7 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg *messages.Fro
return gtserror.Newf("unhandled: %s %s", cMsg.APActivityType, cMsg.APObjectType)
}
-func (p *clientAPI) CreateAccount(ctx context.Context, cMsg *messages.FromClientAPI) error {
+func (p *clientAPI) CreateUser(ctx context.Context, cMsg *messages.FromClientAPI) error {
newUser, ok := cMsg.GTSModel.(*gtsmodel.User)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.User", cMsg.GTSModel)
@@ -219,7 +223,7 @@ func (p *clientAPI) CreateAccount(ctx context.Context, cMsg *messages.FromClient
}
// Send "please confirm your address" email to the new user.
- if err := p.surface.emailUserPleaseConfirm(ctx, newUser); err != nil {
+ if err := p.surface.emailUserPleaseConfirm(ctx, newUser, true); err != nil {
log.Errorf(ctx, "error emailing confirm: %v", err)
}
@@ -479,6 +483,22 @@ func (p *clientAPI) UpdateReport(ctx context.Context, cMsg *messages.FromClientA
return nil
}
+func (p *clientAPI) UpdateUser(ctx context.Context, cMsg *messages.FromClientAPI) error {
+ user, ok := cMsg.GTSModel.(*gtsmodel.User)
+ if !ok {
+ return gtserror.Newf("cannot cast %T -> *gtsmodel.User", cMsg.GTSModel)
+ }
+
+ // The only possible "UpdateUser" action is to update the
+ // user's email address, so we can safely assume by this
+ // point that a new unconfirmed email address has been set.
+ if err := p.surface.emailUserPleaseConfirm(ctx, user, false); err != nil {
+ log.Errorf(ctx, "error emailing report closed: %v", err)
+ }
+
+ return nil
+}
+
func (p *clientAPI) AcceptFollow(ctx context.Context, cMsg *messages.FromClientAPI) error {
follow, ok := cMsg.GTSModel.(*gtsmodel.Follow)
if !ok {
@@ -669,7 +689,7 @@ func (p *clientAPI) DeleteStatus(ctx context.Context, cMsg *messages.FromClientA
return nil
}
-func (p *clientAPI) DeleteAccount(ctx context.Context, cMsg *messages.FromClientAPI) error {
+func (p *clientAPI) DeleteAccountOrUser(ctx context.Context, cMsg *messages.FromClientAPI) error {
// The originID of the delete, one of:
// - ID of a domain block, for which
// this account delete is a side effect.
@@ -768,7 +788,7 @@ func (p *clientAPI) MoveAccount(ctx context.Context, cMsg *messages.FromClientAP
return nil
}
-func (p *clientAPI) AcceptAccount(ctx context.Context, cMsg *messages.FromClientAPI) error {
+func (p *clientAPI) AcceptUser(ctx context.Context, cMsg *messages.FromClientAPI) error {
newUser, ok := cMsg.GTSModel.(*gtsmodel.User)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.User", cMsg.GTSModel)
@@ -791,7 +811,7 @@ func (p *clientAPI) AcceptAccount(ctx context.Context, cMsg *messages.FromClient
return nil
}
-func (p *clientAPI) RejectAccount(ctx context.Context, cMsg *messages.FromClientAPI) error {
+func (p *clientAPI) RejectUser(ctx context.Context, cMsg *messages.FromClientAPI) error {
deniedUser, ok := cMsg.GTSModel.(*gtsmodel.DeniedUser)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.DeniedUser", cMsg.GTSModel)
diff --git a/internal/processing/workers/fromfediapi.go b/internal/processing/workers/fromfediapi.go
index 49756a47a..ac4003f6a 100644
--- a/internal/processing/workers/fromfediapi.go
+++ b/internal/processing/workers/fromfediapi.go
@@ -115,8 +115,8 @@ func (p *Processor) ProcessFromFediAPI(ctx context.Context, fMsg *messages.FromF
case ap.ObjectNote:
return p.fediAPI.UpdateStatus(ctx, fMsg)
- // UPDATE PROFILE/ACCOUNT
- case ap.ObjectProfile:
+ // UPDATE ACCOUNT
+ case ap.ActorPerson:
return p.fediAPI.UpdateAccount(ctx, fMsg)
}
@@ -137,17 +137,17 @@ func (p *Processor) ProcessFromFediAPI(ctx context.Context, fMsg *messages.FromF
case ap.ObjectNote:
return p.fediAPI.DeleteStatus(ctx, fMsg)
- // DELETE PROFILE/ACCOUNT
- case ap.ObjectProfile:
+ // DELETE ACCOUNT
+ case ap.ActorPerson:
return p.fediAPI.DeleteAccount(ctx, fMsg)
}
// MOVE SOMETHING
case ap.ActivityMove:
- // MOVE PROFILE/ACCOUNT
+ // MOVE ACCOUNT
// fromfediapi_move.go.
- if fMsg.APObjectType == ap.ObjectProfile {
+ if fMsg.APObjectType == ap.ActorPerson {
return p.fediAPI.MoveAccount(ctx, fMsg)
}
}
diff --git a/internal/processing/workers/fromfediapi_test.go b/internal/processing/workers/fromfediapi_test.go
index 8429fe17c..e69e2c7a8 100644
--- a/internal/processing/workers/fromfediapi_test.go
+++ b/internal/processing/workers/fromfediapi_test.go
@@ -337,7 +337,7 @@ func (suite *FromFediAPITestSuite) TestProcessAccountDelete() {
// now they are mufos!
err = testStructs.Processor.Workers().ProcessFromFediAPI(ctx, &messages.FromFediAPI{
- APObjectType: ap.ObjectProfile,
+ APObjectType: ap.ActorPerson,
APActivityType: ap.ActivityDelete,
GTSModel: deletedAccount,
Receiving: receivingAccount,
@@ -613,7 +613,7 @@ func (suite *FromFediAPITestSuite) TestMoveAccount() {
// Process the Move.
err := testStructs.Processor.Workers().ProcessFromFediAPI(ctx, &messages.FromFediAPI{
- APObjectType: ap.ObjectProfile,
+ APObjectType: ap.ActorPerson,
APActivityType: ap.ActivityMove,
GTSModel: >smodel.Move{
OriginURI: requestingAcct.URI,
diff --git a/internal/processing/workers/surfaceemail.go b/internal/processing/workers/surfaceemail.go
index 5f8ae1823..d0a40e6ba 100644
--- a/internal/processing/workers/surfaceemail.go
+++ b/internal/processing/workers/surfaceemail.go
@@ -74,7 +74,10 @@ func (s *Surface) emailUserReportClosed(ctx context.Context, report *gtsmodel.Re
// emailUserPleaseConfirm emails the given user
// to ask them to confirm their email address.
-func (s *Surface) emailUserPleaseConfirm(ctx context.Context, user *gtsmodel.User) error {
+//
+// If newSignup is true, template will be geared
+// towards someone who just created an account.
+func (s *Surface) emailUserPleaseConfirm(ctx context.Context, user *gtsmodel.User, newSignup bool) error {
if user.UnconfirmedEmail == "" ||
user.UnconfirmedEmail == user.Email {
// User has already confirmed this
@@ -104,6 +107,7 @@ func (s *Surface) emailUserPleaseConfirm(ctx context.Context, user *gtsmodel.Use
InstanceURL: instance.URI,
InstanceName: instance.Title,
ConfirmLink: confirmLink,
+ NewSignup: newSignup,
},
); err != nil {
return err
diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go
index 68db61128..e1380fc9e 100644
--- a/internal/typeutils/internaltofrontend.go
+++ b/internal/typeutils/internaltofrontend.go
@@ -63,6 +63,44 @@ func toMastodonVersion(in string) string {
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
// 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.
diff --git a/internal/web/signup.go b/internal/web/signup.go
index 691469dff..bc30749f8 100644
--- a/internal/web/signup.go
+++ b/internal/web/signup.go
@@ -108,9 +108,9 @@ func (m *Module) signupPOSTHandler(c *gin.Context) {
}
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).
- user, errWithCode := m.processor.Account().Create(
+ user, errWithCode := m.processor.User().Create(
c.Request.Context(),
// nil to use
// instance app.
--
cgit v1.2.3