web/satellite user api simplification (#2787)

This commit is contained in:
Yehor Butko 2019-08-14 21:11:18 +03:00 committed by GitHub
parent 2c769fe9d9
commit 012775f874
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 598 additions and 692 deletions

View File

@ -0,0 +1,163 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import { BaseGql } from '@/api/baseGql';
import { User } from '@/types/users';
/**
* AuthApiGql is a graphql implementation of Auth API.
* Exposes all auth-related functionality
*/
export class AuthApi extends BaseGql {
/**
* Used to resend an registration confirmation email
*
* @param userId - id of newly created user
* @throws Error
*/
public async resendEmail(userId: string): Promise<void> {
const query =
`query ($userId: String!){
resendAccountActivationEmail(id: $userId)
}`;
const variables = {
userId,
};
await this.query(query, variables);
}
/**
* Used to get authentication token
*
* @param email - email of the user
* @param password - password of the user
* @throws Error
*/
public async token(email: string, password: string): Promise<string> {
const query =
` query ($email: String!, $password: String!) {
token(email: $email, password: $password) {
token
}
}`;
const variables = {
email,
password,
};
const response = await this.query(query, variables);
return response.data.token.token;
}
/**
* Used to restore password
*
* @param email - email of the user
* @throws Error
*/
public async forgotPassword(email: string): Promise<void> {
const query =
`query($email: String!) {
forgotPassword(email: $email)
}`;
const variables = {
email,
};
await this.query(query, variables);
}
/**
* Used to change password
*
* @param password - old password of the user
* @param newPassword - new password of the user
* @throws Error
*/
public async changePassword(password: string, newPassword: string): Promise<void> {
const query =
`mutation($password: String!, $newPassword: String!) {
changePassword (
password: $password,
newPassword: $newPassword
) {
email
}
}`;
const variables = {
password,
newPassword,
};
await this.mutate(query, variables);
}
/**
* Used to delete account
*
* @param password - password of the user
* @throws Error
*/
public async delete(password: string): Promise<void> {
const query =
`mutation ($password: String!){
deleteAccount(password: $password) {
email
}
}`;
const variables = {
password,
};
await this.mutate(query, variables);
}
// TODO: remove secret after Vanguard release
/**
* Used to create account
*
* @param user - stores user information
* @param secret - registration token used in Vanguard release
* @param refUserId - referral id to participate in bonus program
* @returns id of created user
* @throws Error
*/
public async create(user: User, password: string, secret: string, referrerUserId: string = ''): Promise<string> {
const query =
`mutation($email: String!, $password: String!, $fullName: String!, $shortName: String!,
$partnerID: String!, $referrerUserId: String!, $secret: String!) {
createUser(
input: {
email: $email,
password: $password,
fullName: $fullName,
shortName: $shortName,
partnerId: $partnerID
},
referrerUserId: $referrerUserId,
secret: $secret,
) {email, id}
}`;
const variables = {
email: user.email,
fullName: user.fullName,
shortName: user.shortName,
partnerID: user.partnerId ? user.partnerId : '',
referrerUserId: referrerUserId ? referrerUserId : '',
password,
secret,
};
const response = await this.mutate(query, variables);
return response.data.createUser.id;
}
}

View File

@ -0,0 +1,62 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import gql from 'graphql-tag';
import apolloManager from '@/utils/apolloManager';
/**
* BaseGql is a graphql utility which allows to perform queries and mutations
*/
export class BaseGql {
/**
* performs qraphql query
*
* @param query - qraphql query
* @param variables - variables to bind in query. null by default.
* @throws Error
*/
protected async query(query: string, variables: any = null): Promise<any> {
let response: any = await apolloManager.query(
{
query: gql(query),
variables,
fetchPolicy: 'no-cache',
errorPolicy: 'all',
}
);
if (response.errors) {
throw new Error(this.combineErrors(response.errors));
}
return response;
}
/**
* performs qraphql mutation
*
* @param query - qraphql query
* @param variables - variables to bind in query. null by default.
* @throws Error
*/
protected async mutate(query: string, variables: any = null): Promise<any> {
let response: any = await apolloManager.mutate(
{
mutation: gql(query),
variables,
fetchPolicy: 'no-cache',
errorPolicy: 'all',
}
);
if (response.errors) {
throw new Error(this.combineErrors(response.errors));
}
return response;
}
private combineErrors(gqlError: any): string {
return gqlError.map(err => err.message).join('\n');
}
}

View File

@ -1,276 +1,68 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import apolloManager from '@/utils/apolloManager';
import gql from 'graphql-tag';
import { UpdatedUser, User } from '@/types/users';
import { RequestResponse } from '@/types/response';
import { BaseGql } from '@/api/baseGql';
import { UpdatedUser, User, UsersApi } from '@/types/users';
// Performs update user info graphQL mutation request.
// Returns User object if succeed, null otherwise
export async function updateAccountRequest(user: UpdatedUser): Promise<RequestResponse<User>> {
let result: RequestResponse<User> = new RequestResponse<User>();
/**
* UsersApiGql is a graphql implementation of Users API.
* Exposes all user-related functionality
*/
export class UsersApiGql extends BaseGql implements UsersApi {
let response: any = await apolloManager.mutate(
{
mutation: gql(`
mutation {
updateAccount (
input: {
fullName: $fullName,
shortName: $shortName
}
) {
email,
fullName,
shortName
/**
* Updates users full name and short name
*
* @param user - contains information that should be updated
* @throws Error
*/
public async update(user: UpdatedUser): Promise<void> {
const query: string =
`mutation ($fullName: String!, $shortName: String!) {
updateAccount (
input: {
fullName: $fullName,
shortName: $shortName
}
}`,
),
variables: {
fullName: user.fullName,
shortName: user.fullName,
},
fetchPolicy: 'no-cache',
errorPolicy: 'all',
}
);
) {
email,
fullName,
shortName
}
}`;
if (response.errors) {
result.errorMessage = response.errors[0].message;
} else {
result.isSuccess = true;
result.data = response.data.updateAccount;
const variables: any = {
fullName: user.fullName,
shortName: user.shortName,
};
await this.mutate(query, variables);
}
return result;
}
/**
* Fetch user
*
* @returns User
* @throws Error
*/
public async get(): Promise<User> {
const query =
` query {
user {
id,
fullName,
shortName,
email,
partnerId,
}
}`;
// Performs change password graphQL mutation
// Returns base user fields
export async function changePasswordRequest(password: string, newPassword: string): Promise<RequestResponse<null>> {
let result: RequestResponse<null> = new RequestResponse<null>();
const response = await this.query(query);
let response: any = await apolloManager.mutate(
{
mutation: gql(`
mutation($password: String!, $newPassword: String!) {
changePassword (
password: $password,
newPassword: $newPassword
) {
email
}
}`
),
variables: {
password: password,
newPassword: newPassword
},
fetchPolicy: 'no-cache',
errorPolicy: 'all',
}
);
if (response.errors) {
result.errorMessage = response.errors[0].message;
} else {
result.isSuccess = true;
return this.fromJson(response.data.user);
}
return result;
}
export async function forgotPasswordRequest(email: string): Promise<RequestResponse<null>> {
let result: RequestResponse<null> = new RequestResponse<null>();
let response: any = await apolloManager.query(
{
query: gql(`
query($email: String!) {
forgotPassword(email: $email)
}`),
variables: {
email: email
},
fetchPolicy: 'no-cache',
errorPolicy: 'all',
},
);
if (response.errors) {
result.errorMessage = response.errors[0].message;
} else {
result.isSuccess = true;
private fromJson(user): User {
return new User(user.id, user.fullName, user.shortName, user.email, user.partnerId);
}
return result;
}
// Performs Create user graqhQL request.
export async function createUserRequest(user: User, password: string, secret: string, refUserId?: string): Promise<RequestResponse<string>> {
let result: RequestResponse<string> = new RequestResponse<string>();
let response = await apolloManager.mutate(
{
mutation: gql(`
mutation($email: String!, $password: String!, $fullName: String!, $shortName: String!, $partnerID: String!, $referrerUserID: String!, $secret: String!) {
createUser(
input:{
email: $email,
password: $password,
fullName: $fullName,
shortName: $shortName,
partnerId: $partnerID
},
referrerUserId: $referrerUserID,
secret: $secret,
){email, id}
}`
),
variables: {
email: user.email,
password: password,
fullName: user.fullName,
shortName: user.shortName,
partnerID: user.partnerId ? user.partnerId : '',
referrerUserID: refUserId ? refUserId : '',
secret: secret
},
fetchPolicy: 'no-cache',
errorPolicy: 'all',
}
);
if (response.errors) {
result.errorMessage = response.errors[0].message;
} else {
result.isSuccess = true;
if (response.data) {
result.data = response.data.createUser.id;
}
}
return result;
}
// Performs graqhQL request.
// Returns Token.
export async function getTokenRequest(email: string, password: string): Promise<RequestResponse<string>> {
let result: RequestResponse<string> = new RequestResponse<string>();
let response: any = await apolloManager.query(
{
query: gql(`
query ($email: String!, $password: String!) {
token(email: $email, password: $password) {
token
}
}`
),
variables: {
email: email,
password: password
},
fetchPolicy: 'no-cache',
errorPolicy: 'all',
}
);
if (response.errors) {
result.errorMessage = response.errors[0].message;
} else {
result.isSuccess = true;
result.data = response.data.token.token;
}
return result;
}
// Performs graqhQL request.
// Returns User object.
export async function getUserRequest(): Promise<RequestResponse<User>> {
let result: RequestResponse<User> = new RequestResponse<User>();
let response: any = await apolloManager.query(
{
query: gql(`
query {
user {
fullName,
shortName,
email,
}
}`
),
fetchPolicy: 'no-cache',
errorPolicy: 'all',
}
);
if (response.errors) {
result.errorMessage = response.errors[0].message;
} else {
result.isSuccess = true;
result.data = response.data.user;
}
return result;
}
// Performs graqhQL request.
export async function deleteAccountRequest(password: string): Promise<RequestResponse<null>> {
let result: RequestResponse<null> = new RequestResponse<null>();
let response = await apolloManager.mutate(
{
mutation: gql(`
mutation ($password: String!){
deleteAccount(password: $password) {
email
}
}`
),
variables: {
password: password
},
fetchPolicy: 'no-cache',
errorPolicy: 'all',
}
);
if (response.errors) {
result.errorMessage = response.errors[0].message;
} else {
result.isSuccess = true;
}
return result;
}
export async function resendEmailRequest(userID: string): Promise<RequestResponse<null>> {
let result: RequestResponse<null> = new RequestResponse<null>();
let response = await apolloManager.query(
{
query: gql(`
query ($userID: String!){
resendAccountActivationEmail(id: $userID)
}`
),
variables: {
userID: userID
},
fetchPolicy: 'no-cache',
errorPolicy: 'all',
}
);
if (response.errors) {
result.errorMessage = response.errors[0].message;
} else {
result.isSuccess = true;
}
return result;
}

View File

@ -63,9 +63,9 @@
import { Component, Vue } from 'vue-property-decorator';
import HeaderlessInput from '@/components/common/HeaderlessInput.vue';
import Button from '@/components/common/Button.vue';
import { USER_ACTIONS, NOTIFICATION_ACTIONS, APP_STATE_ACTIONS } from '@/utils/constants/actionNames';
import { NOTIFICATION_ACTIONS, APP_STATE_ACTIONS } from '@/utils/constants/actionNames';
import { validatePassword } from '@/utils/validation';
import { RequestResponse } from '@/types/response';
import { AuthApi } from '@/api/auth';
@Component({
components: {
@ -81,6 +81,8 @@
private newPasswordError: string = '';
private confirmationPasswordError: string = '';
private readonly auth: AuthApi = new AuthApi();
public setOldPassword(value: string): void {
this.oldPassword = value;
this.oldPasswordError = '';
@ -122,63 +124,21 @@
return;
}
let response: RequestResponse<object> = await this.$store.dispatch(USER_ACTIONS.CHANGE_PASSWORD,
{
oldPassword: this.oldPassword,
newPassword: this.newPassword
}
);
if (!response.isSuccess) {
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, response.errorMessage);
try {
await this.auth.changePassword(this.oldPassword, this.newPassword);
} catch (error) {
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, error.message);
return;
}
this.$store.dispatch(NOTIFICATION_ACTIONS.SUCCESS, 'Password successfully changed!');
this.$store.dispatch(APP_STATE_ACTIONS.TOGGLE_CHANGE_PASSWORD_POPUP);
this.oldPassword = '';
this.newPassword = '';
this.confirmationPassword = '';
this.oldPasswordError = '';
this.newPasswordError = '';
this.confirmationPasswordError = '';
let oldPasswordInput: any = this.$refs['oldPasswordInput'];
oldPasswordInput.setValue('');
let newPasswordInput: any = this.$refs['newPasswordInput'];
newPasswordInput.setValue('');
let confirmPasswordInput: any = this.$refs['confirmPasswordInput'];
confirmPasswordInput.setValue('');
}
public onCloseClick(): void {
this.cancel();
this.$store.dispatch(APP_STATE_ACTIONS.TOGGLE_CHANGE_PASSWORD_POPUP);
}
private cancel(): void {
this.oldPassword = '';
this.newPassword = '';
this.confirmationPassword = '';
this.oldPasswordError = '';
this.newPasswordError = '';
this.confirmationPasswordError = '';
let oldPasswordInput: any = this.$refs['oldPasswordInput'];
oldPasswordInput.setValue('');
let newPasswordInput: any = this.$refs['newPasswordInput'];
newPasswordInput.setValue('');
let confirmPasswordInput: any = this.$refs['confirmPasswordInput'];
confirmPasswordInput.setValue('');
}
}
</script>

View File

@ -64,11 +64,12 @@
<script lang='ts'>
import { Component, Vue } from 'vue-property-decorator';
import HeaderedInput from '@/components/common/HeaderedInput.vue';
import { AuthApi } from '@/api/auth';
import Button from '@/components/common/Button.vue';
import HeaderedInput from '@/components/common/HeaderedInput.vue';
import { APP_STATE_ACTIONS, NOTIFICATION_ACTIONS } from '@/utils/constants/actionNames';
import { AuthToken } from '@/utils/authToken';
import { APP_STATE_ACTIONS, USER_ACTIONS, NOTIFICATION_ACTIONS } from '@/utils/constants/actionNames';
import { RequestResponse } from '@/types/response';
import ROUTES from '@/utils/constants/routerConstants';
@Component({
components: {
@ -81,6 +82,8 @@
private password: string = '';
private isLoading: boolean = false;
private readonly auth: AuthApi = new AuthApi();
public setPassword(value: string): void {
this.password = value;
}
@ -92,20 +95,18 @@
this.isLoading = true;
let response: RequestResponse<object> = await this.$store.dispatch(USER_ACTIONS.DELETE, this.password);
try {
await this.auth.delete(this.password);
this.$store.dispatch(NOTIFICATION_ACTIONS.SUCCESS, 'Account was successfully deleted');
AuthToken.remove();
if (!response.isSuccess) {
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, response.errorMessage);
this.isLoading = false;
return;
this.$router.push(ROUTES.LOGIN.path);
} catch (error) {
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, error.message);
this.isLoading = false;
}
this.$store.dispatch(NOTIFICATION_ACTIONS.SUCCESS, 'Account was successfully deleted');
AuthToken.remove();
this.isLoading = false;
this.$router.push('/login');
}
public onCloseClick(): void {

View File

@ -18,7 +18,7 @@
width="100%"
ref="fullNameInput"
:error="fullNameError"
:initValue="originalFullName"
:initValue="userInfo.fullName"
@setData="setFullName" />
<HeaderedInput
class="full-input"
@ -26,7 +26,7 @@
placeholder="Enter Nickname"
width="100%"
ref="shortNameInput"
:initValue="originalShortName"
:initValue="userInfo.shortName"
@setData="setShortName"/>
<div class="edit-profile-popup__form-container__button-container">
<Button label="Cancel" width="205px" height="48px" :onPress="onCloseClick" isWhite="true" />
@ -47,6 +47,7 @@
import HeaderedInput from '@/components/common/HeaderedInput.vue';
import Button from '@/components/common/Button.vue';
import { USER_ACTIONS, NOTIFICATION_ACTIONS, APP_STATE_ACTIONS } from '@/utils/constants/actionNames';
import { UpdatedUser } from '@/types/users';
@Component({
components: {
@ -55,61 +56,41 @@
}
})
export default class EditProfilePopup extends Vue {
private originalFullName: string = this.$store.getters.user.fullName;
private originalShortName: string = this.$store.getters.user.shortName;
private fullName: string = this.$store.getters.user.fullName;
private shortName: string = this.$store.getters.user.shortName;
private fullNameError: string = '';
private readonly userInfo: UpdatedUser =
new UpdatedUser(this.$store.getters.user.fullName, this.$store.getters.user.shortName);
public setFullName(value: string): void {
this.fullName = value.trim();
this.userInfo.setFullName(value);
this.fullNameError = '';
}
public setShortName(value: string): void {
this.shortName = value.trim();
}
public cancel(): void {
this.fullName = this.originalFullName;
this.fullNameError = '';
this.shortName = this.originalShortName;
let fullNameInput: any = this.$refs['fullNameInput'];
fullNameInput.setValue(this.originalFullName);
let shortNameInput: any = this.$refs['shortNameInput'];
shortNameInput.setValue(this.originalShortName);
this.userInfo.setShortName(value);
}
public async onUpdateClick(): Promise<void> {
if (!this.fullName) {
if (!this.userInfo.isValid()) {
this.fullNameError = 'Full name expected';
return;
}
let user = {
fullName: this.fullName,
shortName: this.shortName,
};
let response = await this.$store.dispatch(USER_ACTIONS.UPDATE, user);
if (!response.isSuccess) {
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, response.errorMessage);
try {
await this.$store.dispatch(USER_ACTIONS.UPDATE, this.userInfo);
} catch (error) {
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, error.message);
return;
}
this.$store.dispatch(NOTIFICATION_ACTIONS.SUCCESS, 'Account info successfully updated!');
this.originalFullName = this.$store.getters.user.fullName;
this.originalShortName = this.$store.getters.user.shortName;
this.$store.dispatch(APP_STATE_ACTIONS.TOGGLE_EDIT_PROFILE_POPUP);
}
public onCloseClick(): void {
this.cancel();
this.$store.dispatch(APP_STATE_ACTIONS.TOGGLE_EDIT_PROFILE_POPUP);
}

View File

@ -6,10 +6,10 @@
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import Button from '@/components/common/Button.vue';
import { APP_STATE_ACTIONS } from '@/utils/constants/actionNames';
import { APP_STATE_ACTIONS, NOTIFICATION_ACTIONS } from '@/utils/constants/actionNames';
import ROUTES from '@/utils/constants/routerConstants';
import { resendEmailRequest } from '../../api/users';
import { getUserID } from '@/utils/consoleLocalStorage';
import { AuthApi } from '@/api/auth';
import { getUserId } from '@/utils/consoleLocalStorage';
@Component({
components: {
@ -21,6 +21,8 @@
private timeToEnableResendEmailButton: string = '00:30';
private intervalID: any = null;
private readonly auth: AuthApi = new AuthApi();
public beforeDestroy(): void {
if (this.intervalID) {
clearInterval(this.intervalID);
@ -30,15 +32,18 @@
public async onResendEmailButtonClick(): Promise<void> {
this.isResendEmailButtonDisabled = true;
let userID = getUserID();
if (!userID) {
const userId = getUserId();
if (!userId) {
return;
}
let response = await resendEmailRequest(userID);
if (response.isSuccess) {
this.startResendEmailCountdown();
try {
await this.auth.resendEmail(userId);
} catch (error) {
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, 'could not send email ');
}
this.startResendEmailCountdown();
}
public onCloseClick(): void {
@ -52,19 +57,18 @@
private startResendEmailCountdown(): void {
let countdown = 30;
let self = this;
this.intervalID = setInterval(function () {
this.intervalID = setInterval(() => {
countdown--;
let secondsLeft = countdown > 9 ? countdown : `0${countdown}`;
self.timeToEnableResendEmailButton = `00:${secondsLeft}`;
this.timeToEnableResendEmailButton = `00:${secondsLeft}`;
if (countdown <= 0) {
clearInterval(self.intervalID);
self.isResendEmailButtonDisabled = false;
clearInterval(this.intervalID);
this.isResendEmailButtonDisabled = false;
}
}.bind(this), 1000);
}, 1000);
}
}
</script>

View File

@ -4,8 +4,7 @@
import Vue from 'vue';
import Vuex from 'vuex';
import { usersModule } from '@/store/modules/users';
import { makeUsersModule } from '@/store/modules/users';
import { projectsModule } from '@/store/modules/projects';
import { projectMembersModule } from '@/store/modules/projectMembers';
import { notificationsModule } from '@/store/modules/notifications';
@ -13,13 +12,23 @@ import { appStateModule } from '@/store/modules/appState';
import { apiKeysModule } from '@/store/modules/apiKeys';
import { bucketUsageModule, usageModule, creditUsageModule } from '@/store/modules/usage';
import { projectPaymentsMethodsModule } from '@/store/modules/paymentMethods';
import { UsersApiGql } from '@/api/users';
Vue.use(Vuex);
export class StoreModule<S> {
public state: S;
public mutations: any;
public actions: any;
public getters: any;
}
const usersApi = new UsersApiGql();
// Satellite store (vuex)
const store = new Vuex.Store({
modules: {
usersModule,
usersModule: makeUsersModule(usersApi),
projectsModule,
projectMembersModule,
notificationsModule,

View File

@ -2,82 +2,68 @@
// See LICENSE for copying information.
import { USER_MUTATIONS } from '../mutationConstants';
import {
deleteAccountRequest,
updateAccountRequest,
changePasswordRequest,
getUserRequest,
} from '@/api/users';
import { UpdatedUser, UpdatePasswordModel, User } from '@/types/users';
import { RequestResponse } from '@/types/response';
import { UpdatedUser, User, UsersApi } from '@/types/users';
import { StoreModule } from '@/store';
export const usersModule = {
state: {
user: {
fullName: '',
shortName: '',
email: ''
}
},
const {
SET_USER,
UPDATE_USER,
CLEAR,
} = USER_MUTATIONS;
mutations: {
[USER_MUTATIONS.SET_USER_INFO](state: any, user: User): void {
state.user = user;
/**
* creates users module with all dependencies
*
* @param api - users api
*/
export function makeUsersModule(api: UsersApi): StoreModule<User> {
return {
state: new User(),
mutations: {
setUser(state: User, user: User): void {
state.id = user.id;
state.email = user.email;
state.shortName = user.shortName;
state.fullName = user.fullName;
state.partnerId = user.partnerId;
},
clearUser(state: User): void {
state.id = '';
state.email = '';
state.shortName = '';
state.fullName = '';
state.partnerId = '';
},
updateUser(state: User, user: UpdatedUser): void {
state.fullName = user.fullName;
state.shortName = user.shortName;
},
},
[USER_MUTATIONS.REVERT_TO_DEFAULT_USER_INFO](state: any): void {
state.user.fullName = '';
state.user.shortName = '';
state.user.email = '';
actions: {
updateUser: async function ({commit}: any, userInfo: UpdatedUser): Promise<void> {
await api.update(userInfo);
commit(UPDATE_USER, userInfo);
},
getUser: async function ({commit}: any): Promise<User> {
let user = await api.get();
commit(SET_USER, user);
return user;
},
clearUser: function({commit}: any) {
commit(CLEAR);
},
},
[USER_MUTATIONS.UPDATE_USER_INFO](state: any, user: User): void {
state.user = user;
getters: {
user: (state: User): User => state,
userName: (state: User): string => state.getFullName(),
},
[USER_MUTATIONS.CLEAR](state: any): void {
state.user = {
fullName: '',
shortName: '',
email: ''
};
},
},
actions: {
updateAccount: async function ({commit}: any, userInfo: UpdatedUser): Promise<RequestResponse<User>> {
let response = await updateAccountRequest(userInfo);
if (response.isSuccess) {
commit(USER_MUTATIONS.UPDATE_USER_INFO, response.data);
}
return response;
},
changePassword: async function ({state}: any, updateModel: UpdatePasswordModel): Promise<RequestResponse<null>> {
return await changePasswordRequest(updateModel.oldPassword, updateModel.newPassword);
},
deleteAccount: async function ({commit, state}: any, password: string): Promise<RequestResponse<null>> {
return await deleteAccountRequest(password);
},
getUser: async function ({commit}: any): Promise<RequestResponse<User>> {
let response = await getUserRequest();
if (response.isSuccess) {
commit(USER_MUTATIONS.SET_USER_INFO, response.data);
}
return response;
},
clearUser: function({commit}: any) {
commit(USER_MUTATIONS.CLEAR);
},
},
getters: {
user: (state: any) => {
return state.user;
},
userName: (state: any) => state.user.shortName == '' ? state.user.fullName : state.user.shortName
},
};
};
}

View File

@ -2,10 +2,9 @@
// See LICENSE for copying information.
export const USER_MUTATIONS = {
SET_USER_INFO: 'SET_USER_INFO',
REVERT_TO_DEFAULT_USER_INFO: 'REVERT_TO_DEFAULT_USER_INFO',
UPDATE_USER_INFO: 'UPDATE_USER_INFO',
CLEAR: 'CLEAR_USER',
SET_USER: 'setUser',
UPDATE_USER: 'updateUser',
CLEAR: 'clearUser',
};
export const PROJECTS_MUTATIONS = {

View File

@ -1,19 +1,42 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
/**
* Exposes all user-related functionality
*/
export interface UsersApi {
/**
* Updates users full name and short name
*
* @param user - contains information that should be updated
* @throws Error
*/
update(user: UpdatedUser): Promise<void>;
/**
* Fetch user
*
* @returns User
* @throws Error
*/
get(): Promise<User>;
}
/**
* User class holds info for User entity.
*/
export class User {
public id: string;
public fullName: string;
public shortName: string;
public email: string;
public partnerId?: string;
public partnerId: string;
public constructor(fullName: string, shortName: string, email: string, partnerId?: string) {
this.id = '';
public constructor(id: string = '', fullName: string = '', shortName: string = '', email: string = '', partnerId: string = '') {
this.id = id;
this.fullName = fullName;
this.shortName = shortName;
this.email = email;
this.partnerId = partnerId || '';
this.partnerId = partnerId;
}
public getFullName(): string {
@ -21,13 +44,27 @@ export class User {
}
}
/**
* User class holds info for updating User.
*/
export class UpdatedUser {
public fullName: string;
public shortName: string;
}
// Used in users module to pass parameters to action
export class UpdatePasswordModel {
public oldPassword: string;
public newPassword: string;
public constructor(fullName: string, shortName: string) {
this.fullName = fullName;
this.shortName = shortName;
}
public setFullName(value: string) {
this.fullName = value.trim();
}
public setShortName(value: string) {
this.shortName = value.trim();
}
public isValid(): boolean {
return !!this.fullName;
}
}

View File

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

View File

@ -54,12 +54,9 @@ export const PROJETS_ACTIONS = {
};
export const USER_ACTIONS = {
UPDATE: 'updateAccount',
CHANGE_PASSWORD: 'changePassword',
DELETE: 'deleteAccount',
UPDATE: 'updateUser',
GET: 'getUser',
CLEAR: 'clearUser',
ACTIVATE: 'activateAccount',
};
export const API_KEYS_ACTIONS = {

View File

@ -44,10 +44,13 @@
@Component({
mounted: async function() {
setTimeout(async () => {
let response: RequestResponse<User> = await this.$store.dispatch(USER_ACTIONS.GET);
if (!response.isSuccess) {
let user: User;
try {
user = await this.$store.dispatch(USER_ACTIONS.GET);
} catch (error) {
this.$store.dispatch(APP_STATE_ACTIONS.CHANGE_STATE, AppState.ERROR);
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, response.errorMessage);
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, error.message);
this.$router.push(ROUTES.LOGIN);
AuthToken.remove();

View File

@ -7,11 +7,11 @@
import { Component, Vue } from 'vue-property-decorator';
import HeaderlessInput from '@/components/common/HeaderlessInput.vue';
import { LOADING_CLASSES } from '@/utils/constants/classConstants';
import { forgotPasswordRequest } from '@/api/users';
import { NOTIFICATION_ACTIONS } from '@/utils/constants/actionNames';
import ROUTES from '@/utils/constants/routerConstants';
import { validateEmail } from '@/utils/validation';
import EVENTS from '@/utils/constants/analyticsEventNames';
import { AuthApi } from '@/api/auth';
@Component({
components: {
@ -23,6 +23,8 @@
private email: string = '';
private emailError: string = '';
private readonly auth: AuthApi = new AuthApi();
public setEmail(value: string): void {
this.email = value;
this.emailError = '';
@ -35,14 +37,12 @@
return;
}
let passwordRecoveryResponse = await forgotPasswordRequest(this.email);
if (!passwordRecoveryResponse.isSuccess) {
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, passwordRecoveryResponse.errorMessage);
return;
try {
await this.auth.forgotPassword(this.email);
this.$store.dispatch(NOTIFICATION_ACTIONS.SUCCESS, 'Please look for instructions at your email');
} catch (error) {
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, error.message);
}
this.$store.dispatch(NOTIFICATION_ACTIONS.SUCCESS, 'Please look for instructions at your email');
}
public onBackToLoginClick(): void {

View File

@ -10,7 +10,7 @@
import { AuthToken } from '@/utils/authToken';
import ROUTES from '@/utils/constants/routerConstants';
import { APP_STATE_ACTIONS, NOTIFICATION_ACTIONS } from '@/utils/constants/actionNames';
import { getTokenRequest } from '@/api/users';
import { AuthApi } from '@/api/auth';
import { LOADING_CLASSES } from '@/utils/constants/classConstants';
import { AppState } from '@/utils/constants/appStateEnum';
import { validateEmail, validatePassword } from '@/utils/validation';
@ -23,14 +23,18 @@
}
})
export default class Login extends Vue {
public forgotPasswordRouterPath: string = ROUTES.FORGOT_PASSWORD.path;
private email: string = '';
private password: string = '';
private authToken: string = '';
public forgotPasswordRouterPath: string = ROUTES.FORGOT_PASSWORD.path;
private loadingClassName: string = LOADING_CLASSES.LOADING_OVERLAY;
private loadingLogoClassName: string = LOADING_CLASSES.LOADING_LOGO;
private emailError: string = '';
private passwordError: string = '';
private readonly auth: AuthApi = new AuthApi();
public onLogoClick(): void {
location.reload();
}
@ -57,9 +61,10 @@
return;
}
let loginResponse = await getTokenRequest(this.email, this.password);
if (!loginResponse.isSuccess) {
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, loginResponse.errorMessage);
try {
this.authToken = await this.auth.token(this.email, this.password);
} catch (error) {
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, error.message);
return;
}
@ -67,7 +72,7 @@
this.activateLoadingOverlay();
setTimeout(() => {
AuthToken.set(loginResponse.data);
AuthToken.set(this.authToken);
this.$store.dispatch(APP_STATE_ACTIONS.CHANGE_STATE, AppState.LOADING);
this.$router.push(ROUTES.PROJECT_OVERVIEW.path + '/' + ROUTES.PROJECT_DETAILS.path);
}, 2000);

View File

@ -12,7 +12,7 @@
import EVENTS from '../../utils/constants/analyticsEventNames';
import { LOADING_CLASSES } from '../../utils/constants/classConstants';
import { APP_STATE_ACTIONS, NOTIFICATION_ACTIONS } from '../../utils/constants/actionNames';
import { createUserRequest } from '../../api/users';
import { AuthApi } from '../../api/auth';
import { setUserId } from '@/utils/consoleLocalStorage';
import { User } from '../../types/users';
import InfoComponent from '../../components/common/InfoComponent.vue';
@ -25,22 +25,28 @@
},
})
export default class Register extends Vue {
private fullName: string = '';
private fullNameError: string = '';
private shortName: string = '';
private email: string = '';
private emailError: string = '';
private password: string = '';
private passwordError: string = '';
private repeatedPassword: string = '';
private repeatedPasswordError: string = '';
private isTermsAccepted: boolean = false;
private isTermsAcceptedError: boolean = false;
private readonly user = new User();
// tardigrade logic
private secret: string = '';
private partnerId: string = '';
private refUserId: string = '';
private userId: string = '';
private isTermsAccepted: boolean = false;
private password: string = '';
private repeatedPassword: string = '';
private fullNameError: string = '';
private emailError: string = '';
private passwordError: string = '';
private repeatedPasswordError: string = '';
private isTermsAcceptedError: boolean = false;
private loadingClassName: string = LOADING_CLASSES.LOADING_OVERLAY;
private readonly auth: AuthApi = new AuthApi();
mounted(): void {
if (this.$route.query.token) {
this.secret = this.$route.query.token.toString();
@ -51,15 +57,15 @@
try {
decoded = atob(ids);
} catch {
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, "Invalid Referral URL.");
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, 'Invalid Referral URL');
this.loadingClassName = LOADING_CLASSES.LOADING_OVERLAY;
return;
}
let referralIds = ids ? JSON.parse(decoded) : undefined;
if (referralIds) {
this.$data.partnerId = referralIds.partnerId;
this.$data.refUserId = referralIds.userId;
this.user.partnerId = referralIds.partnerId;
this.refUserId = referralIds.userId;
}
}
@ -83,15 +89,15 @@
this.$router.push(ROUTES.LOGIN.path);
}
public setEmail(value: string): void {
this.email = value;
this.user.email = value.trim();
this.emailError = '';
}
public setFullName(value: string): void {
this.fullName = value;
this.user.fullName = value.trim();
this.fullNameError = '';
}
public setShortName(value: string): void {
this.shortName = value;
this.user.shortName = value.trim();
}
public setPassword(value: string): void {
this.password = value;
@ -105,12 +111,12 @@
private validateFields(): boolean {
let isNoErrors = true;
if (!this.fullName.trim()) {
if (!this.user.fullName.trim()) {
this.fullNameError = 'Invalid Name';
isNoErrors = false;
}
if (!validateEmail(this.email.trim())) {
if (!validateEmail(this.user.email.trim())) {
this.emailError = 'Invalid Email';
isNoErrors = false;
}
@ -132,22 +138,21 @@
return isNoErrors;
}
private async createUser(): Promise<void> {
let user = new User(this.fullName.trim(), this.shortName.trim(), this.email.trim(), this.partnerId);
let response = await createUserRequest(user, this.password, this.secret, this.refUserId);
if (!response.isSuccess) {
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, response.errorMessage);
this.loadingClassName = LOADING_CLASSES.LOADING_OVERLAY;
return;
}
if (response.data) {
setUserId(response.data);
}
// TODO: improve it
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();
private async createUser(): Promise<void> {
try {
this.userId = await this.auth.create(this.user, this.password , this.secret, this.refUserId);
setUserId(this.userId);
// TODO: improve it
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();
}
} catch (error) {
this.$store.dispatch(NOTIFICATION_ACTIONS.ERROR, error.message);
this.loadingClassName = LOADING_CLASSES.LOADING_OVERLAY;
}
}
}

View File

@ -1,84 +1,54 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
import { usersModule } from '@/store/modules/users';
import * as api from '@/api/users';
import { changePasswordRequest, deleteAccountRequest, getUserRequest, updateAccountRequest } from '@/api/users';
import { USER_MUTATIONS } from '@/store/mutationConstants';
import Vuex from 'vuex';
import { createLocalVue } from '@vue/test-utils';
import { RequestResponse } from '@/types/response';
import { User } from '@/types/users';
import { UsersApiGql } from '@/api/users';
import { makeUsersModule } from '@/store/modules/users';
import { USER_MUTATIONS } from '@/store/mutationConstants';
import { UpdatedUser, User } from '@/types/users';
import { USER_ACTIONS } from '@/utils/constants/actionNames';
const mutations = usersModule.mutations;
const Vue = createLocalVue();
const usersApi = new UsersApiGql();
const usersModule = makeUsersModule(usersApi);
const { UPDATE, GET, CLEAR } = USER_ACTIONS;
Vue.use(Vuex);
const store = new Vuex.Store(usersModule);
describe('mutations', () => {
beforeEach(() => {
createLocalVue().use(Vuex);
});
it('Set user info', () => {
const state = {
user: {
fullName: '',
shortName: '',
email: '',
}
};
it('Set user', () => {
const user = new User('1', 'fullName', 'shortName', 'example@email.com');
const store = new Vuex.Store({state, mutations});
store.commit(USER_MUTATIONS.SET_USER, user);
const user = {
fullName: 'fullName',
shortName: 'shortName',
email: 'email',
};
store.commit(USER_MUTATIONS.SET_USER_INFO, user);
expect(state.user.email).toBe('email');
expect(state.user.fullName).toBe('fullName');
expect(state.user.shortName).toBe('shortName');
expect(store.state.id).toBe(user.id);
expect(store.state.email).toBe(user.email);
expect(store.state.fullName).toBe(user.fullName);
expect(store.state.shortName).toBe(user.shortName);
});
it('clear user info', () => {
const state = {
user: {
fullName: 'fullName',
shortName: 'shortName',
email: 'email',
}
};
it('clear user', () => {
store.commit(USER_MUTATIONS.CLEAR);
const store = new Vuex.Store({state, mutations});
store.commit(USER_MUTATIONS.REVERT_TO_DEFAULT_USER_INFO);
expect(state.user.email).toBe('');
expect(state.user.fullName).toBe('');
expect(state.user.shortName).toBe('');
expect(store.state.id).toBe('');
expect(store.state.email).toBe('');
expect(store.state.fullName).toBe('');
expect(store.state.shortName).toBe('');
});
it('Update user info', () => {
const state = {
user: {
fullName: '',
shortName: '',
email: '',
}
};
const user = {
fullName: 'fullName',
shortName: 'shortName',
email: 'email',
};
it('Update user', () => {
const user = new UpdatedUser('fullName', 'shortName');
const store = new Vuex.Store({state, mutations});
store.commit(USER_MUTATIONS.UPDATE_USER, user);
store.commit(USER_MUTATIONS.UPDATE_USER_INFO, user);
expect(state.user.email).toBe('email');
expect(state.user.fullName).toBe('fullName');
expect(state.user.shortName).toBe('shortName');
expect(store.state.fullName).toBe(user.fullName);
expect(store.state.shortName).toBe(user.shortName);
});
});
@ -87,139 +57,74 @@ describe('actions', () => {
jest.resetAllMocks();
});
it('success update account', async () => {
jest.spyOn(api, 'updateAccountRequest').mockReturnValue(
Promise.resolve(<RequestResponse<User>>{
isSuccess: true, data: {
fullName: 'fullName',
shortName: 'shortName',
email: 'email',
}
})
jest.spyOn(usersApi, 'update').mockReturnValue(
Promise.resolve()
);
const commit = jest.fn();
const user = {
fullName: '',
shortName: '',
email: '',
};
const dispatchResponse = await usersModule.actions.updateAccount({commit}, user);
const user = new UpdatedUser('fullName1', 'shortName2');
expect(dispatchResponse.isSuccess).toBeTruthy();
expect(commit).toHaveBeenCalledWith(USER_MUTATIONS.UPDATE_USER_INFO, {
fullName: 'fullName',
shortName: 'shortName',
email: 'email',
});
await store.dispatch(UPDATE, user);
expect(store.state.fullName).toBe('fullName1');
expect(store.state.shortName).toBe('shortName2');
});
it('error update account', async () => {
jest.spyOn(api, 'updateAccountRequest').mockReturnValue(
Promise.resolve(<RequestResponse<User>>{
isSuccess: false
})
);
const commit = jest.fn();
const user = {
fullName: '',
shortName: '',
email: '',
};
it('update throws an error when api call fails', async () => {
jest.spyOn(usersApi, 'update').mockImplementation(() => { throw new Error(); });
const newUser = new UpdatedUser('', '');
const oldUser = store.getters.user;
const dispatchResponse = await usersModule.actions.updateAccount({commit}, user);
expect(dispatchResponse.isSuccess).toBeFalsy();
expect(commit).toHaveBeenCalledTimes(0);
});
it('password change', async () => {
jest.spyOn(api, 'changePasswordRequest').mockReturnValue(
Promise.resolve(<RequestResponse<null>>{
isSuccess: true
})
);
const commit = jest.fn();
const updatePasswordModel = {oldPassword: 'o', newPassword: 'n'};
const requestResponse = await usersModule.actions.changePassword({commit}, updatePasswordModel);
expect(requestResponse.isSuccess).toBeTruthy();
});
it('delete account', async () => {
jest.spyOn(api, 'deleteAccountRequest').mockReturnValue(
Promise.resolve(<RequestResponse<null>>{
isSuccess: true
})
);
const commit = jest.fn();
const password = '';
const dispatchResponse = await usersModule.actions.deleteAccount(commit, password);
expect(dispatchResponse.isSuccess).toBeTruthy();
try {
await store.dispatch(UPDATE, newUser);
expect(true).toBe(false);
} catch (error) {
expect(store.state.fullName).toBe(oldUser.fullName);
expect(store.state.shortName).toBe(oldUser.shortName);
}
});
it('success get user', async () => {
jest.spyOn(api, 'getUserRequest').mockReturnValue(
Promise.resolve(<RequestResponse<User>>{
isSuccess: true,
data: {
fullName: '',
shortName: '',
email: '',
}
})
const user = new User('2', 'newFullName', 'newShortName', 'example2@email.com');
jest.spyOn(usersApi, 'get').mockReturnValue(
Promise.resolve(user)
);
const commit = jest.fn();
const requestResponse = await usersModule.actions.getUser({commit});
await store.dispatch(GET);
expect(requestResponse.isSuccess).toBeTruthy();
expect(store.state.id).toBe(user.id);
expect(store.state.shortName).toBe(user.shortName);
expect(store.state.fullName).toBe(user.fullName);
expect(store.state.email).toBe(user.email);
});
it('error get user', async () => {
jest.spyOn(api, 'getUserRequest').mockReturnValue(
Promise.resolve(<RequestResponse<User>>{
isSuccess: false
})
);
const commit = jest.fn();
it('get throws an error when api call fails', async () => {
const user = store.getters.user;
jest.spyOn(usersApi, 'get').mockImplementation(() => { throw new Error(); });
const requestResponse = await usersModule.actions.getUser({commit});
expect(requestResponse.isSuccess).toBeFalsy();
try {
await store.dispatch(GET);
expect(true).toBe(false);
} catch (error) {
expect(store.state.fullName).toBe(user.fullName);
expect(store.state.shortName).toBe(user.shortName);
}
});
});
describe('getters', () => {
it('user model', function () {
const state = {
user: {
fullName: 'fullName',
shortName: 'shortName',
email: 'email',
}
};
const retrievedUser = store.getters.user;
const retrievedUser = usersModule.getters.user(state);
expect(retrievedUser.fullName).toBe('fullName');
expect(retrievedUser.shortName).toBe('shortName');
expect(retrievedUser.email).toBe('email');
expect(retrievedUser.id).toBe(store.state.id);
expect(retrievedUser.fullName).toBe(store.state.fullName);
expect(retrievedUser.shortName).toBe(store.state.shortName);
expect(retrievedUser.email).toBe(store.state.email);
});
it('user name', function () {
const state = {
user: {
fullName: 'John',
shortName: 'Doe'
}
};
const retrievedUserName = store.getters.userName;
const retrievedUserName = usersModule.getters.userName(state);
expect(retrievedUserName).toBe('John Doe');
expect(retrievedUserName).toBe(store.state.getFullName());
});
});