Resend account activation email (#1961)

This commit is contained in:
Bogdan Artemenko 2019-05-15 11:28:36 +03:00 committed by GitHub
parent 2692c83cfe
commit 38acc4dba6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 224 additions and 31 deletions

View File

@ -86,7 +86,6 @@ func rootMutation(log *zap.Logger, service *console.Service, mailService *mailse
if err != nil {
return nil, err
}
log.Error("register: failed to create account",
zap.String("rawSecret", secretInput),
zap.Error(err))

View File

@ -101,6 +101,11 @@ func graphqlProject(service *console.Service, types *TypeCreator) *graphql.Objec
return nil, err
}
_, err = console.GetAuth(p.Context)
if err != nil {
return nil, err
}
var users []projectMember
for _, member := range members {
user, err := service.GetUser(p.Context, member.MemberID)

View File

@ -28,6 +28,8 @@ const (
TokenQuery = "token"
// ForgotPasswordQuery is a query name for password recovery request
ForgotPasswordQuery = "forgotPassword"
// ResendAccountActivationEmailQuery is a query name for password recovery request
ResendAccountActivationEmailQuery = "resendAccountActivationEmail"
)
// rootQuery creates query for graphql populated by AccountsClient
@ -47,6 +49,10 @@ func rootQuery(service *console.Service, mailService *mailservice.Service, types
if err != nil {
return nil, err
}
_, err = console.GetAuth(p.Context)
if err != nil {
return nil, err
}
return service.GetUser(p.Context, *id)
},
@ -140,6 +146,54 @@ func rootQuery(service *console.Service, mailService *mailservice.Service, types
)
}()
return true, nil
},
},
ResendAccountActivationEmailQuery: &graphql.Field{
Type: graphql.Boolean,
Args: graphql.FieldConfigArgument{
FieldID: &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
id, _ := p.Args[FieldID].(string)
userID, err := uuid.Parse(id)
if err != nil {
return false, err
}
user, err := service.GetUser(p.Context, *userID)
if err != nil {
return false, err
}
token, err := service.GenerateActivationToken(p.Context, user.ID, user.Email)
if err != nil {
return false, err
}
rootObject := p.Info.RootValue.(map[string]interface{})
origin := rootObject["origin"].(string)
link := origin + rootObject[ActivationPath].(string) + token
userName := user.ShortName
if user.ShortName == "" {
userName = user.FullName
}
// TODO: think of a better solution
go func() {
_ = mailService.SendRendered(
p.Context,
[]post.Address{{Address: user.Email, Name: userName}},
&AccountActivationEmail{
Origin: origin,
ActivationLink: link,
},
)
}()
return true, nil
},
},

View File

@ -513,5 +513,33 @@ func TestGraphqlQuery(t *testing.T) {
ok := data[consoleql.ForgotPasswordQuery].(bool)
assert.True(t, ok)
})
t.Run("Resend activation email query", func(t *testing.T) {
regToken, err := service.CreateRegToken(ctx, 2)
if err != nil {
t.Fatal(err)
}
user, err := service.CreateUser(authCtx, console.CreateUser{
UserInfo: console.UserInfo{
FullName: "Example User",
ShortName: "Example",
Email: "user1@example.com",
},
Password: "123a123",
}, regToken.Secret)
if err != nil {
t.Fatal(err)
}
query := fmt.Sprintf("query {resendAccountActivationEmail(id: \"%s\")}", user.ID)
result := testQuery(t, query)
assert.NotNil(t, result)
data := result.(map[string]interface{})
ok := data[consoleql.ResendAccountActivationEmailQuery].(bool)
assert.True(t, ok)
})
})
}

View File

