satellite/console: implement account activation with code
This change implements account activation using OTP code. Based on whether this activation method is activated, signup will generate a 6-digit code and send it via email. A new endpoint is added to validate this code and activate the user's account and log them in. Issue: #6428 Change-Id: Ia78bb123258021bce78ab9e98dce2c900328057a
This commit is contained in:
parent
1546732afc
commit
116d8cbea1
@ -5,9 +5,11 @@ package consoleapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@ -16,6 +18,7 @@ import (
|
||||
"github.com/zeebo/errs"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"storj.io/common/http/requestid"
|
||||
"storj.io/common/uuid"
|
||||
"storj.io/storj/private/post"
|
||||
"storj.io/storj/private/web"
|
||||
@ -46,6 +49,7 @@ type Auth struct {
|
||||
PasswordRecoveryURL string
|
||||
CancelPasswordRecoveryURL string
|
||||
ActivateAccountURL string
|
||||
ActivationCodeEnabled bool
|
||||
SatelliteName string
|
||||
service *console.Service
|
||||
accountFreezeService *console.AccountFreezeService
|
||||
@ -55,7 +59,7 @@ type Auth struct {
|
||||
}
|
||||
|
||||
// NewAuth is a constructor for api auth controller.
|
||||
func NewAuth(log *zap.Logger, service *console.Service, accountFreezeService *console.AccountFreezeService, mailService *mailservice.Service, cookieAuth *consolewebauth.CookieAuth, analytics *analytics.Service, satelliteName, externalAddress, letUsKnowURL, termsAndConditionsURL, contactInfoURL, generalRequestURL string) *Auth {
|
||||
func NewAuth(log *zap.Logger, service *console.Service, accountFreezeService *console.AccountFreezeService, mailService *mailservice.Service, cookieAuth *consolewebauth.CookieAuth, analytics *analytics.Service, satelliteName, externalAddress, letUsKnowURL, termsAndConditionsURL, contactInfoURL, generalRequestURL string, activationCodeEnabled bool) *Auth {
|
||||
return &Auth{
|
||||
log: log,
|
||||
ExternalAddress: externalAddress,
|
||||
@ -67,6 +71,7 @@ func NewAuth(log *zap.Logger, service *console.Service, accountFreezeService *co
|
||||
PasswordRecoveryURL: externalAddress + "password-recovery",
|
||||
CancelPasswordRecoveryURL: externalAddress + "cancel-password-recovery",
|
||||
ActivateAccountURL: externalAddress + "activation",
|
||||
ActivationCodeEnabled: activationCodeEnabled,
|
||||
service: service,
|
||||
accountFreezeService: accountFreezeService,
|
||||
mailService: mailService,
|
||||
@ -295,6 +300,20 @@ func (a *Auth) Register(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
var code string
|
||||
var requestID string
|
||||
if a.ActivationCodeEnabled {
|
||||
randNum, err := rand.Int(rand.Reader, big.NewInt(900000))
|
||||
if err != nil {
|
||||
a.serveJSONError(ctx, w, console.Error.Wrap(err))
|
||||
return
|
||||
}
|
||||
randNum = randNum.Add(randNum, big.NewInt(100000))
|
||||
code = randNum.String()
|
||||
|
||||
requestID = requestid.FromContext(ctx)
|
||||
}
|
||||
|
||||
user, err = a.service.CreateUser(ctx,
|
||||
console.CreateUser{
|
||||
FullName: registerData.FullName,
|
||||
@ -310,6 +329,8 @@ func (a *Auth) Register(w http.ResponseWriter, r *http.Request) {
|
||||
CaptchaResponse: registerData.CaptchaResponse,
|
||||
IP: ip,
|
||||
SignupPromoCode: registerData.SignupPromoCode,
|
||||
ActivationCode: code,
|
||||
SignupId: requestID,
|
||||
},
|
||||
secret,
|
||||
)
|
||||
@ -374,6 +395,17 @@ func (a *Auth) Register(w http.ResponseWriter, r *http.Request) {
|
||||
a.analytics.TrackCreateUser(trackCreateUserFields)
|
||||
}
|
||||
|
||||
if a.ActivationCodeEnabled {
|
||||
a.mailService.SendRenderedAsync(
|
||||
ctx,
|
||||
[]post.Address{{Address: user.Email}},
|
||||
&console.AccountActivationCodeEmail{
|
||||
ActivationCode: user.ActivationCode,
|
||||
},
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
token, err := a.service.GenerateActivationToken(ctx, user.ID, user.Email)
|
||||
if err != nil {
|
||||
a.serveJSONError(ctx, w, err)
|
||||
@ -392,6 +424,93 @@ func (a *Auth) Register(w http.ResponseWriter, r *http.Request) {
|
||||
)
|
||||
}
|
||||
|
||||
// ActivateAccount verifies a signup activation code.
|
||||
func (a *Auth) ActivateAccount(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
var err error
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
var activateData struct {
|
||||
Email string `json:"email"`
|
||||
Code string `json:"code"`
|
||||
SignupId string `json:"signupId"`
|
||||
}
|
||||
err = json.NewDecoder(r.Body).Decode(&activateData)
|
||||
if err != nil {
|
||||
a.serveJSONError(ctx, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
verified, unverified, err := a.service.GetUserByEmailWithUnverified(ctx, activateData.Email)
|
||||
if err != nil && !console.ErrEmailNotFound.Has(err) {
|
||||
a.serveJSONError(ctx, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if verified != nil {
|
||||
satelliteAddress := a.ExternalAddress
|
||||
if !strings.HasSuffix(satelliteAddress, "/") {
|
||||
satelliteAddress += "/"
|
||||
}
|
||||
a.mailService.SendRenderedAsync(
|
||||
ctx,
|
||||
[]post.Address{{Address: verified.Email}},
|
||||
&console.AccountAlreadyExistsEmail{
|
||||
Origin: satelliteAddress,
|
||||
SatelliteName: a.SatelliteName,
|
||||
SignInLink: satelliteAddress + "login",
|
||||
ResetPasswordLink: satelliteAddress + "forgot-password",
|
||||
CreateAccountLink: satelliteAddress + "signup",
|
||||
},
|
||||
)
|
||||
// return error since verified user already exists.
|
||||
a.serveJSONError(ctx, w, console.ErrUnauthorized.New("user already verified"))
|
||||
return
|
||||
}
|
||||
|
||||
var user *console.User
|
||||
if len(unverified) == 0 {
|
||||
a.serveJSONError(ctx, w, console.ErrEmailNotFound.New("no unverified user found"))
|
||||
return
|
||||
}
|
||||
user = &unverified[0]
|
||||
|
||||
if user.ActivationCode != activateData.Code || user.SignupId != activateData.SignupId {
|
||||
a.serveJSONError(ctx, w, console.ErrActivationCode.New("invalid activation code"))
|
||||
return
|
||||
}
|
||||
|
||||
err = a.service.SetAccountActive(ctx, user)
|
||||
if err != nil {
|
||||
a.serveJSONError(ctx, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
ip, err := web.GetRequestIP(r)
|
||||
if err != nil {
|
||||
a.serveJSONError(ctx, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
tokenInfo, err := a.service.GenerateSessionToken(ctx, user.ID, user.Email, ip, r.UserAgent())
|
||||
if err != nil {
|
||||
a.serveJSONError(ctx, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
a.cookieAuth.SetTokenCookie(w, *tokenInfo)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
err = json.NewEncoder(w).Encode(struct {
|
||||
console.TokenInfo
|
||||
Token string `json:"token"`
|
||||
}{*tokenInfo, tokenInfo.Token.String()})
|
||||
if err != nil {
|
||||
a.log.Error("could not encode token response", zap.Error(ErrAuthAPI.Wrap(err)))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// loadSession looks for a cookie for the session id.
|
||||
// this cookie is set from the reverse proxy if the user opts into cookies from Storj.
|
||||
func loadSession(req *http.Request) string {
|
||||
@ -717,6 +836,24 @@ func (a *Auth) ResendEmail(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
user := unverified[0]
|
||||
|
||||
if a.ActivationCodeEnabled {
|
||||
user, err = a.service.SetActivationCodeAndSignupID(ctx, user)
|
||||
if err != nil {
|
||||
a.serveJSONError(ctx, w, err)
|
||||
return
|
||||
}
|
||||
|
||||
a.mailService.SendRenderedAsync(
|
||||
ctx,
|
||||
[]post.Address{{Address: user.Email}},
|
||||
&console.AccountActivationCodeEmail{
|
||||
ActivationCode: user.ActivationCode,
|
||||
},
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
token, err := a.service.GenerateActivationToken(ctx, user.ID, user.Email)
|
||||
if err != nil {
|
||||
a.serveJSONError(ctx, w, err)
|
||||
@ -1108,7 +1245,7 @@ func (a *Auth) getStatusCode(err error) int {
|
||||
switch {
|
||||
case console.ErrValidation.Has(err), console.ErrCaptcha.Has(err), console.ErrMFAMissing.Has(err), console.ErrMFAPasscode.Has(err), console.ErrMFARecoveryCode.Has(err), console.ErrChangePassword.Has(err), console.ErrInvalidProjectLimit.Has(err):
|
||||
return http.StatusBadRequest
|
||||
case console.ErrUnauthorized.Has(err), console.ErrTokenExpiration.Has(err), console.ErrRecoveryToken.Has(err), console.ErrLoginCredentials.Has(err):
|
||||
case console.ErrUnauthorized.Has(err), console.ErrTokenExpiration.Has(err), console.ErrRecoveryToken.Has(err), console.ErrLoginCredentials.Has(err), console.ErrActivationCode.Has(err):
|
||||
return http.StatusUnauthorized
|
||||
case console.ErrEmailUsed.Has(err), console.ErrMFAConflict.Has(err):
|
||||
return http.StatusConflict
|
||||
@ -1155,6 +1292,8 @@ func (a *Auth) getUserErrorMessage(err error) string {
|
||||
return "The server is incapable of fulfilling the request"
|
||||
case errors.As(err, &maxBytesError):
|
||||
return "Request body is too large"
|
||||
case console.ErrActivationCode.Has(err):
|
||||
return "The activation code is invalid"
|
||||
default:
|
||||
return "There was an error processing your request"
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
@ -315,7 +316,7 @@ func TestDeleteAccount(t *testing.T) {
|
||||
|
||||
actualHandler := func(r *http.Request) (status int, body []byte) {
|
||||
rr := httptest.NewRecorder()
|
||||
authController := consoleapi.NewAuth(log, nil, nil, nil, nil, nil, "", "", "", "", "", "")
|
||||
authController := consoleapi.NewAuth(log, nil, nil, nil, nil, nil, "", "", "", "", "", "", false)
|
||||
authController.DeleteAccount(rr, r)
|
||||
|
||||
result := rr.Result()
|
||||
@ -731,6 +732,45 @@ func TestRegistrationEmail(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestRegistrationEmail_CodeEnabled(t *testing.T) {
|
||||
testplanet.Run(t, testplanet.Config{
|
||||
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0,
|
||||
Reconfigure: testplanet.Reconfigure{
|
||||
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
|
||||
config.Console.SignupActivationCodeEnabled = true
|
||||
},
|
||||
},
|
||||
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
||||
sat := planet.Satellites[0]
|
||||
email := "test@mail.test"
|
||||
|
||||
sender := &EmailVerifier{Context: ctx}
|
||||
sat.API.Mail.Service.Sender = sender
|
||||
|
||||
jsonBody, err := json.Marshal(map[string]interface{}{
|
||||
"fullName": "Test User",
|
||||
"shortName": "Test",
|
||||
"email": email,
|
||||
"password": "123a123",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
signupURL := planet.Satellites[0].ConsoleURL() + "/api/v0/auth/register"
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, signupURL, bytes.NewBuffer(jsonBody))
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
result, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, result.StatusCode)
|
||||
require.NoError(t, result.Body.Close())
|
||||
|
||||
body, err := sender.Data.Get(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, body, "code")
|
||||
})
|
||||
}
|
||||
|
||||
func TestIncreaseLimit(t *testing.T) {
|
||||
testplanet.Run(t, testplanet.Config{
|
||||
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0,
|
||||
@ -839,6 +879,67 @@ func TestResendActivationEmail(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestResendActivationEmail_CodeEnabled(t *testing.T) {
|
||||
testplanet.Run(t, testplanet.Config{
|
||||
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0,
|
||||
Reconfigure: testplanet.Reconfigure{
|
||||
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
|
||||
config.Console.SignupActivationCodeEnabled = true
|
||||
},
|
||||
},
|
||||
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
||||
sat := planet.Satellites[0]
|
||||
usersRepo := sat.DB.Console().Users()
|
||||
|
||||
user, err := sat.AddUser(ctx, console.CreateUser{
|
||||
FullName: "Test User",
|
||||
Email: "test@mail.test",
|
||||
}, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Expect activation e-mail to be sent when using unverified e-mail address.
|
||||
user.Status = console.Inactive
|
||||
require.NoError(t, usersRepo.Update(ctx, user.ID, console.UpdateUserRequest{
|
||||
Status: &user.Status,
|
||||
}))
|
||||
|
||||
sender := &EmailVerifier{Context: ctx}
|
||||
sat.API.Mail.Service.Sender = sender
|
||||
|
||||
resendURL := planet.Satellites[0].ConsoleURL() + "/api/v0/auth/resend-email/" + user.Email
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, resendURL, bytes.NewBufferString(user.Email))
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, result.Body.Close())
|
||||
require.Equal(t, http.StatusOK, result.StatusCode)
|
||||
|
||||
body, err := sender.Data.Get(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, body, "code")
|
||||
|
||||
regex := regexp.MustCompile(`(\d{6})\n\s*<\/h1>`)
|
||||
code := strings.Replace(regex.FindString(body.(string)), "</h1>", "", 1)
|
||||
code = strings.TrimSpace(code)
|
||||
require.Contains(t, body, code)
|
||||
|
||||
// resending should send a new code.
|
||||
result, err = http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, result.Body.Close())
|
||||
require.Equal(t, http.StatusOK, result.StatusCode)
|
||||
|
||||
body, err = sender.Data.Get(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, body, "code")
|
||||
|
||||
newCode := strings.Replace(regex.FindString(body.(string)), "</h1>", "", 1)
|
||||
newCode = strings.TrimSpace(newCode)
|
||||
require.NotEqual(t, code, newCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuth_Register_ShortPartnerOrPromo(t *testing.T) {
|
||||
testplanet.Run(t, testplanet.Config{
|
||||
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0,
|
||||
@ -961,3 +1062,106 @@ func TestAuth_Register_PasswordLength(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccountActivationWithCode(t *testing.T) {
|
||||
testplanet.Run(t, testplanet.Config{
|
||||
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 0,
|
||||
Reconfigure: testplanet.Reconfigure{
|
||||
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
|
||||
config.Console.SignupActivationCodeEnabled = true
|
||||
config.Console.RateLimit.Burst = 10
|
||||
},
|
||||
},
|
||||
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
||||
sat := planet.Satellites[0]
|
||||
email := "test@mail.test"
|
||||
|
||||
sender := &EmailVerifier{Context: ctx}
|
||||
sat.API.Mail.Service.Sender = sender
|
||||
|
||||
jsonBody, err := json.Marshal(map[string]interface{}{
|
||||
"fullName": "Test User",
|
||||
"shortName": "Test",
|
||||
"email": email,
|
||||
"password": "123a123",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
signupURL := planet.Satellites[0].ConsoleURL() + "/api/v0/auth/register"
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, signupURL, bytes.NewBuffer(jsonBody))
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
result, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, result.StatusCode)
|
||||
require.NoError(t, result.Body.Close())
|
||||
|
||||
body, err := sender.Data.Get(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, body, "code")
|
||||
|
||||
regex := regexp.MustCompile(`(\d{6})\n\s*<\/h1>`)
|
||||
code := strings.Replace(regex.FindString(body.(string)), "</h1>", "", 1)
|
||||
code = strings.TrimSpace(code)
|
||||
require.Contains(t, body, code)
|
||||
|
||||
signupID := result.Header.Get("x-request-id")
|
||||
|
||||
activateURL := planet.Satellites[0].ConsoleURL() + "/api/v0/auth/code-activation"
|
||||
jsonBody, err = json.Marshal(map[string]interface{}{
|
||||
"email": email,
|
||||
"code": code,
|
||||
"signupId": "wrong id",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
req, err = http.NewRequestWithContext(ctx, http.MethodPatch, activateURL, bytes.NewBuffer(jsonBody))
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
result, err = http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, result)
|
||||
require.Equal(t, http.StatusUnauthorized, result.StatusCode)
|
||||
require.NoError(t, result.Body.Close())
|
||||
|
||||
jsonBody, err = json.Marshal(map[string]interface{}{
|
||||
"email": email,
|
||||
"code": code,
|
||||
"signupId": signupID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
req, err = http.NewRequestWithContext(ctx, http.MethodPatch, activateURL, bytes.NewBuffer(jsonBody))
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
result, err = http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, result)
|
||||
require.Equal(t, http.StatusOK, result.StatusCode)
|
||||
require.NoError(t, result.Body.Close())
|
||||
|
||||
cookies := result.Cookies()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, cookies, 1)
|
||||
require.Equal(t, "_tokenKey", cookies[0].Name)
|
||||
require.NotEmpty(t, cookies[0].Value)
|
||||
|
||||
// trying to activate an activated account should send account already exists email
|
||||
req, err = http.NewRequestWithContext(ctx, http.MethodPatch, activateURL, bytes.NewBuffer(jsonBody))
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
result, err = http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, result)
|
||||
require.Equal(t, http.StatusUnauthorized, result.StatusCode)
|
||||
require.NoError(t, result.Body.Close())
|
||||
|
||||
body, err = sender.Data.Get(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, body, "/login")
|
||||
require.Contains(t, body, "/forgot-password")
|
||||
require.Contains(t, body, "/signup")
|
||||
})
|
||||
}
|
||||
|
@ -300,7 +300,7 @@ func NewServer(logger *zap.Logger, config Config, service *console.Service, oidc
|
||||
projectsRouter.Handle("/{id}/daily-usage", http.HandlerFunc(usageLimitsController.DailyUsage)).Methods(http.MethodGet, http.MethodOptions)
|
||||
projectsRouter.Handle("/usage-report", http.HandlerFunc(usageLimitsController.UsageReport)).Methods(http.MethodGet, http.MethodOptions)
|
||||
|
||||
authController := consoleapi.NewAuth(logger, service, accountFreezeService, mailService, server.cookieAuth, server.analytics, config.SatelliteName, server.config.ExternalAddress, config.LetUsKnowURL, config.TermsAndConditionsURL, config.ContactInfoURL, config.GeneralRequestURL)
|
||||
authController := consoleapi.NewAuth(logger, service, accountFreezeService, mailService, server.cookieAuth, server.analytics, config.SatelliteName, server.config.ExternalAddress, config.LetUsKnowURL, config.TermsAndConditionsURL, config.ContactInfoURL, config.GeneralRequestURL, config.SignupActivationCodeEnabled)
|
||||
authRouter := router.PathPrefix("/api/v0/auth").Subrouter()
|
||||
authRouter.Use(server.withCORS)
|
||||
authRouter.Handle("/account", server.withAuth(http.HandlerFunc(authController.GetAccount))).Methods(http.MethodGet, http.MethodOptions)
|
||||
@ -321,6 +321,7 @@ func NewServer(logger *zap.Logger, config Config, service *console.Service, oidc
|
||||
authRouter.Handle("/token", server.ipRateLimiter.Limit(http.HandlerFunc(authController.Token))).Methods(http.MethodPost, http.MethodOptions)
|
||||
authRouter.Handle("/token-by-api-key", server.ipRateLimiter.Limit(http.HandlerFunc(authController.TokenByAPIKey))).Methods(http.MethodPost, http.MethodOptions)
|
||||
authRouter.Handle("/register", server.ipRateLimiter.Limit(http.HandlerFunc(authController.Register))).Methods(http.MethodPost, http.MethodOptions)
|
||||
authRouter.Handle("/code-activation", server.ipRateLimiter.Limit(http.HandlerFunc(authController.ActivateAccount))).Methods(http.MethodPatch, http.MethodOptions)
|
||||
authRouter.Handle("/forgot-password", server.ipRateLimiter.Limit(http.HandlerFunc(authController.ForgotPassword))).Methods(http.MethodPost, http.MethodOptions)
|
||||
authRouter.Handle("/resend-email/{email}", server.ipRateLimiter.Limit(http.HandlerFunc(authController.ResendEmail))).Methods(http.MethodPost, http.MethodOptions)
|
||||
authRouter.Handle("/reset-password", server.ipRateLimiter.Limit(http.HandlerFunc(authController.ResetPassword))).Methods(http.MethodPost, http.MethodOptions)
|
||||
|
@ -87,7 +87,7 @@ func (chore *Chore) Run(ctx context.Context) (err error) {
|
||||
chore.log.Error("error generating activation token", zap.Error(err))
|
||||
return nil
|
||||
}
|
||||
authController := consoleapi.NewAuth(chore.log, nil, nil, nil, nil, nil, "", chore.address, "", "", "", "")
|
||||
authController := consoleapi.NewAuth(chore.log, nil, nil, nil, nil, nil, "", chore.address, "", "", "", "", false)
|
||||
|
||||
link := authController.ActivateAccountURL + "?token=" + token
|
||||
|
||||
|
@ -6,10 +6,12 @@ package console
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
"sort"
|
||||
@ -109,6 +111,9 @@ var (
|
||||
// ErrLoginCredentials occurs when provided invalid login credentials.
|
||||
ErrLoginCredentials = errs.Class("login credentials")
|
||||
|
||||
// ErrActivationCode is error class for failed signup code activation.
|
||||
ErrActivationCode = errs.Class("activation code")
|
||||
|
||||
// ErrChangePassword occurs when provided old password is incorrect.
|
||||
ErrChangePassword = errs.Class("change password")
|
||||
|
||||
@ -837,6 +842,8 @@ func (s *Service) CreateUser(ctx context.Context, user CreateUser, tokenSecret R
|
||||
HaveSalesContact: user.HaveSalesContact,
|
||||
SignupPromoCode: user.SignupPromoCode,
|
||||
SignupCaptcha: captchaScore,
|
||||
ActivationCode: user.ActivationCode,
|
||||
SignupId: user.SignupId,
|
||||
}
|
||||
|
||||
if user.UserAgent != nil {
|
||||
@ -1026,17 +1033,57 @@ func (s *Service) ActivateAccount(ctx context.Context, activationToken string) (
|
||||
return nil, Error.Wrap(err)
|
||||
}
|
||||
|
||||
err = s.SetAccountActive(ctx, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// SetAccountActive - is a method for setting user account status to Active and sending
|
||||
// event to hubspot.
|
||||
func (s *Service) SetAccountActive(ctx context.Context, user *User) (err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
status := Active
|
||||
err = s.store.Users().Update(ctx, user.ID, UpdateUserRequest{
|
||||
Status: &status,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, Error.Wrap(err)
|
||||
return Error.Wrap(err)
|
||||
}
|
||||
s.auditLog(ctx, "activate account", &user.ID, user.Email)
|
||||
|
||||
s.auditLog(ctx, "activate account", &user.ID, user.Email)
|
||||
s.analytics.TrackAccountVerified(user.ID, user.Email)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetActivationCodeAndSignupID - generates and updates a new code for user's signup verification.
|
||||
// It updates the request ID associated with the signup as well.
|
||||
func (s *Service) SetActivationCodeAndSignupID(ctx context.Context, user User) (_ User, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
randNum, err := rand.Int(rand.Reader, big.NewInt(900000))
|
||||
if err != nil {
|
||||
return User{}, Error.Wrap(err)
|
||||
}
|
||||
randNum = randNum.Add(randNum, big.NewInt(100000))
|
||||
code := randNum.String()
|
||||
|
||||
requestID := requestid.FromContext(ctx)
|
||||
err = s.store.Users().Update(ctx, user.ID, UpdateUserRequest{
|
||||
ActivationCode: &code,
|
||||
SignupId: &requestID,
|
||||
})
|
||||
if err != nil {
|
||||
return User{}, Error.Wrap(err)
|
||||
}
|
||||
|
||||
user.SignupId = requestID
|
||||
user.ActivationCode = code
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
|
@ -1366,6 +1366,27 @@ func TestUserSettings(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetActivationCodeAndSignupID(t *testing.T) {
|
||||
testplanet.Run(t, testplanet.Config{
|
||||
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 1,
|
||||
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
|
||||
sat := planet.Satellites[0]
|
||||
srv := sat.API.Console.Service
|
||||
|
||||
existingUser, _, err := srv.GetUserByEmailWithUnverified(ctx, planet.Uplinks[0].User[sat.ID()].Email)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, existingUser.ActivationCode)
|
||||
|
||||
updatedUser, err := srv.SetActivationCodeAndSignupID(ctx, *existingUser)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, updatedUser.ActivationCode)
|
||||
|
||||
updatedUser2, err := srv.SetActivationCodeAndSignupID(ctx, *existingUser)
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, updatedUser.ActivationCode, updatedUser2.ActivationCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRESTKeys(t *testing.T) {
|
||||
testplanet.Run(t, testplanet.Config{
|
||||
SatelliteCount: 1, StorageNodeCount: 0, UplinkCount: 1,
|
||||
|
Loading…
Reference in New Issue
Block a user