summaryrefslogtreecommitdiff
path: root/internal/api
diff options
context:
space:
mode:
Diffstat (limited to 'internal/api')
-rw-r--r--internal/api/activitypub/users/userget_test.go2
-rw-r--r--internal/api/client/accounts/accountcreate.go6
-rw-r--r--internal/api/client/accounts/accountdelete.go2
-rw-r--r--internal/api/client/admin/accountapprove.go2
-rw-r--r--internal/api/client/admin/accountreject.go2
-rw-r--r--internal/api/client/user/emailchange.go104
-rw-r--r--internal/api/client/user/emailchange_test.go142
-rw-r--r--internal/api/client/user/passwordchange_test.go122
-rw-r--r--internal/api/client/user/user.go4
-rw-r--r--internal/api/client/user/user_test.go43
-rw-r--r--internal/api/client/user/userget.go78
-rw-r--r--internal/api/model/user.go61
12 files changed, 468 insertions, 100 deletions
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 <http://www.gnu.org/licenses/>.
+
+package user
+
+import (
+ "errors"
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+)
+
+// EmailChangePOSTHandler swagger:operation POST /api/v1/user/email_change userEmailChange
+//
+// Request changing the email address of authenticated user.
+//
+// ---
+// tags:
+// - user
+//
+// consumes:
+// - application/json
+// - application/xml
+// - application/x-www-form-urlencoded
+//
+// produces:
+// - application/json
+//
+// security:
+// - OAuth2 Bearer:
+// - write:user
+//
+// responses:
+// '202':
+// description: "Accepted: email change is processing; check your inbox to confirm new address."
+// schema:
+// "$ref": "#/definitions/user"
+// '400':
+// description: bad request
+// '401':
+// description: unauthorized
+// '403':
+// description: forbidden
+// '406':
+// description: not acceptable
+// '409':
+// description: "Conflict: desired email address already in use"
+// '500':
+// description: internal error
+func (m *Module) EmailChangePOSTHandler(c *gin.Context) {
+ authed, err := oauth.Authed(c, true, true, true, true)
+ if err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ form := &apimodel.EmailChangeRequest{}
+ if err := c.ShouldBind(form); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if form.Password == "" {
+ err := errors.New("email change request missing field password")
+ apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ user, errWithCode := m.processor.User().EmailChange(
+ c.Request.Context(),
+ authed.User,
+ form.Password,
+ form.NewEmail,
+ )
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ apiutil.JSON(c, http.StatusAccepted, user)
+}
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 <http://www.gnu.org/licenses/>.
+
+package user_test
+
+import (
+ "encoding/json"
+ "io"
+ "net/http"
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+ "github.com/superseriousbusiness/gotosocial/internal/api/client/user"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/state"
+ "github.com/superseriousbusiness/gotosocial/testrig"
+)
+
+type EmailChangeTestSuite struct {
+ UserStandardTestSuite
+}
+
+func (suite *EmailChangeTestSuite) TestEmailChangePOST() {
+ // Get a new processor for this test, as
+ // we're expecting an email, and we don't
+ // want the other tests interfering if
+ // we're running them at the same time.
+ state := new(state.State)
+ state.DB = testrig.NewTestDB(&suite.state)
+ storage := testrig.NewInMemoryStorage()
+ sentEmails := make(map[string]string)
+ emailSender := testrig.NewEmailSender("../../../../web/template/", sentEmails)
+ processor := testrig.NewTestProcessor(state, suite.federator, emailSender, suite.mediaManager)
+ testrig.StartWorkers(state, processor.Workers())
+ userModule := user.New(processor)
+ testrig.StandardDBSetup(state.DB, suite.testAccounts)
+ testrig.StandardStorageSetup(storage, "../../../../testrig/media")
+
+ defer func() {
+ testrig.StandardDBTeardown(state.DB)
+ testrig.StandardStorageTeardown(storage)
+ testrig.StopWorkers(state)
+ }()
+
+ response, code := suite.POST(user.EmailChangePath, map[string][]string{
+ "password": {"password"},
+ "new_email": {"someone@example.org"},
+ }, userModule.EmailChangePOSTHandler)
+ defer response.Body.Close()
+
+ // Check response
+ suite.EqualValues(http.StatusAccepted, code)
+ b, err := io.ReadAll(response.Body)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ apiUser := new(apimodel.User)
+ if err := json.Unmarshal(b, apiUser); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Unconfirmed email should be set now.
+ suite.Equal("someone@example.org", apiUser.UnconfirmedEmail)
+
+ // Ensure unconfirmed address gets an email.
+ if !testrig.WaitFor(func() bool {
+ _, ok := sentEmails["someone@example.org"]
+ return ok
+ }) {
+ suite.FailNow("no email received")
+ }
+}
+
+func (suite *EmailChangeTestSuite) TestEmailChangePOSTAddressInUse() {
+ response, code := suite.POST(user.EmailChangePath, map[string][]string{
+ "password": {"password"},
+ "new_email": {"admin@example.org"},
+ }, suite.userModule.EmailChangePOSTHandler)
+ defer response.Body.Close()
+
+ // Check response
+ suite.EqualValues(http.StatusConflict, code)
+ b, err := io.ReadAll(response.Body)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ suite.Equal(`{"error":"Conflict: new email address is already in use on this instance"}`, string(b))
+}
+
+func (suite *EmailChangeTestSuite) TestEmailChangePOSTSameEmail() {
+ response, code := suite.POST(user.EmailChangePath, map[string][]string{
+ "password": {"password"},
+ "new_email": {"zork@example.org"},
+ }, suite.userModule.EmailChangePOSTHandler)
+ defer response.Body.Close()
+
+ // Check response
+ suite.EqualValues(http.StatusBadRequest, code)
+ b, err := io.ReadAll(response.Body)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ suite.Equal(`{"error":"Bad Request: new email address cannot be the same as current email address"}`, string(b))
+}
+
+func (suite *EmailChangeTestSuite) TestEmailChangePOSTBadPassword() {
+ response, code := suite.POST(user.EmailChangePath, map[string][]string{
+ "password": {"notmypassword"},
+ "new_email": {"someone@example.org"},
+ }, suite.userModule.EmailChangePOSTHandler)
+ defer response.Body.Close()
+
+ // Check response
+ suite.EqualValues(http.StatusUnauthorized, code)
+ b, err := io.ReadAll(response.Body)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ suite.Equal(`{"error":"Unauthorized: password was incorrect"}`, string(b))
+}
+
+func TestEmailChangeTestSuite(t *testing.T) {
+ suite.Run(t, &EmailChangeTestSuite{})
+}
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 := &gtsmodel.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 <http://www.gnu.org/licenses/>.
+
+package user
+
+import (
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+)
+
+// UserGETHandler swagger:operation GET /api/v1/user getUser
+//
+// Get your own user model.
+//
+// ---
+// tags:
+// - user
+//
+// produces:
+// - application/json
+//
+// security:
+// - OAuth2 Bearer:
+// - read:user
+//
+// responses:
+// '200':
+// description: The requested user.
+// schema:
+// "$ref": "#/definitions/user"
+// '400':
+// description: bad request
+// '401':
+// description: unauthorized
+// '403':
+// description: forbidden
+// '406':
+// description: not acceptable
+// '500':
+// description: internal error
+func (m *Module) UserGETHandler(c *gin.Context) {
+ authed, err := oauth.Authed(c, true, true, true, true)
+ if err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ user, errWithCode := m.processor.User().Get(c.Request.Context(), authed.User)
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ apiutil.JSON(c, http.StatusOK, user)
+}
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"`
+}