@ -299,10 +299,6 @@ func (s *Service) Token(ctx context.Context, email, password string) (token stri
// GetUser returns User by id
func (s *Service) GetUser(ctx context.Context, id uuid.UUID) (u *User, err error) {
defer mon.Task()(&ctx)(&err)
_, err = GetAuth(ctx)
if err != nil {
return nil, err
}
user, err := s.store.Users().Get(ctx, id)
if err != nil {

View File

@ -111,11 +111,11 @@ export async function forgotPasswordRequest(email: string): Promise<RequestRespo
}
// Performs Create user graqhQL request.
export async function createUserRequest(user: User, password: string, secret: string): Promise<RequestResponse<null>> {
let result: RequestResponse<null> = {
export async function createUserRequest(user: User, password: string, secret: string): Promise<RequestResponse<string>> {
let result: RequestResponse<string> = {
errorMessage: '',
isSuccess: false,
data: null
data: ''
};
let response = await apolloManager.mutate(
@ -130,7 +130,7 @@ export async function createUserRequest(user: User, password: string, secret: st
shortName: "${user.shortName}",
},
secret: "${secret}",
){email}
){email, id}
}`
),
fetchPolicy: 'no-cache',
@ -142,6 +142,9 @@ export async function createUserRequest(user: User, password: string, secret: st
result.errorMessage = response.errors[0].message;
} else {
result.isSuccess = true;
if (response.data) {
result.data = response.data.createUser.id;
}
}
return result;
@ -250,3 +253,31 @@ export async function deleteAccountRequest(password: string): Promise<RequestRes
return result;
}
export async function resendEmailRequest(userID: string): Promise<RequestResponse<null>> {
let result: RequestResponse<null> = {
errorMessage: '',
isSuccess: false,
data: null
};
let response = await apolloManager.query(
{
query: gql(`
query {
resendAccountActivationEmail(id: "${userID}")
}`
),
fetchPolicy: 'no-cache',
errorPolicy: 'all',
}
);
if (response.errors) {
result.errorMessage = response.errors[0].message;
} else {
result.isSuccess = true;
}
return result;
}

View File

@ -8,28 +8,71 @@ import { Component, Vue } from 'vue-property-decorator';
import Button from '@/components/common/Button.vue';
import { APP_STATE_ACTIONS } from '@/utils/constants/actionNames';
import ROUTES from '@/utils/constants/routerConstants';
import { resendEmailRequest } from '../../api/users';
import { getUserID } from '@/utils/consoleLocalStorage';
@Component(
{
computed:{
isPopupShown: function () {
return this.$store.state.appStateModule.appState.isSuccessfulRegistrationPopupShown;
}
},
methods: {
onCloseClick: function () {
this.$store.dispatch(APP_STATE_ACTIONS.CLOSE_POPUPS);
this.$router.push(ROUTES.LOGIN.path);
}
},
components: {
Button,
},
}
)
{
beforeDestroy: function() {
if (this.$data.intervalID) {
clearInterval(this.$data.intervalID);
}
},
data: function () {
return {
isResendEmailButtonDisabled: true,
timeToEnableResendEmailButton: '00:30',
intervalID: null,
};
},
computed: {
isPopupShown: function () {
return this.$store.state.appStateModule.appState.isSuccessfulRegistrationPopupShown;
}
},
methods: {
onResendEmailButtonClick: async function () {
this.$data.isResendEmailButtonDisabled = true;
export default class RegistrationSuccessPopup extends Vue {
let userID = getUserID();
if (!userID) {
return;
}
let response = await resendEmailRequest(userID);
if (response.isSuccess) {
(this as any).startResendEmailCountdown();
}
},
onCloseClick: function () {
this.$store.dispatch(APP_STATE_ACTIONS.CLOSE_POPUPS);
this.$router.push(ROUTES.LOGIN.path);
},
startResendEmailCountdown: function () {
let countdown = 30;
let self = this;
this.$data.intervalID = setInterval(function () {
countdown--;
let secondsLeft = countdown > 9 ? countdown : `0${countdown}`;
self.$data.timeToEnableResendEmailButton = `00:${secondsLeft}`;
if (countdown <= 0) {
clearInterval(self.$data.intervalID);
self.$data.isResendEmailButtonDisabled = false;
}
}.bind(this), 1000);
}
},
components: {
Button,
},
}
)
export default class RegistrationSuccessPopup extends Vue {
}
</script>
<style scoped lang="scss">
@ -42,6 +85,19 @@ import ROUTES from '@/utils/constants/routerConstants';
margin: 0;
}
h3 {
font-family: 'font_medium';
font-size: 12px;
line-height: 16px;
color: #354049;
padding: 27px 0 0 0;
margin: 0;
}
b {
color: #2683FF;
}
a {
font-family: 'font_bold';
color: #2683ff;
@ -59,6 +115,7 @@ import ROUTES from '@/utils/constants/routerConstants';
justify-content: center;
align-items: center;
}
.register-success-popup {
width: 100%;
max-width: 845px;

View File

@ -18,13 +18,13 @@
<div class="register-success-popup__form-container">
<h2 class="register-success-popup__form-container__main-label-text">Verify Your Email</h2>
<p>You have almost finished registering your account on the Tardigrade Satellite. Only one step left. Please check your inbox for a verification email.</p>
<!--<p>Didnt receive a verification email?</p>-->
<h3>Didnt receive a verification email?<b> {{timeToEnableResendEmailButton}}</b></h3>
<div class="register-success-popup__form-container__button-container">
<Button label="Go to Login" width="450px" height="50px" :onPress="onCloseClick" isWhite />
<Button label="Resend Email" width="450px" height="50px" :onPress="onResendEmailButtonClick" :isDisabled="isResendEmailButtonDisabled"/>
</div>
</div>
<div class="register-success-popup__close-cross-container">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" v-on:click="onCloseClick">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" v-on:click="onCloseClick" >
<path d="M15.7071 1.70711C16.0976 1.31658 16.0976 0.683417 15.7071 0.292893C15.3166 -0.0976311 14.6834 -0.0976311 14.2929 0.292893L15.7071 1.70711ZM0.292893 14.2929C-0.0976311 14.6834 -0.0976311 15.3166 0.292893 15.7071C0.683417 16.0976 1.31658 16.0976 1.70711 15.7071L0.292893 14.2929ZM1.70711 0.292893C1.31658 -0.0976311 0.683417 -0.0976311 0.292893 0.292893C-0.0976311 0.683417 -0.0976311 1.31658 0.292893 1.70711L1.70711 0.292893ZM14.2929 15.7071C14.6834 16.0976 15.3166 16.0976 15.7071 15.7071C16.0976 15.3166 16.0976 14.6834 15.7071 14.2929L14.2929 15.7071ZM14.2929 0.292893L0.292893 14.2929L1.70711 15.7071L15.7071 1.70711L14.2929 0.292893ZM0.292893 1.70711L14.2929 15.7071L15.7071 14.2929L1.70711 0.292893L0.292893 1.70711Z" fill="#384B65"/>
</svg>
</div>

View File

@ -0,0 +1,15 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
const localStorageConstants = {
USER_ID: 'userID',
USER_EMAIL: 'userEmail'
};
export function setUserId(userID: string) {
localStorage.setItem(localStorageConstants.USER_ID, userID);
}
export function getUserID() {
return localStorage.getItem(localStorageConstants.USER_ID);
}

View File

@ -13,6 +13,7 @@ import ROUTES from '../../utils/constants/routerConstants';
import { LOADING_CLASSES } from '../../utils/constants/classConstants';
import { APP_STATE_ACTIONS, NOTIFICATION_ACTIONS } from '../../utils/constants/actionNames';
import { createUserRequest } from '../../api/users';
import { setUserId } from '@/utils/consoleLocalStorage';
@Component(
{
@ -97,7 +98,14 @@ import { createUserRequest } from '../../api/users';
return;
}
if (response.data) {
setUserId(response.data);
}
this.$store.dispatch(APP_STATE_ACTIONS.TOGGLE_SUCCESSFUL_REGISTRATION_POPUP);
if (this.$refs['register_success_popup'] !== null) {
(this.$refs['register_success_popup'] as any).startResendEmailCountdown();
}
},
onCreateClick: function (): any {
let self = this as any;

View File

@ -88,5 +88,5 @@
</div>
</div>
</div>
<RegistrationSuccessPopup />
<RegistrationSuccessPopup ref="register_success_popup"/>
</div>