(core) Add new Grist login page

Summary:
Adds a new Grist login page to the login app, and replaces the
server-side Cognito Google Sign-In flow with Google's own OAuth flow.

Test Plan: Browser and server tests.

Reviewers: jarek

Reviewed By: jarek

Differential Revision: https://phab.getgrist.com/D3332
This commit is contained in:
George Gevoian
2022-04-01 14:31:24 -07:00
parent 8fdfb02646
commit 6305811ca6
24 changed files with 188 additions and 185 deletions

View File

@@ -1,31 +0,0 @@
import {parseSubdomain} from 'app/common/gristUrls';
// This interface is used by the standalone login-connect tool for knowing where to redirect to,
// by Client.ts to construct this info, and by CognitoClient to decide what to do.
export interface LoginState {
// Locally-running Grist uses localPort, while hosted uses subdomain. Login-connect uses this to
// redirect back to the localhost or to the subdomain.
localPort?: number;
subdomain?: string;
baseDomain?: string; // the domain with the (left-most) subdomain removed, e.g. ".getgrist.com".
// undefined on localhost.
// Standalone version sets clientId, used later to find the LoginSession. Hosted and dev
// versions rely on the browser cookies instead, specifically on the session cookie.
clientId?: string;
// Hosted and dev versions set redirectUrl and redirect to it when login or logout completes.
// Standalone version omits redirectUrl, and serves a page which closes the window.
redirectUrl?: string;
}
/// Allowed localhost addresses.
export const localhostRegex = /^localhost(?::(\d+))?$/i;
export function getLoginState(reqHost: string): LoginState|null {
const {org, base} = parseSubdomain(reqHost);
const matchPort = localhostRegex.exec(reqHost);
return org ? {subdomain: org, baseDomain: base} :
matchPort ? {localPort: matchPort[1] ? parseInt(matchPort[1], 10) : 80} : null;
}

View File

@@ -2,7 +2,6 @@ import {BillingPage, BillingSubPage, BillingTask} from 'app/common/BillingAPI';
import {OpenDocMode} from 'app/common/DocListAPI';
import {EngineCode} from 'app/common/DocumentSettings';
import {encodeQueryParams, isAffirmative} from 'app/common/gutil';
import {localhostRegex} from 'app/common/LoginState';
import {LocalPlugin} from 'app/common/plugin';
import {StringUnion} from 'app/common/StringUnion';
import {UIRowId} from 'app/common/UIRowId';
@@ -34,7 +33,7 @@ export type WelcomePage = typeof WelcomePage.type;
export const AccountPage = StringUnion('account');
export type AccountPage = typeof AccountPage.type;
export const LoginPage = StringUnion('signup', 'verified', 'forgot-password');
export const LoginPage = StringUnion('signup', 'login', 'verified', 'forgot-password');
export type LoginPage = typeof LoginPage.type;
// Overall UI style. "full" is normal, "light" is a single page focused, panels hidden experience.
@@ -89,7 +88,7 @@ export interface IGristUrlState {
billingPlan?: string;
billingTask?: BillingTask;
embed?: boolean;
next?: string;
state?: string;
style?: InterfaceStyle;
compare?: string;
linkParameters?: Record<string, string>; // Parameters to pass as 'user.Link' in granular ACLs.
@@ -302,13 +301,16 @@ export function decodeUrl(gristConfig: Partial<GristLoadConfig>, location: Locat
if (map.has('signup')) {
state.login = 'signup';
} else if (map.has('login')) {
state.login = 'login';
} else if (map.has('verified')) {
state.login = 'verified';
} else if (map.has('forgot-password')) {
state.login = 'forgot-password';
}
if (sp.has('next')) { state.params!.next = sp.get('next')!; }
if (sp.has('state')) {
state.params!.state = sp.get('state')!;
}
if (sp.has('style')) {
state.params!.style = InterfaceStyle.parse(sp.get('style'));
@@ -407,6 +409,9 @@ export function parseSubdomain(host: string|undefined): {org?: string, base?: st
return {};
}
// Allowed localhost addresses.
const localhostRegex = /^localhost(?::(\d+))?$/i;
/**
* Like parseSubdomain, but throws an error if neither of these cases apply:
* - host can be parsed into a valid subdomain and a valid base domain.
@@ -566,7 +571,7 @@ export function isOrgInPathOnly(host?: string): boolean {
const gristConfig: GristLoadConfig = (window as any).gristConfig;
return (gristConfig && gristConfig.pathOnly) || false;
} else {
if (host && host.match(/^localhost(:[0-9]+)?$/)) { return true; }
if (host && host.match(localhostRegex)) { return true; }
return (process.env.GRIST_ORG_IN_PATH === 'true');
}
}

View File

@@ -921,3 +921,12 @@ export function getDistinctValues<T>(values: readonly T[], count: number = Infin
}
return distinct;
}
/**
* Asserts that variable `name` has a non-nullish `value`.
*/
export function assertIsDefined<T>(name: string, value: T): asserts value is NonNullable<T> {
if (value === undefined || value === null) {
throw new Error(`Expected '${name}' to be defined, but received ${value}`);
}
}