diff options
Diffstat (limited to 'internal/api')
-rw-r--r-- | internal/api/activitypub/users/userget_test.go | 2 | ||||
-rw-r--r-- | internal/api/client/accounts/accountcreate.go | 6 | ||||
-rw-r--r-- | internal/api/client/accounts/accountdelete.go | 2 | ||||
-rw-r--r-- | internal/api/client/admin/accountapprove.go | 2 | ||||
-rw-r--r-- | internal/api/client/admin/accountreject.go | 2 | ||||
-rw-r--r-- | internal/api/client/user/emailchange.go | 104 | ||||
-rw-r--r-- | internal/api/client/user/emailchange_test.go | 142 | ||||
-rw-r--r-- | internal/api/client/user/passwordchange_test.go | 122 | ||||
-rw-r--r-- | internal/api/client/user/user.go | 4 | ||||
-rw-r--r-- | internal/api/client/user/user_test.go | 43 | ||||
-rw-r--r-- | internal/api/client/user/userget.go | 78 | ||||
-rw-r--r-- | internal/api/model/user.go | 61 |
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 := >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 <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"` +} |