diff --git a/satellite/console/consoleweb/consoleapi/auth.go b/satellite/console/consoleweb/consoleapi/auth.go index 58a87a6f2..d509cce6a 100644 --- a/satellite/console/consoleweb/consoleapi/auth.go +++ b/satellite/console/consoleweb/consoleapi/auth.go @@ -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" } diff --git a/satellite/console/consoleweb/consoleapi/auth_test.go b/satellite/console/consoleweb/consoleapi/auth_test.go index 0bbdc2c16..5c313a766 100644 --- a/satellite/console/consoleweb/consoleapi/auth_test.go +++ b/satellite/console/consoleweb/consoleapi/auth_test.go @@ -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)), "", "", 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)), "", "", 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)), "", "", 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") + }) +} diff --git a/satellite/console/consoleweb/server.go b/satellite/console/consoleweb/server.go index 68a5a9d60..5ce997756 100644 --- a/satellite/console/consoleweb/server.go +++ b/satellite/console/consoleweb/server.go @@ -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) diff --git a/satellite/console/emailreminders/chore.go b/satellite/console/emailreminders/chore.go index 424cc7045..ba17933c1 100644 --- a/satellite/console/emailreminders/chore.go +++ b/satellite/console/emailreminders/chore.go @@ -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 diff --git a/satellite/console/service.go b/satellite/console/service.go index 7bdc7b63f..af4a0e793 100644 --- a/satellite/console/service.go +++ b/satellite/console/service.go @@ -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 } diff --git a/satellite/console/service_test.go b/satellite/console/service_test.go index e1f888251..cca0c9ab6 100644 --- a/satellite/console/service_test.go +++ b/satellite/console/service_test.go @@ -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,