web/satellite: added MFA feature flag, updated client-side api and Vuex store module
Added feature flag for MFA Added new client-side api call to enable MFA returning secret Updated users Vuex module to include new API call Change-Id: Ia9e10f68c4a7da39b4f7c1073e657c2de98fb0db
This commit is contained in:
parent
ee107fe8cd
commit
8855c0dff7
@ -89,6 +89,7 @@ type Config struct {
|
||||
CSPEnabled bool `help:"indicates if Content Security Policy is enabled" devDefault:"false" releaseDefault:"true"`
|
||||
LinksharingURL string `help:"url link for linksharing requests" default:"https://link.us1.storjshare.io"`
|
||||
PathwayOverviewEnabled bool `help:"indicates if the overview onboarding step should render with pathways" default:"true"`
|
||||
MFAEnabled bool `help:"indicates if MFA is enabled" default:"false"`
|
||||
|
||||
RateLimit web.IPRateLimiterConfig
|
||||
|
||||
@ -360,6 +361,7 @@ func (server *Server) appHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ObjectPrice string
|
||||
RecaptchaEnabled bool
|
||||
RecaptchaSiteKey string
|
||||
MFAEnabled bool
|
||||
}
|
||||
|
||||
data.ExternalAddress = server.config.ExternalAddress
|
||||
@ -384,6 +386,7 @@ func (server *Server) appHandler(w http.ResponseWriter, r *http.Request) {
|
||||
data.ObjectPrice = server.pricing.ObjectPrice
|
||||
data.RecaptchaEnabled = server.config.Recaptcha.Enabled
|
||||
data.RecaptchaSiteKey = server.config.Recaptcha.SiteKey
|
||||
data.MFAEnabled = server.config.MFAEnabled
|
||||
|
||||
if server.templates.index == nil {
|
||||
server.log.Error("index template is not set")
|
||||
|
3
scripts/testdata/satellite-config.yaml.lock
vendored
3
scripts/testdata/satellite-config.yaml.lock
vendored
@ -124,6 +124,9 @@ compensation.withheld-percents: 75,75,75,50,50,50,25,25,25,0,0,0,0,0,0
|
||||
# url link for linksharing requests
|
||||
# console.linksharing-url: https://link.us1.storjshare.io
|
||||
|
||||
# indicates if MFA is enabled
|
||||
# console.mfa-enabled: false
|
||||
|
||||
# enable open registration
|
||||
# console.open-registration-enabled: false
|
||||
|
||||
|
@ -24,6 +24,7 @@
|
||||
<meta name="object-price" content="{{ .ObjectPrice }}">
|
||||
<meta name="recaptcha-enabled" content="{{ .RecaptchaEnabled }}">
|
||||
<meta name="recaptcha-site-key" content="{{ .RecaptchaSiteKey }}">
|
||||
<meta name="mfa-enabled" content="{{ .MFAEnabled }}">
|
||||
<title>{{ .SatelliteName }}</title>
|
||||
<link rel="shortcut icon" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAACDVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////8nbP8obf8pbf8qbv8rb/8sb/8tcP8ucf8vcf8vcv8xc/8zdP81df81dv82dv83d/84eP85eP86ef87ev89e/8+fP8/fP9Aff9Bfv9Cfv9Df/9EgP9FgP9Ggf9Hgv9Jg/9LhP9Mhf9Nhv9Oh/9Ph/9RiP9Rif9Sif9Ui/9Vi/9WjP9Xjf9Yjf9aj/9dkf9ekf9ilP9jlf9llv9nl/9omP9rmv9sm/9tnP9vnf9wnv9yn/91of92ov93o/94o/98pv9/qP+Bqf+Cqv+Eq/+FrP+Hrf+Irv+Jr/+Kr/+Msf+Nsv+Stf+Ttf+Ttv+Utv+Vt/+WuP+XuP+Zuf+Zuv+hv/+kwf+lwv+mwv+nw/+oxP+pxP+pxf+qxf+rxv+yy/+0zP+1zf+3zv+4z/+60P+70f+90v+/0/+/1P/B1f/D1v/E1//F1//F2P/G2P/H2f/I2v/J2v/K2//L3P/P3v/Q3//R4P/S4P/V4v/V4//W4//X5P/Y5P/b5v/b5//c5//d6P/e6f/f6f/g6v/h6//j7P/k7f/l7f/m7v/q8f/r8f/u8//w9f/x9f/x9v/z9//0+P/2+f/3+f/3+v/4+v/5+//6/P/8/f/9/v/+/v////9uCbVDAAAAFXRSTlMABAU4Ozw9PpSWl5ilp6ip4+Tl/P6nIcp/AAAAAWJLR0SuuWuTpwAAAh5JREFUOMtjYGBgYOcXEl6HAYSF+FgZQICJex1OwMkEVIAi3+Xh1ozM5wKaj8xfpBwcITsbWYSNgR+JtzpJYvU6jbAVSEK8DEIITpOZqnxItISWfgVCTJAB7v4ZXpKRC9uMNCqXJci6TID7hQFMrV2zJE7abTKQFesDJGb7SYTOX7sGLAVWUKCgrGZcDeaDFaxb12alqC6XDlMwTyKnRLJ1HbKCddNEc0skJkAVdEssXatRiKqgVmLlatUqqILVpuaOEnLJy4GsIhONuHlAOldVwtJWcwnMDb2i4dPKdHVKV3uqRCdYqU9psVDOmh0vUQN35FTRhevWLU+V0FeZBdTtpSQRvgAoKtuMqmBdpKxvKYjXJ+o+cx0WBRPFO6ABHuesMheLghIdePiutc7AoqBLchZchVMSFgUr9HTS8sEgL1C0E1XBRNGUeeV6OlFONjbqSjY2Nv7mKjnzMyXqYQrW2OsYS8smLkOE5OpsFSkdQ6PlUAU9EgtXq6MFdZ3EkpVKNVAFc8TKW6QbURVMFK1slOyGuSFdUkLOoQtZwSRXaRmpKLgj1y1eMjdIImguTMHCCAnvGcuXQhIMPMl1O8hnrOy31GtfnaNi3oRIcohEu7ZY20DZK0DGTCV7NVKi5UVK40vDJVatU/dfgCTEw8AsgsSdLx+TKjUdOeMAsycnMr/BzrIcmc8ByrycuDMvByM4f7PyCmLL/gK8LEBJALYsGEdXEyupAAAAAElFTkSuQmCC" type="image/x-icon">
|
||||
<link rel="dns-prefetch" href="https://js.stripe.com">
|
||||
|
@ -5,7 +5,7 @@ import { ErrorBadRequest } from '@/api/errors/ErrorBadRequest';
|
||||
import { ErrorEmailUsed } from '@/api/errors/ErrorEmailUsed';
|
||||
import { ErrorTooManyRequests } from '@/api/errors/ErrorTooManyRequests';
|
||||
import { ErrorUnauthorized } from '@/api/errors/ErrorUnauthorized';
|
||||
import { UpdatedUser, User } from '@/types/users';
|
||||
import { EnableUserMFARequest, UpdatedUser, User } from '@/types/users';
|
||||
import { HttpClient } from '@/utils/httpClient';
|
||||
|
||||
/**
|
||||
@ -242,4 +242,64 @@ export class AuthHttpApi {
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to enable user's MFA.
|
||||
*
|
||||
* @throws Error
|
||||
*/
|
||||
public async enableUserMFA(body: EnableUserMFARequest): Promise<void> {
|
||||
const path = `${this.ROOT_PATH}/mfa/enable`;
|
||||
const response = await this.http.post(path, JSON.stringify(body));
|
||||
|
||||
if (response.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.status === 401) {
|
||||
throw new ErrorUnauthorized();
|
||||
}
|
||||
|
||||
throw new Error('Can not enable MFA. Please try again later');
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to disable user's MFA.
|
||||
*
|
||||
* @throws Error
|
||||
*/
|
||||
public async disableUserMFA(): Promise<void> {
|
||||
const path = `${this.ROOT_PATH}/mfa/disable`;
|
||||
const response = await this.http.post(path, null);
|
||||
|
||||
if (response.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.status === 401) {
|
||||
throw new ErrorUnauthorized();
|
||||
}
|
||||
|
||||
throw new Error('Can not enable MFA. Please try again later');
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to generate user's MFA recovery codes.
|
||||
*
|
||||
* @throws Error
|
||||
*/
|
||||
public async generateUserMFARecoveryCodes(): Promise<string[]> {
|
||||
const path = `${this.ROOT_PATH}/mfa/generate-recovery-codes`;
|
||||
const response = await this.http.post(path, null);
|
||||
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
if (response.status === 401) {
|
||||
throw new ErrorUnauthorized();
|
||||
}
|
||||
|
||||
throw new Error('Can not enable MFA. Please try again later');
|
||||
}
|
||||
}
|
||||
|
@ -2,29 +2,42 @@
|
||||
// See LICENSE for copying information.
|
||||
|
||||
import { StoreModule } from '@/store';
|
||||
import { UpdatedUser, User, UsersApi } from '@/types/users';
|
||||
import { EnableUserMFARequest, UpdatedUser, User, UsersApi } from '@/types/users';
|
||||
import { MetaUtils } from '@/utils/meta';
|
||||
|
||||
export const USER_ACTIONS = {
|
||||
UPDATE: 'updateUser',
|
||||
GET: 'getUser',
|
||||
ENABLE_USER_MFA: 'enableUserMFA',
|
||||
DISABLE_USER_MFA: 'disableUserMFA',
|
||||
GENERATE_USER_MFA_RECOVERY_CODES: 'generateUserMFARecoveryCodes',
|
||||
CLEAR: 'clearUser',
|
||||
};
|
||||
|
||||
export const USER_MUTATIONS = {
|
||||
SET_USER: 'setUser',
|
||||
SET_USER_MFA_RECOVERY_CODES: 'setUserMFARecoveryCodes',
|
||||
UPDATE_USER: 'updateUser',
|
||||
CLEAR: 'clearUser',
|
||||
};
|
||||
|
||||
export class UsersState {
|
||||
public user: User = new User();
|
||||
public userMFARecoveryCodes: string[] = [];
|
||||
}
|
||||
|
||||
const {
|
||||
GET,
|
||||
UPDATE,
|
||||
ENABLE_USER_MFA,
|
||||
DISABLE_USER_MFA,
|
||||
GENERATE_USER_MFA_RECOVERY_CODES,
|
||||
} = USER_ACTIONS;
|
||||
|
||||
const {
|
||||
SET_USER,
|
||||
UPDATE_USER,
|
||||
SET_USER_MFA_RECOVERY_CODES,
|
||||
CLEAR,
|
||||
} = USER_MUTATIONS;
|
||||
|
||||
@ -33,42 +46,34 @@ const {
|
||||
*
|
||||
* @param api - users api
|
||||
*/
|
||||
export function makeUsersModule(api: UsersApi): StoreModule<User> {
|
||||
export function makeUsersModule(api: UsersApi): StoreModule<UsersState> {
|
||||
return {
|
||||
state: new User(),
|
||||
state: new UsersState(),
|
||||
|
||||
mutations: {
|
||||
[SET_USER](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;
|
||||
[SET_USER](state: UsersState, user: User): void {
|
||||
state.user = user;
|
||||
|
||||
if (user.projectLimit === 0) {
|
||||
const limitFromConfig = MetaUtils.getMetaContent('default-project-limit');
|
||||
|
||||
state.projectLimit = parseInt(limitFromConfig);
|
||||
state.user.projectLimit = parseInt(limitFromConfig);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
state.projectLimit = user.projectLimit;
|
||||
state.paidTier = user.paidTier;
|
||||
state.user.projectLimit = user.projectLimit;
|
||||
},
|
||||
|
||||
[CLEAR](state: User): void {
|
||||
state.id = '';
|
||||
state.email = '';
|
||||
state.shortName = '';
|
||||
state.fullName = '';
|
||||
state.partnerId = '';
|
||||
state.projectLimit = 1;
|
||||
[CLEAR](state: UsersState): void {
|
||||
state.user = new User();
|
||||
state.user.projectLimit = 1;
|
||||
},
|
||||
|
||||
[UPDATE_USER](state: User, user: UpdatedUser): void {
|
||||
state.fullName = user.fullName;
|
||||
state.shortName = user.shortName;
|
||||
[UPDATE_USER](state: UsersState, user: UpdatedUser): void {
|
||||
state.user.fullName = user.fullName;
|
||||
state.user.shortName = user.shortName;
|
||||
},
|
||||
[SET_USER_MFA_RECOVERY_CODES](state: UsersState, codes: string[]): void {
|
||||
state.userMFARecoveryCodes = codes;
|
||||
},
|
||||
},
|
||||
|
||||
@ -85,14 +90,25 @@ export function makeUsersModule(api: UsersApi): StoreModule<User> {
|
||||
|
||||
return user;
|
||||
},
|
||||
[ENABLE_USER_MFA]: async function (_): Promise<void> {
|
||||
await api.disableUserMFA();
|
||||
},
|
||||
[DISABLE_USER_MFA]: async function (_, request: EnableUserMFARequest): Promise<void> {
|
||||
await api.enableUserMFA(request);
|
||||
},
|
||||
[GENERATE_USER_MFA_RECOVERY_CODES]: async function ({commit}: any): Promise<void> {
|
||||
const codes = await api.generateUserMFARecoveryCodes();
|
||||
|
||||
commit(SET_USER_MFA_RECOVERY_CODES, codes);
|
||||
},
|
||||
[CLEAR]: function({commit}: any) {
|
||||
commit(CLEAR);
|
||||
},
|
||||
},
|
||||
|
||||
getters: {
|
||||
user: (state: User): User => state,
|
||||
userName: (state: User): string => state.getFullName(),
|
||||
user: (state: UsersState): User => state.user,
|
||||
userName: (state: UsersState): string => state.user.getFullName(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -19,6 +19,24 @@ export interface UsersApi {
|
||||
* @throws Error
|
||||
*/
|
||||
get(): Promise<User>;
|
||||
/**
|
||||
* Enable user's MFA.
|
||||
*
|
||||
* @throws Error
|
||||
*/
|
||||
enableUserMFA(request: EnableUserMFARequest): Promise<void>;
|
||||
/**
|
||||
* Disable user's MFA.
|
||||
*
|
||||
* @throws Error
|
||||
*/
|
||||
disableUserMFA(): Promise<void>;
|
||||
/**
|
||||
* Generate user's MFA recovery codes.
|
||||
*
|
||||
* @throws Error
|
||||
*/
|
||||
generateUserMFARecoveryCodes(): Promise<string[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -68,3 +86,11 @@ export class UpdatedUser {
|
||||
return !!this.fullName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents request to enable user's MFA.
|
||||
*/
|
||||
export interface EnableUserMFARequest {
|
||||
secret: string;
|
||||
passcode: string;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
import { UpdatedUser, User, UsersApi } from '@/types/users';
|
||||
import { EnableUserMFARequest, UpdatedUser, User, UsersApi } from '@/types/users';
|
||||
|
||||
/**
|
||||
* Mock for UsersApi
|
||||
@ -20,4 +20,16 @@ export class UsersApiMock implements UsersApi {
|
||||
public update(user: UpdatedUser): Promise<void> {
|
||||
throw new Error('not implemented');
|
||||
}
|
||||
|
||||
public enableUserMFA(_: EnableUserMFARequest): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
public disableUserMFA(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
public generateUserMFARecoveryCodes(): Promise<string[]> {
|
||||
return Promise.resolve(['test', 'test1', 'test2']);
|
||||
}
|
||||
}
|
||||
|
@ -26,19 +26,19 @@ describe('mutations', () => {
|
||||
|
||||
store.commit(USER_MUTATIONS.SET_USER, user);
|
||||
|
||||
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);
|
||||
expect(store.state.user.id).toBe(user.id);
|
||||
expect(store.state.user.email).toBe(user.email);
|
||||
expect(store.state.user.fullName).toBe(user.fullName);
|
||||
expect(store.state.user.shortName).toBe(user.shortName);
|
||||
});
|
||||
|
||||
it('clear user', () => {
|
||||
store.commit(USER_MUTATIONS.CLEAR);
|
||||
|
||||
expect(store.state.id).toBe('');
|
||||
expect(store.state.email).toBe('');
|
||||
expect(store.state.fullName).toBe('');
|
||||
expect(store.state.shortName).toBe('');
|
||||
expect(store.state.user.id).toBe('');
|
||||
expect(store.state.user.email).toBe('');
|
||||
expect(store.state.user.fullName).toBe('');
|
||||
expect(store.state.user.shortName).toBe('');
|
||||
});
|
||||
|
||||
it('Update user', () => {
|
||||
@ -46,8 +46,8 @@ describe('mutations', () => {
|
||||
|
||||
store.commit(USER_MUTATIONS.UPDATE_USER, user);
|
||||
|
||||
expect(store.state.fullName).toBe(user.fullName);
|
||||
expect(store.state.shortName).toBe(user.shortName);
|
||||
expect(store.state.user.fullName).toBe(user.fullName);
|
||||
expect(store.state.user.shortName).toBe(user.shortName);
|
||||
});
|
||||
});
|
||||
|
||||
@ -64,8 +64,8 @@ describe('actions', () => {
|
||||
|
||||
await store.dispatch(UPDATE, user);
|
||||
|
||||
expect(store.state.fullName).toBe('fullName1');
|
||||
expect(store.state.shortName).toBe('shortName2');
|
||||
expect(store.state.user.fullName).toBe('fullName1');
|
||||
expect(store.state.user.shortName).toBe('shortName2');
|
||||
});
|
||||
|
||||
it('update throws an error when api call fails', async () => {
|
||||
@ -77,19 +77,19 @@ describe('actions', () => {
|
||||
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);
|
||||
expect(store.state.user.fullName).toBe(oldUser.fullName);
|
||||
expect(store.state.user.shortName).toBe(oldUser.shortName);
|
||||
}
|
||||
});
|
||||
|
||||
it('clears state', async () => {
|
||||
await store.dispatch(CLEAR);
|
||||
|
||||
expect(store.state.fullName).toBe('');
|
||||
expect(store.state.shortName).toBe('');
|
||||
expect(store.state.email).toBe('');
|
||||
expect(store.state.partnerId).toBe('');
|
||||
expect(store.state.id).toBe('');
|
||||
expect(store.state.user.fullName).toBe('');
|
||||
expect(store.state.user.shortName).toBe('');
|
||||
expect(store.state.user.email).toBe('');
|
||||
expect(store.state.user.partnerId).toBe('');
|
||||
expect(store.state.user.id).toBe('');
|
||||
});
|
||||
|
||||
it('success get user', async () => {
|
||||
@ -101,10 +101,10 @@ describe('actions', () => {
|
||||
|
||||
await store.dispatch(GET);
|
||||
|
||||
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);
|
||||
expect(store.state.user.id).toBe(user.id);
|
||||
expect(store.state.user.shortName).toBe(user.shortName);
|
||||
expect(store.state.user.fullName).toBe(user.fullName);
|
||||
expect(store.state.user.email).toBe(user.email);
|
||||
});
|
||||
|
||||
it('get throws an error when api call fails', async () => {
|
||||
@ -115,8 +115,8 @@ describe('actions', () => {
|
||||
await store.dispatch(GET);
|
||||
expect(true).toBe(false);
|
||||
} catch (error) {
|
||||
expect(store.state.fullName).toBe(user.fullName);
|
||||
expect(store.state.shortName).toBe(user.shortName);
|
||||
expect(store.state.user.fullName).toBe(user.fullName);
|
||||
expect(store.state.user.shortName).toBe(user.shortName);
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -125,15 +125,15 @@ describe('getters', () => {
|
||||
it('user model', function () {
|
||||
const retrievedUser = store.getters.user;
|
||||
|
||||
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);
|
||||
expect(retrievedUser.id).toBe(store.state.user.id);
|
||||
expect(retrievedUser.fullName).toBe(store.state.user.fullName);
|
||||
expect(retrievedUser.shortName).toBe(store.state.user.shortName);
|
||||
expect(retrievedUser.email).toBe(store.state.user.email);
|
||||
});
|
||||
|
||||
it('user name', function () {
|
||||
const retrievedUserName = store.getters.userName;
|
||||
|
||||
expect(retrievedUserName).toBe(store.state.getFullName());
|
||||
expect(retrievedUserName).toBe(store.state.user.getFullName());
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user