diff --git a/internal/processing/processor.go b/internal/processing/processor.go index abaafecda..a36d2ee14 100644 --- a/internal/processing/processor.go +++ b/internal/processing/processor.go @@ -179,6 +179,9 @@ type Processor interface { // UserChangePassword changes the password for the given user, with the given form. UserChangePassword(ctx context.Context, authed *oauth.Auth, form *apimodel.PasswordChangeRequest) gtserror.WithCode + // UserConfirmEmail confirms an email address using the given token. + // The user belonging to the confirmed email is also returned. + UserConfirmEmail(ctx context.Context, token string) (*gtsmodel.User, gtserror.WithCode) /* FEDERATION API-FACING PROCESSING FUNCTIONS diff --git a/internal/processing/user.go b/internal/processing/user.go index a5fca53dd..612788ed8 100644 --- a/internal/processing/user.go +++ b/internal/processing/user.go @@ -23,9 +23,14 @@ import ( apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) func (p *processor) UserChangePassword(ctx context.Context, authed *oauth.Auth, form *apimodel.PasswordChangeRequest) gtserror.WithCode { return p.userProcessor.ChangePassword(ctx, authed.User, form.OldPassword, form.NewPassword) } + +func (p *processor) UserConfirmEmail(ctx context.Context, token string) (*gtsmodel.User, gtserror.WithCode) { + return p.userProcessor.ConfirmEmail(ctx, token) +} diff --git a/internal/processing/user/emailconfirm.go b/internal/processing/user/emailconfirm.go index 0eb535730..7049e0e8f 100644 --- a/internal/processing/user/emailconfirm.go +++ b/internal/processing/user/emailconfirm.go @@ -20,12 +20,14 @@ package user import ( "context" + "errors" "fmt" "time" "github.com/google/uuid" "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/util" ) @@ -76,3 +78,46 @@ func (p *processor) SendConfirmEmail(ctx context.Context, user *gtsmodel.User, u 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 := >smodel.User{} + if err := p.db.GetWhere(ctx, []db.Where{{Key: "confirmation_token", Value: token}}, user); 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 + } + + // mark the user's email address as confirmed + remove the unconfirmed address and the token + user.Email = user.UnconfirmedEmail + user.UnconfirmedEmail = "" + user.ConfirmedAt = time.Now() + user.ConfirmationToken = "" + + if err := p.db.UpdateByPrimaryKey(ctx, user); err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + return user, nil +} diff --git a/internal/processing/user/user.go b/internal/processing/user/user.go index 3a94b219f..73cdb4901 100644 --- a/internal/processing/user/user.go +++ b/internal/processing/user/user.go @@ -33,7 +33,10 @@ 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 { diff --git a/internal/web/confirmemail.go b/internal/web/confirmemail.go index 5dd898604..566e30344 100644 --- a/internal/web/confirmemail.go +++ b/internal/web/confirmemail.go @@ -18,8 +18,12 @@ package web -import "github.com/gin-gonic/gin" +import ( + "net/http" + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" +) func (m *Module) ConfirmEmailGETHandler(c *gin.Context) { // if there's no token in the query, just serve the 404 web handler @@ -29,5 +33,25 @@ func (m *Module) ConfirmEmailGETHandler(c *gin.Context) { return } + ctx := c.Request.Context() + user, errWithCode := m.processor.UserConfirmEmail(ctx, token) + if errWithCode != nil { + logrus.Debugf("error confirming email: %s", errWithCode.Error()) + // if something goes wrong, just log it and direct to the 404 handler to not give anything away + m.NotFoundHandler(c) + return + } + + instance, err := m.processor.InstanceGet(ctx, m.config.Host) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.HTML(http.StatusOK, "confirmed.tmpl", gin.H{ + "instance": instance, + "email": user.Email, + "username": user.Account.Username, + }) } diff --git a/web/template/confirmed.tmpl b/web/template/confirmed.tmpl new file mode 100644 index 000000000..920f7d3b2 --- /dev/null +++ b/web/template/confirmed.tmpl @@ -0,0 +1,9 @@ +{{ template "header.tmpl" .}} +
+
+

Email Address Confirmed

+

Thanks {{.username}}! Your email address {{.email}} has been confirmed.

+

+
+ +{{ template "footer.tmpl" .}} \ No newline at end of file