(core) Move user profile to new page and begin MFA work

Summary:
The user profile dialog is now a separate page, in preparation
for upcoming work to enable MFA. This commit also contains
some MFA changes, but the UI is currently disabled and the
implementation is limited to software tokens (TOTP) only.

Test Plan:
Updated browser tests for new profile page. Tests for MFAConfig
and CognitoClient will be added in a later diff, once the UI is enabled.

Reviewers: paulfitz

Reviewed By: paulfitz

Subscribers: dsagal

Differential Revision: https://phab.getgrist.com/D3199
This commit is contained in:
George Gevoian
2022-01-07 10:11:52 -08:00
parent 8f531ef622
commit ba6ecc5e9e
21 changed files with 1179 additions and 211 deletions

View File

@@ -266,6 +266,22 @@ export interface DocStateComparisonDetails {
rightChanges: ActionSummary;
}
/**
* User multi-factor authentication preferences, as fetched from Cognito.
*/
export interface UserMFAPreferences {
isSmsMfaEnabled: boolean;
isSoftwareTokenMfaEnabled: boolean;
}
/**
* Cognito response to initiating software token MFA registration.
*/
export interface SoftwareTokenRegistrationInfo {
session: string;
secretCode: string;
}
export {UserProfile} from 'app/common/LoginSessionAPI';
export interface UserAPI {
@@ -304,6 +320,7 @@ export interface UserAPI {
unpinDoc(docId: string): Promise<void>;
moveDoc(docId: string, workspaceId: number): Promise<void>;
getUserProfile(): Promise<FullUser>;
getUserMfaPreferences(): Promise<UserMFAPreferences>;
updateUserName(name: string): Promise<void>;
getWorker(key: string): Promise<string>;
getWorkerAPI(key: string): Promise<DocWorkerAPI>;
@@ -320,6 +337,8 @@ export interface UserAPI {
onUploadProgress?: (ev: ProgressEvent) => void,
}): Promise<string>;
deleteUser(userId: number, name: string): Promise<void>;
registerSoftwareToken(): Promise<SoftwareTokenRegistrationInfo>;
unregisterSoftwareToken(): Promise<void>;
getBaseUrl(): string; // Get the prefix for all the endpoints this object wraps.
forRemoved(): UserAPI; // Get a version of the API that works on removed resources.
getWidgets(): Promise<ICustomWidget[]>;
@@ -582,6 +601,10 @@ export class UserAPIImpl extends BaseAPI implements UserAPI {
return this.requestJson(`${this._url}/api/profile/user`);
}
public async getUserMfaPreferences(): Promise<UserMFAPreferences> {
return this.requestJson(`${this._url}/api/profile/mfa_preferences`);
}
public async updateUserName(name: string): Promise<void> {
await this.request(`${this._url}/api/profile/user/name`, {
method: 'POST',
@@ -668,6 +691,14 @@ export class UserAPIImpl extends BaseAPI implements UserAPI {
body: JSON.stringify({name})});
}
public async registerSoftwareToken(): Promise<SoftwareTokenRegistrationInfo> {
return this.requestJson(`${this._url}/api/auth/register_totp`, {method: 'POST'});
}
public async unregisterSoftwareToken(): Promise<void> {
await this.request(`${this._url}/api/auth/unregister_totp`, {method: 'POST'});
}
public getBaseUrl(): string { return this._url; }
// Recomputes the URL on every call to pick up changes in the URL when switching orgs.

View File

@@ -20,6 +20,9 @@ export type IHomePage = typeof HomePage.type;
export const WelcomePage = StringUnion('user', 'info', 'teams', 'signup', 'verify', 'select-account');
export type WelcomePage = typeof WelcomePage.type;
export const AccountPage = StringUnion('profile');
export type AccountPage = typeof AccountPage.type;
// Overall UI style. "full" is normal, "light" is a single page focused, panels hidden experience.
export const InterfaceStyle = StringUnion('light', 'full');
export type InterfaceStyle = typeof InterfaceStyle.type;
@@ -62,6 +65,7 @@ export interface IGristUrlState {
fork?: UrlIdParts;
docPage?: IDocPage;
newui?: boolean;
account?: AccountPage;
billing?: BillingPage;
welcome?: WelcomePage;
welcomeTour?: boolean;
@@ -183,6 +187,8 @@ export function encodeUrl(gristConfig: Partial<GristLoadConfig>,
parts.push(`p/${state.homePage}`);
}
if (state.account) { parts.push('account'); }
if (state.billing) {
parts.push(state.billing === 'billing' ? 'billing' : `billing/${state.billing}`);
}
@@ -271,6 +277,7 @@ export function decodeUrl(gristConfig: Partial<GristLoadConfig>, location: Locat
}
if (map.has('m')) { state.mode = OpenDocMode.parse(map.get('m')); }
if (sp.has('newui')) { state.newui = useNewUI(sp.get('newui') ? sp.get('newui') === '1' : undefined); }
if (map.has('account')) { state.account = AccountPage.parse('account') || 'profile'; }
if (map.has('billing')) { state.billing = BillingSubPage.parse(map.get('billing')) || 'billing'; }
if (map.has('welcome')) { state.welcome = WelcomePage.parse(map.get('welcome')) || 'user'; }
if (sp.has('billingPlan')) { state.params!.billingPlan = sp.get('billingPlan')!; }