diff options
author | 2024-06-06 15:43:25 +0200 | |
---|---|---|
committer | 2024-06-06 14:43:25 +0100 | |
commit | bcda048eab799284fc46d74706334bf9ef76dc83 (patch) | |
tree | c4595fe5e6e6fd570d59cee7095a336f2e884344 /internal/api/client | |
parent | drop date (#2969) (diff) | |
download | gotosocial-bcda048eab799284fc46d74706334bf9ef76dc83.tar.xz |
[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
Diffstat (limited to 'internal/api/client')
-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 |
10 files changed, 406 insertions, 99 deletions
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) +} |