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) +} | 
