From b6fbdc66c1ce1ec61ebfb6fcc0351ea627a1d288 Mon Sep 17 00:00:00 2001
From: tobi <31960611+tsmethurst@users.noreply.github.com>
Date: Wed, 22 Feb 2023 16:05:26 +0100
Subject: [chore] Deinterface processor and subprocessors (#1501)
* [chore] Deinterface processor and subprocessors
* expose subprocessors via function calls
* missing license header
---
internal/processing/user/changepassword.go | 51 ---------
internal/processing/user/changepassword_test.go | 92 ----------------
internal/processing/user/email.go | 137 ++++++++++++++++++++++++
internal/processing/user/email_test.go | 116 ++++++++++++++++++++
internal/processing/user/emailconfirm.go | 134 -----------------------
internal/processing/user/emailconfirm_test.go | 116 --------------------
internal/processing/user/password.go | 52 +++++++++
internal/processing/user/password_test.go | 92 ++++++++++++++++
internal/processing/user/user.go | 19 +---
9 files changed, 399 insertions(+), 410 deletions(-)
delete mode 100644 internal/processing/user/changepassword.go
delete mode 100644 internal/processing/user/changepassword_test.go
create mode 100644 internal/processing/user/email.go
create mode 100644 internal/processing/user/email_test.go
delete mode 100644 internal/processing/user/emailconfirm.go
delete mode 100644 internal/processing/user/emailconfirm_test.go
create mode 100644 internal/processing/user/password.go
create mode 100644 internal/processing/user/password_test.go
(limited to 'internal/processing/user')
diff --git a/internal/processing/user/changepassword.go b/internal/processing/user/changepassword.go
deleted file mode 100644
index 03b8c4525..000000000
--- a/internal/processing/user/changepassword.go
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-*/
-
-package user
-
-import (
- "context"
-
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/validate"
- "golang.org/x/crypto/bcrypt"
-)
-
-func (p *processor) ChangePassword(ctx context.Context, user *gtsmodel.User, oldPassword string, newPassword string) gtserror.WithCode {
- if err := bcrypt.CompareHashAndPassword([]byte(user.EncryptedPassword), []byte(oldPassword)); err != nil {
- return gtserror.NewErrorUnauthorized(err, "old password was incorrect")
- }
-
- if err := validate.NewPassword(newPassword); err != nil {
- return gtserror.NewErrorBadRequest(err, err.Error())
- }
-
- newPasswordHash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
- if err != nil {
- return gtserror.NewErrorInternalError(err, "error hashing password")
- }
-
- user.EncryptedPassword = string(newPasswordHash)
-
- if err := p.db.UpdateUser(ctx, user, "encrypted_password"); err != nil {
- return gtserror.NewErrorInternalError(err)
- }
-
- return nil
-}
diff --git a/internal/processing/user/changepassword_test.go b/internal/processing/user/changepassword_test.go
deleted file mode 100644
index 74676b323..000000000
--- a/internal/processing/user/changepassword_test.go
+++ /dev/null
@@ -1,92 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-*/
-
-package user_test
-
-import (
- "context"
- "net/http"
- "testing"
-
- "github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "golang.org/x/crypto/bcrypt"
-)
-
-type ChangePasswordTestSuite struct {
- UserStandardTestSuite
-}
-
-func (suite *ChangePasswordTestSuite) TestChangePasswordOK() {
- user := suite.testUsers["local_account_1"]
-
- errWithCode := suite.user.ChangePassword(context.Background(), user, "password", "verygoodnewpassword")
- suite.NoError(errWithCode)
-
- err := bcrypt.CompareHashAndPassword([]byte(user.EncryptedPassword), []byte("verygoodnewpassword"))
- suite.NoError(err)
-
- // get user from the db again
- dbUser := >smodel.User{}
- err = suite.db.GetByID(context.Background(), user.ID, dbUser)
- suite.NoError(err)
-
- // check the password has changed
- err = bcrypt.CompareHashAndPassword([]byte(dbUser.EncryptedPassword), []byte("verygoodnewpassword"))
- suite.NoError(err)
-}
-
-func (suite *ChangePasswordTestSuite) TestChangePasswordIncorrectOld() {
- user := suite.testUsers["local_account_1"]
-
- errWithCode := suite.user.ChangePassword(context.Background(), user, "ooooopsydoooopsy", "verygoodnewpassword")
- suite.EqualError(errWithCode, "crypto/bcrypt: hashedPassword is not the hash of the given password")
- suite.Equal(http.StatusUnauthorized, errWithCode.Code())
- suite.Equal("Unauthorized: old password was incorrect", errWithCode.Safe())
-
- // get user from the db again
- dbUser := >smodel.User{}
- err := suite.db.GetByID(context.Background(), user.ID, dbUser)
- suite.NoError(err)
-
- // check the password has not changed
- err = bcrypt.CompareHashAndPassword([]byte(dbUser.EncryptedPassword), []byte("password"))
- suite.NoError(err)
-}
-
-func (suite *ChangePasswordTestSuite) TestChangePasswordWeakNew() {
- user := suite.testUsers["local_account_1"]
-
- errWithCode := suite.user.ChangePassword(context.Background(), user, "password", "1234")
- suite.EqualError(errWithCode, "password is only 11% strength, try including more special characters, using lowercase letters, using uppercase letters or using a longer password")
- suite.Equal(http.StatusBadRequest, errWithCode.Code())
- suite.Equal("Bad Request: password is only 11% strength, try including more special characters, using lowercase letters, using uppercase letters or using a longer password", errWithCode.Safe())
-
- // get user from the db again
- dbUser := >smodel.User{}
- err := suite.db.GetByID(context.Background(), user.ID, dbUser)
- suite.NoError(err)
-
- // check the password has not changed
- err = bcrypt.CompareHashAndPassword([]byte(dbUser.EncryptedPassword), []byte("password"))
- suite.NoError(err)
-}
-
-func TestChangePasswordTestSuite(t *testing.T) {
- suite.Run(t, &ChangePasswordTestSuite{})
-}
diff --git a/internal/processing/user/email.go b/internal/processing/user/email.go
new file mode 100644
index 000000000..349e27f47
--- /dev/null
+++ b/internal/processing/user/email.go
@@ -0,0 +1,137 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+package user
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/email"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/uris"
+)
+
+var oneWeek = 168 * time.Hour
+
+// EmailSendConfirmation sends an email address confirmation request email to the given user.
+func (p *Processor) EmailSendConfirmation(ctx context.Context, user *gtsmodel.User, username string) error {
+ if user.UnconfirmedEmail == "" || user.UnconfirmedEmail == user.Email {
+ // user has already confirmed this email address, so there's nothing to do
+ return nil
+ }
+
+ // We need a token and a link for the user to click on.
+ // We'll use a uuid as our token since it's basically impossible to guess.
+ // From the uuid package we use (which uses crypto/rand under the hood):
+ // Randomly generated UUIDs have 122 random bits. One's annual risk of being
+ // hit by a meteorite is estimated to be one chance in 17 billion, that
+ // means the probability is about 0.00000000006 (6 × 10−11),
+ // equivalent to the odds of creating a few tens of trillions of UUIDs in a
+ // year and having one duplicate.
+ confirmationToken := uuid.NewString()
+ confirmationLink := uris.GenerateURIForEmailConfirm(confirmationToken)
+
+ // pull our instance entry from the database so we can greet the user nicely in the email
+ instance := >smodel.Instance{}
+ host := config.GetHost()
+ if err := p.db.GetWhere(ctx, []db.Where{{Key: "domain", Value: host}}, instance); err != nil {
+ return fmt.Errorf("SendConfirmEmail: error getting instance: %s", err)
+ }
+
+ // assemble the email contents and send the email
+ confirmData := email.ConfirmData{
+ Username: username,
+ InstanceURL: instance.URI,
+ InstanceName: instance.Title,
+ ConfirmLink: confirmationLink,
+ }
+ if err := p.emailSender.SendConfirmEmail(user.UnconfirmedEmail, confirmData); err != nil {
+ return fmt.Errorf("SendConfirmEmail: error sending to email address %s belonging to user %s: %s", user.UnconfirmedEmail, username, err)
+ }
+
+ // email sent, now we need to update the user entry with the token we just sent them
+ updatingColumns := []string{"confirmation_sent_at", "confirmation_token", "last_emailed_at", "updated_at"}
+ user.ConfirmationSentAt = time.Now()
+ user.ConfirmationToken = confirmationToken
+ user.LastEmailedAt = time.Now()
+ user.UpdatedAt = time.Now()
+
+ if err := p.db.UpdateByID(ctx, user, user.ID, updatingColumns...); err != nil {
+ return fmt.Errorf("SendConfirmEmail: error updating user entry after email sent: %s", err)
+ }
+
+ return nil
+}
+
+// EmailConfirm processes an email confirmation request, usually initiated as a result of clicking on a link
+// in a 'confirm your email address' type email.
+func (p *Processor) EmailConfirm(ctx context.Context, token string) (*gtsmodel.User, gtserror.WithCode) {
+ if token == "" {
+ return nil, gtserror.NewErrorNotFound(errors.New("no token provided"))
+ }
+
+ user, err := p.db.GetUserByConfirmationToken(ctx, token)
+ if err != nil {
+ if err == db.ErrNoEntries {
+ return nil, gtserror.NewErrorNotFound(err)
+ }
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if user.Account == nil {
+ a, err := p.db.GetAccountByID(ctx, user.AccountID)
+ if err != nil {
+ return nil, gtserror.NewErrorNotFound(err)
+ }
+ user.Account = a
+ }
+
+ if !user.Account.SuspendedAt.IsZero() {
+ return nil, gtserror.NewErrorForbidden(fmt.Errorf("ConfirmEmail: account %s is suspended", user.AccountID))
+ }
+
+ if user.UnconfirmedEmail == "" || user.UnconfirmedEmail == user.Email {
+ // no pending email confirmations so just return OK
+ return user, nil
+ }
+
+ if user.ConfirmationSentAt.Before(time.Now().Add(-oneWeek)) {
+ return nil, gtserror.NewErrorForbidden(errors.New("ConfirmEmail: confirmation token expired"))
+ }
+
+ // mark the user's email address as confirmed + remove the unconfirmed address and the token
+ updatingColumns := []string{"email", "unconfirmed_email", "confirmed_at", "confirmation_token", "updated_at"}
+ user.Email = user.UnconfirmedEmail
+ user.UnconfirmedEmail = ""
+ user.ConfirmedAt = time.Now()
+ user.ConfirmationToken = ""
+ user.UpdatedAt = time.Now()
+
+ if err := p.db.UpdateByID(ctx, user, user.ID, updatingColumns...); err != nil {
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return user, nil
+}
diff --git a/internal/processing/user/email_test.go b/internal/processing/user/email_test.go
new file mode 100644
index 000000000..f66b7987c
--- /dev/null
+++ b/internal/processing/user/email_test.go
@@ -0,0 +1,116 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+package user_test
+
+import (
+ "context"
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/suite"
+)
+
+type EmailConfirmTestSuite struct {
+ UserStandardTestSuite
+}
+
+func (suite *EmailConfirmTestSuite) TestSendConfirmEmail() {
+ user := suite.testUsers["local_account_1"]
+
+ // set a bunch of stuff on the user as though zork hasn't been confirmed (perish the thought)
+ user.UnconfirmedEmail = "some.email@example.org"
+ user.Email = ""
+ user.ConfirmedAt = time.Time{}
+ user.ConfirmationSentAt = time.Time{}
+ user.ConfirmationToken = ""
+
+ err := suite.user.EmailSendConfirmation(context.Background(), user, "the_mighty_zork")
+ suite.NoError(err)
+
+ // zork should have an email now
+ suite.Len(suite.sentEmails, 1)
+ email, ok := suite.sentEmails["some.email@example.org"]
+ suite.True(ok)
+
+ // a token should be set on zork
+ token := user.ConfirmationToken
+ suite.NotEmpty(token)
+
+ // email should contain the token
+ emailShould := fmt.Sprintf("To: some.email@example.org\r\nSubject: GoToSocial Email Confirmation\r\n\r\nHello the_mighty_zork!\r\n\r\nYou are receiving this mail because you've requested an account on http://localhost:8080.\r\n\r\nWe just need to confirm that this is your email address. To confirm your email, paste the following in your browser's address bar:\r\n\r\nhttp://localhost:8080/confirm_email?token=%s\r\n\r\nIf you believe you've been sent this email in error, feel free to ignore it, or contact the administrator of http://localhost:8080\r\n\r\n", token)
+ suite.Equal(emailShould, email)
+
+ // confirmationSentAt should be recent
+ suite.WithinDuration(time.Now(), user.ConfirmationSentAt, 1*time.Minute)
+}
+
+func (suite *EmailConfirmTestSuite) TestConfirmEmail() {
+ ctx := context.Background()
+
+ user := suite.testUsers["local_account_1"]
+
+ // set a bunch of stuff on the user as though zork hasn't been confirmed yet, but has had an email sent 5 minutes ago
+ updatingColumns := []string{"unconfirmed_email", "email", "confirmed_at", "confirmation_sent_at", "confirmation_token"}
+ user.UnconfirmedEmail = "some.email@example.org"
+ user.Email = ""
+ user.ConfirmedAt = time.Time{}
+ user.ConfirmationSentAt = time.Now().Add(-5 * time.Minute)
+ user.ConfirmationToken = "1d1aa44b-afa4-49c8-ac4b-eceb61715cc6"
+
+ err := suite.db.UpdateByID(ctx, user, user.ID, updatingColumns...)
+ suite.NoError(err)
+
+ // confirm with the token set above
+ updatedUser, errWithCode := suite.user.EmailConfirm(ctx, "1d1aa44b-afa4-49c8-ac4b-eceb61715cc6")
+ suite.NoError(errWithCode)
+
+ // email should now be confirmed and token cleared
+ suite.Equal("some.email@example.org", updatedUser.Email)
+ suite.Empty(updatedUser.UnconfirmedEmail)
+ suite.Empty(updatedUser.ConfirmationToken)
+ suite.WithinDuration(updatedUser.ConfirmedAt, time.Now(), 1*time.Minute)
+ suite.WithinDuration(updatedUser.UpdatedAt, time.Now(), 1*time.Minute)
+}
+
+func (suite *EmailConfirmTestSuite) TestConfirmEmailOldToken() {
+ ctx := context.Background()
+
+ user := suite.testUsers["local_account_1"]
+
+ // set a bunch of stuff on the user as though zork hasn't been confirmed yet, but has had an email sent 8 days ago
+ updatingColumns := []string{"unconfirmed_email", "email", "confirmed_at", "confirmation_sent_at", "confirmation_token"}
+ user.UnconfirmedEmail = "some.email@example.org"
+ user.Email = ""
+ user.ConfirmedAt = time.Time{}
+ user.ConfirmationSentAt = time.Now().Add(-192 * time.Hour)
+ user.ConfirmationToken = "1d1aa44b-afa4-49c8-ac4b-eceb61715cc6"
+
+ err := suite.db.UpdateByID(ctx, user, user.ID, updatingColumns...)
+ suite.NoError(err)
+
+ // confirm with the token set above
+ updatedUser, errWithCode := suite.user.EmailConfirm(ctx, "1d1aa44b-afa4-49c8-ac4b-eceb61715cc6")
+ suite.Nil(updatedUser)
+ suite.EqualError(errWithCode, "ConfirmEmail: confirmation token expired")
+}
+
+func TestEmailConfirmTestSuite(t *testing.T) {
+ suite.Run(t, &EmailConfirmTestSuite{})
+}
diff --git a/internal/processing/user/emailconfirm.go b/internal/processing/user/emailconfirm.go
deleted file mode 100644
index 3bc889024..000000000
--- a/internal/processing/user/emailconfirm.go
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-*/
-
-package user
-
-import (
- "context"
- "errors"
- "fmt"
- "time"
-
- "github.com/google/uuid"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/db"
- "github.com/superseriousbusiness/gotosocial/internal/email"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
- "github.com/superseriousbusiness/gotosocial/internal/uris"
-)
-
-var oneWeek = 168 * time.Hour
-
-func (p *processor) SendConfirmEmail(ctx context.Context, user *gtsmodel.User, username string) error {
- if user.UnconfirmedEmail == "" || user.UnconfirmedEmail == user.Email {
- // user has already confirmed this email address, so there's nothing to do
- return nil
- }
-
- // We need a token and a link for the user to click on.
- // We'll use a uuid as our token since it's basically impossible to guess.
- // From the uuid package we use (which uses crypto/rand under the hood):
- // Randomly generated UUIDs have 122 random bits. One's annual risk of being
- // hit by a meteorite is estimated to be one chance in 17 billion, that
- // means the probability is about 0.00000000006 (6 × 10−11),
- // equivalent to the odds of creating a few tens of trillions of UUIDs in a
- // year and having one duplicate.
- confirmationToken := uuid.NewString()
- confirmationLink := uris.GenerateURIForEmailConfirm(confirmationToken)
-
- // pull our instance entry from the database so we can greet the user nicely in the email
- instance := >smodel.Instance{}
- host := config.GetHost()
- if err := p.db.GetWhere(ctx, []db.Where{{Key: "domain", Value: host}}, instance); err != nil {
- return fmt.Errorf("SendConfirmEmail: error getting instance: %s", err)
- }
-
- // assemble the email contents and send the email
- confirmData := email.ConfirmData{
- Username: username,
- InstanceURL: instance.URI,
- InstanceName: instance.Title,
- ConfirmLink: confirmationLink,
- }
- if err := p.emailSender.SendConfirmEmail(user.UnconfirmedEmail, confirmData); err != nil {
- return fmt.Errorf("SendConfirmEmail: error sending to email address %s belonging to user %s: %s", user.UnconfirmedEmail, username, err)
- }
-
- // email sent, now we need to update the user entry with the token we just sent them
- updatingColumns := []string{"confirmation_sent_at", "confirmation_token", "last_emailed_at", "updated_at"}
- user.ConfirmationSentAt = time.Now()
- user.ConfirmationToken = confirmationToken
- user.LastEmailedAt = time.Now()
- user.UpdatedAt = time.Now()
-
- if err := p.db.UpdateByID(ctx, user, user.ID, updatingColumns...); err != nil {
- return fmt.Errorf("SendConfirmEmail: error updating user entry after email sent: %s", err)
- }
-
- return nil
-}
-
-func (p *processor) ConfirmEmail(ctx context.Context, token string) (*gtsmodel.User, gtserror.WithCode) {
- if token == "" {
- return nil, gtserror.NewErrorNotFound(errors.New("no token provided"))
- }
-
- user, err := p.db.GetUserByConfirmationToken(ctx, token)
- if err != nil {
- if err == db.ErrNoEntries {
- return nil, gtserror.NewErrorNotFound(err)
- }
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- if user.Account == nil {
- a, err := p.db.GetAccountByID(ctx, user.AccountID)
- if err != nil {
- return nil, gtserror.NewErrorNotFound(err)
- }
- user.Account = a
- }
-
- if !user.Account.SuspendedAt.IsZero() {
- return nil, gtserror.NewErrorForbidden(fmt.Errorf("ConfirmEmail: account %s is suspended", user.AccountID))
- }
-
- if user.UnconfirmedEmail == "" || user.UnconfirmedEmail == user.Email {
- // no pending email confirmations so just return OK
- return user, nil
- }
-
- if user.ConfirmationSentAt.Before(time.Now().Add(-oneWeek)) {
- return nil, gtserror.NewErrorForbidden(errors.New("ConfirmEmail: confirmation token expired"))
- }
-
- // mark the user's email address as confirmed + remove the unconfirmed address and the token
- updatingColumns := []string{"email", "unconfirmed_email", "confirmed_at", "confirmation_token", "updated_at"}
- user.Email = user.UnconfirmedEmail
- user.UnconfirmedEmail = ""
- user.ConfirmedAt = time.Now()
- user.ConfirmationToken = ""
- user.UpdatedAt = time.Now()
-
- if err := p.db.UpdateByID(ctx, user, user.ID, updatingColumns...); err != nil {
- return nil, gtserror.NewErrorInternalError(err)
- }
-
- return user, nil
-}
diff --git a/internal/processing/user/emailconfirm_test.go b/internal/processing/user/emailconfirm_test.go
deleted file mode 100644
index a13a130d0..000000000
--- a/internal/processing/user/emailconfirm_test.go
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- GoToSocial
- Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see .
-*/
-
-package user_test
-
-import (
- "context"
- "fmt"
- "testing"
- "time"
-
- "github.com/stretchr/testify/suite"
-)
-
-type EmailConfirmTestSuite struct {
- UserStandardTestSuite
-}
-
-func (suite *EmailConfirmTestSuite) TestSendConfirmEmail() {
- user := suite.testUsers["local_account_1"]
-
- // set a bunch of stuff on the user as though zork hasn't been confirmed (perish the thought)
- user.UnconfirmedEmail = "some.email@example.org"
- user.Email = ""
- user.ConfirmedAt = time.Time{}
- user.ConfirmationSentAt = time.Time{}
- user.ConfirmationToken = ""
-
- err := suite.user.SendConfirmEmail(context.Background(), user, "the_mighty_zork")
- suite.NoError(err)
-
- // zork should have an email now
- suite.Len(suite.sentEmails, 1)
- email, ok := suite.sentEmails["some.email@example.org"]
- suite.True(ok)
-
- // a token should be set on zork
- token := user.ConfirmationToken
- suite.NotEmpty(token)
-
- // email should contain the token
- emailShould := fmt.Sprintf("To: some.email@example.org\r\nSubject: GoToSocial Email Confirmation\r\n\r\nHello the_mighty_zork!\r\n\r\nYou are receiving this mail because you've requested an account on http://localhost:8080.\r\n\r\nWe just need to confirm that this is your email address. To confirm your email, paste the following in your browser's address bar:\r\n\r\nhttp://localhost:8080/confirm_email?token=%s\r\n\r\nIf you believe you've been sent this email in error, feel free to ignore it, or contact the administrator of http://localhost:8080\r\n\r\n", token)
- suite.Equal(emailShould, email)
-
- // confirmationSentAt should be recent
- suite.WithinDuration(time.Now(), user.ConfirmationSentAt, 1*time.Minute)
-}
-
-func (suite *EmailConfirmTestSuite) TestConfirmEmail() {
- ctx := context.Background()
-
- user := suite.testUsers["local_account_1"]
-
- // set a bunch of stuff on the user as though zork hasn't been confirmed yet, but has had an email sent 5 minutes ago
- updatingColumns := []string{"unconfirmed_email", "email", "confirmed_at", "confirmation_sent_at", "confirmation_token"}
- user.UnconfirmedEmail = "some.email@example.org"
- user.Email = ""
- user.ConfirmedAt = time.Time{}
- user.ConfirmationSentAt = time.Now().Add(-5 * time.Minute)
- user.ConfirmationToken = "1d1aa44b-afa4-49c8-ac4b-eceb61715cc6"
-
- err := suite.db.UpdateByID(ctx, user, user.ID, updatingColumns...)
- suite.NoError(err)
-
- // confirm with the token set above
- updatedUser, errWithCode := suite.user.ConfirmEmail(ctx, "1d1aa44b-afa4-49c8-ac4b-eceb61715cc6")
- suite.NoError(errWithCode)
-
- // email should now be confirmed and token cleared
- suite.Equal("some.email@example.org", updatedUser.Email)
- suite.Empty(updatedUser.UnconfirmedEmail)
- suite.Empty(updatedUser.ConfirmationToken)
- suite.WithinDuration(updatedUser.ConfirmedAt, time.Now(), 1*time.Minute)
- suite.WithinDuration(updatedUser.UpdatedAt, time.Now(), 1*time.Minute)
-}
-
-func (suite *EmailConfirmTestSuite) TestConfirmEmailOldToken() {
- ctx := context.Background()
-
- user := suite.testUsers["local_account_1"]
-
- // set a bunch of stuff on the user as though zork hasn't been confirmed yet, but has had an email sent 8 days ago
- updatingColumns := []string{"unconfirmed_email", "email", "confirmed_at", "confirmation_sent_at", "confirmation_token"}
- user.UnconfirmedEmail = "some.email@example.org"
- user.Email = ""
- user.ConfirmedAt = time.Time{}
- user.ConfirmationSentAt = time.Now().Add(-192 * time.Hour)
- user.ConfirmationToken = "1d1aa44b-afa4-49c8-ac4b-eceb61715cc6"
-
- err := suite.db.UpdateByID(ctx, user, user.ID, updatingColumns...)
- suite.NoError(err)
-
- // confirm with the token set above
- updatedUser, errWithCode := suite.user.ConfirmEmail(ctx, "1d1aa44b-afa4-49c8-ac4b-eceb61715cc6")
- suite.Nil(updatedUser)
- suite.EqualError(errWithCode, "ConfirmEmail: confirmation token expired")
-}
-
-func TestEmailConfirmTestSuite(t *testing.T) {
- suite.Run(t, &EmailConfirmTestSuite{})
-}
diff --git a/internal/processing/user/password.go b/internal/processing/user/password.go
new file mode 100644
index 000000000..3475e005e
--- /dev/null
+++ b/internal/processing/user/password.go
@@ -0,0 +1,52 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+package user
+
+import (
+ "context"
+
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/validate"
+ "golang.org/x/crypto/bcrypt"
+)
+
+// PasswordChange processes a password change request for the given user.
+func (p *Processor) PasswordChange(ctx context.Context, user *gtsmodel.User, oldPassword string, newPassword string) gtserror.WithCode {
+ if err := bcrypt.CompareHashAndPassword([]byte(user.EncryptedPassword), []byte(oldPassword)); err != nil {
+ return gtserror.NewErrorUnauthorized(err, "old password was incorrect")
+ }
+
+ if err := validate.NewPassword(newPassword); err != nil {
+ return gtserror.NewErrorBadRequest(err, err.Error())
+ }
+
+ newPasswordHash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost)
+ if err != nil {
+ return gtserror.NewErrorInternalError(err, "error hashing password")
+ }
+
+ user.EncryptedPassword = string(newPasswordHash)
+
+ if err := p.db.UpdateUser(ctx, user, "encrypted_password"); err != nil {
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ return nil
+}
diff --git a/internal/processing/user/password_test.go b/internal/processing/user/password_test.go
new file mode 100644
index 000000000..a02581b5b
--- /dev/null
+++ b/internal/processing/user/password_test.go
@@ -0,0 +1,92 @@
+/*
+ GoToSocial
+ Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+*/
+
+package user_test
+
+import (
+ "context"
+ "net/http"
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "golang.org/x/crypto/bcrypt"
+)
+
+type ChangePasswordTestSuite struct {
+ UserStandardTestSuite
+}
+
+func (suite *ChangePasswordTestSuite) TestChangePasswordOK() {
+ user := suite.testUsers["local_account_1"]
+
+ errWithCode := suite.user.PasswordChange(context.Background(), user, "password", "verygoodnewpassword")
+ suite.NoError(errWithCode)
+
+ err := bcrypt.CompareHashAndPassword([]byte(user.EncryptedPassword), []byte("verygoodnewpassword"))
+ suite.NoError(err)
+
+ // get user from the db again
+ dbUser := >smodel.User{}
+ err = suite.db.GetByID(context.Background(), user.ID, dbUser)
+ suite.NoError(err)
+
+ // check the password has changed
+ err = bcrypt.CompareHashAndPassword([]byte(dbUser.EncryptedPassword), []byte("verygoodnewpassword"))
+ suite.NoError(err)
+}
+
+func (suite *ChangePasswordTestSuite) TestChangePasswordIncorrectOld() {
+ user := suite.testUsers["local_account_1"]
+
+ errWithCode := suite.user.PasswordChange(context.Background(), user, "ooooopsydoooopsy", "verygoodnewpassword")
+ suite.EqualError(errWithCode, "crypto/bcrypt: hashedPassword is not the hash of the given password")
+ suite.Equal(http.StatusUnauthorized, errWithCode.Code())
+ suite.Equal("Unauthorized: old password was incorrect", errWithCode.Safe())
+
+ // get user from the db again
+ dbUser := >smodel.User{}
+ err := suite.db.GetByID(context.Background(), user.ID, dbUser)
+ suite.NoError(err)
+
+ // check the password has not changed
+ err = bcrypt.CompareHashAndPassword([]byte(dbUser.EncryptedPassword), []byte("password"))
+ suite.NoError(err)
+}
+
+func (suite *ChangePasswordTestSuite) TestChangePasswordWeakNew() {
+ user := suite.testUsers["local_account_1"]
+
+ errWithCode := suite.user.PasswordChange(context.Background(), user, "password", "1234")
+ suite.EqualError(errWithCode, "password is only 11% strength, try including more special characters, using lowercase letters, using uppercase letters or using a longer password")
+ suite.Equal(http.StatusBadRequest, errWithCode.Code())
+ suite.Equal("Bad Request: password is only 11% strength, try including more special characters, using lowercase letters, using uppercase letters or using a longer password", errWithCode.Safe())
+
+ // get user from the db again
+ dbUser := >smodel.User{}
+ err := suite.db.GetByID(context.Background(), user.ID, dbUser)
+ suite.NoError(err)
+
+ // check the password has not changed
+ err = bcrypt.CompareHashAndPassword([]byte(dbUser.EncryptedPassword), []byte("password"))
+ suite.NoError(err)
+}
+
+func TestChangePasswordTestSuite(t *testing.T) {
+ suite.Run(t, &ChangePasswordTestSuite{})
+}
diff --git a/internal/processing/user/user.go b/internal/processing/user/user.go
index 5ce8cd803..fce628d0c 100644
--- a/internal/processing/user/user.go
+++ b/internal/processing/user/user.go
@@ -19,33 +19,18 @@
package user
import (
- "context"
-
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/email"
- "github.com/superseriousbusiness/gotosocial/internal/gtserror"
- "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
-// Processor wraps a bunch of functions for processing user-level actions.
-type Processor interface {
- // ChangePassword changes the specified user's password from old => new,
- // or returns an error if the new password is too weak, or the old password is incorrect.
- ChangePassword(ctx context.Context, user *gtsmodel.User, oldPassword string, newPassword string) gtserror.WithCode
- // SendConfirmEmail sends a 'confirm-your-email-address' type email to a user.
- SendConfirmEmail(ctx context.Context, user *gtsmodel.User, username string) error
- // ConfirmEmail confirms an email address using the given token.
- ConfirmEmail(ctx context.Context, token string) (*gtsmodel.User, gtserror.WithCode)
-}
-
-type processor struct {
+type Processor struct {
emailSender email.Sender
db db.DB
}
// New returns a new user processor
func New(db db.DB, emailSender email.Sender) Processor {
- return &processor{
+ return Processor{
emailSender: emailSender,
db: db,
}
--
cgit v1.2.3