summaryrefslogtreecommitdiff
path: root/internal/processing/user/emailconfirm.go
blob: eccaae5ec1995f4f3dd50094a6e10d57bcd8a5fe (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
/*
   GoToSocial
   Copyright (C) 2021-2022 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 <http://www.gnu.org/licenses/>.
*/

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 := &gtsmodel.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
	user.ConfirmationSentAt = time.Now()
	user.ConfirmationToken = confirmationToken
	user.LastEmailedAt = time.Now()
	user.UpdatedAt = time.Now()

	if err := p.db.UpdateByPrimaryKey(ctx, user); 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 := &gtsmodel.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
	}

	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
	user.Email = user.UnconfirmedEmail
	user.UnconfirmedEmail = ""
	user.ConfirmedAt = time.Now()
	user.ConfirmationToken = ""
	user.UpdatedAt = time.Now()

	if err := p.db.UpdateByPrimaryKey(ctx, user); err != nil {
		return nil, gtserror.NewErrorInternalError(err)
	}

	return user, nil
}