diff --git a/app/client/models/gristUrlState.ts b/app/client/models/gristUrlState.ts index 87d2ba8a..5ef28d75 100644 --- a/app/client/models/gristUrlState.ts +++ b/app/client/models/gristUrlState.ts @@ -65,8 +65,8 @@ export function getMainOrgUrl(): string { return urlState().makeUrl({}); } export function getCurrentDocUrl(): string { return urlState().makeUrl({docPage: undefined}); } // Get url for the login page, which will then redirect to nextUrl (current page by default). -export function getLoginUrl(nextUrl: string = _getCurrentUrl()): string { - return _getLoginLogoutUrl('login', nextUrl); +export function getLoginUrl(nextUrl: string | null = _getCurrentUrl()): string { + return _getLoginLogoutUrl('login', nextUrl ?? undefined); } // Get url for the signup page, which will then redirect to nextUrl (current page by default). @@ -101,10 +101,10 @@ function _getCurrentUrl(): string { } // Helper for getLoginUrl()/getLogoutUrl(). -function _getLoginLogoutUrl(method: 'login'|'logout'|'signin'|'signup', nextUrl: string): string { +function _getLoginLogoutUrl(method: 'login'|'logout'|'signin'|'signup', nextUrl?: string): string { const startUrl = new URL(window.location.href); startUrl.pathname = addOrgToPath('', window.location.href) + '/' + method; - startUrl.searchParams.set('next', nextUrl); + if (nextUrl) { startUrl.searchParams.set('next', nextUrl); } return startUrl.href; } diff --git a/app/client/ui/AccountWidget.ts b/app/client/ui/AccountWidget.ts index 7acae528..8d9b417f 100644 --- a/app/client/ui/AccountWidget.ts +++ b/app/client/ui/AccountWidget.ts @@ -100,7 +100,7 @@ export class AccountWidget extends Disposable { cssEmail(user.email, testId('usermenu-email')) ) ), - menuItemLink(urlState().setLinkUrl({account: 'profile'}), 'Profile Settings'), + menuItemLink(urlState().setLinkUrl({account: 'account'}), 'Profile Settings'), documentSettingsItem, diff --git a/app/client/ui/WelcomePage.ts b/app/client/ui/WelcomePage.ts index 2c4402e1..f4a7da05 100644 --- a/app/client/ui/WelcomePage.ts +++ b/app/client/ui/WelcomePage.ts @@ -196,11 +196,11 @@ export class WelcomePage extends Disposable { 'form', { method: "post", action: action.href }, handleSubmitForm(pending, (result) => { - if (result.act === 'confirmed') { + if (result.status === 'confirmed') { const verified = new URL(window.location.href); verified.pathname = '/verified'; window.location.assign(verified.href); - } else if (result.act === 'resent') { + } else if (result.status === 'resent') { // just to give a sense that something happened... window.location.reload(); } diff --git a/app/client/ui/errorPages.ts b/app/client/ui/errorPages.ts index ea66d1a9..32ea9eeb 100644 --- a/app/client/ui/errorPages.ts +++ b/app/client/ui/errorPages.ts @@ -1,5 +1,5 @@ import {AppModel} from 'app/client/models/AppModel'; -import {getLoginUrl, getMainOrgUrl, urlState} from 'app/client/models/gristUrlState'; +import {getLoginUrl, urlState} from 'app/client/models/gristUrlState'; import {AppHeader} from 'app/client/ui/AppHeader'; import {leftPanelBasic} from 'app/client/ui/LeftPanelCommon'; import {pagePanels} from 'app/client/ui/PagePanels'; @@ -15,7 +15,6 @@ export function createErrPage(appModel: AppModel) { const gristConfig: GristLoadConfig = (window as any).gristConfig || {}; const message = gristConfig.errMessage; return gristConfig.errPage === 'signed-out' ? createSignedOutPage(appModel) : - gristConfig.errPage === 'verified' ? createVerifiedPage(appModel) : gristConfig.errPage === 'not-found' ? createNotFoundPage(appModel, message) : gristConfig.errPage === 'access-denied' ? createForbiddenPage(appModel, message) : createOtherErrorPage(appModel, message); @@ -56,18 +55,6 @@ export function createSignedOutPage(appModel: AppModel) { ]); } -/** - * Creates a page that shows the user is verified. - */ -export function createVerifiedPage(appModel: AppModel) { - return pagePanelsError(appModel, 'Verified', [ - cssErrorText("Your email is now verified."), - cssButtonWrap(bigPrimaryButtonLink( - 'Sign in', {href: getLoginUrl(getMainOrgUrl())}, testId('error-signin') - )) - ]); -} - /** * Creates a "Page not found" page. */ diff --git a/app/client/ui2018/IconList.ts b/app/client/ui2018/IconList.ts index 3375399d..762ecb81 100644 --- a/app/client/ui2018/IconList.ts +++ b/app/client/ui2018/IconList.ts @@ -30,6 +30,10 @@ export type IconName = "ChartArea" | "FieldText" | "FieldTextbox" | "FieldToggle" | + "LoginStreamline" | + "LoginUnify" | + "LoginVisualize" | + "GoogleLogo" | "GristLogo" | "ThumbPreview" | "BarcodeQR" | @@ -143,6 +147,10 @@ export const IconList: IconName[] = ["ChartArea", "FieldText", "FieldTextbox", "FieldToggle", + "LoginStreamline", + "LoginUnify", + "LoginVisualize", + "GoogleLogo", "GristLogo", "ThumbPreview", "BarcodeQR", diff --git a/app/client/ui2018/cssVars.ts b/app/client/ui2018/cssVars.ts index a8f7583c..ec6714c1 100644 --- a/app/client/ui2018/cssVars.ts +++ b/app/client/ui2018/cssVars.ts @@ -164,10 +164,12 @@ export const testId: TestId = makeTestId('test-'); // Min width for normal screen layout (in px). Note: <768px is bootstrap's definition of small // screen (covers phones, including landscape, but not tablets). +const largeScreenWidth = 992; const mediumScreenWidth = 768; const smallScreenWidth = 576; // Anything below this is extra-small (e.g. portrait phones). // Fractional width for max-query follows https://getbootstrap.com/docs/4.0/layout/overview/#responsive-breakpoints +export const mediaMedium = `(max-width: ${largeScreenWidth - 0.02}px)`; export const mediaSmall = `(max-width: ${mediumScreenWidth - 0.02}px)`; export const mediaNotSmall = `(min-width: ${mediumScreenWidth}px)`; export const mediaXSmall = `(max-width: ${smallScreenWidth - 0.02}px)`; diff --git a/app/common/gristUrls.ts b/app/common/gristUrls.ts index aaedacfa..d31bb601 100644 --- a/app/common/gristUrls.ts +++ b/app/common/gristUrls.ts @@ -22,9 +22,12 @@ 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 const AccountPage = StringUnion('account'); export type AccountPage = typeof AccountPage.type; +export const LoginPage = StringUnion('signup', 'verified'); +export type LoginPage = typeof LoginPage.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; @@ -68,6 +71,7 @@ export interface IGristUrlState { docPage?: IDocPage; account?: AccountPage; billing?: BillingPage; + login?: LoginPage; welcome?: WelcomePage; welcomeTour?: boolean; docTour?: boolean; @@ -76,6 +80,7 @@ export interface IGristUrlState { billingPlan?: string; billingTask?: BillingTask; embed?: boolean; + next?: string; style?: InterfaceStyle; compare?: string; linkParameters?: Record; // Parameters to pass as 'user.Link' in granular ACLs. @@ -188,12 +193,16 @@ export function encodeUrl(gristConfig: Partial, parts.push(`p/${state.homePage}`); } - if (state.account) { parts.push('account'); } + if (state.account) { + parts.push(state.account === 'account' ? 'account' : `account/${state.account}`); + } if (state.billing) { parts.push(state.billing === 'billing' ? 'billing' : `billing/${state.billing}`); } + if (state.login) { parts.push(state.login); } + if (state.welcome) { parts.push(`welcome/${state.welcome}`); } @@ -274,13 +283,22 @@ export function decodeUrl(gristConfig: Partial, location: Locat } } if (map.has('m')) { state.mode = OpenDocMode.parse(map.get('m')); } - if (map.has('account')) { state.account = AccountPage.parse('account') || 'profile'; } + if (map.has('account')) { state.account = AccountPage.parse(map.get('account')) || 'account'; } 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')!; } if (sp.has('billingTask')) { state.params!.billingTask = BillingTask.parse(sp.get('billingTask')); } + + if (map.has('signup')) { + state.login = 'signup'; + } else if (map.has('verified')) { + state.login = 'verified'; + } + + if (sp.has('next')) { state.params!.next = sp.get('next')!; } + if (sp.has('style')) { state.params!.style = InterfaceStyle.parse(sp.get('style')); } diff --git a/app/server/lib/FlexServer.ts b/app/server/lib/FlexServer.ts index 844b1d58..93400d6f 100644 --- a/app/server/lib/FlexServer.ts +++ b/app/server/lib/FlexServer.ts @@ -1,3 +1,4 @@ +import {ApiError} from 'app/common/ApiError'; import {BillingTask} from 'app/common/BillingAPI'; import {delay} from 'app/common/delay'; import {DocCreationInfo} from 'app/common/DocListAPI'; @@ -40,7 +41,7 @@ import {IBilling} from 'app/server/lib/IBilling'; import {IDocStorageManager} from 'app/server/lib/IDocStorageManager'; import {INotifier} from 'app/server/lib/INotifier'; import * as log from 'app/server/lib/log'; -import {getLoginSystem} from 'app/server/lib/logins'; +import {getLoginSubdomain, getLoginSystem} from 'app/server/lib/logins'; import {IPermitStore} from 'app/server/lib/Permit'; import {getAppPathTo, getAppRoot, getUnpackedAppRoot} from 'app/server/lib/places'; import {addPluginEndpoints, limitToPlugins} from 'app/server/lib/PluginEndpoint'; @@ -237,6 +238,14 @@ export class FlexServer implements GristServer { return this.server ? (this.server.address() as AddressInfo).port : this.port; } + /** + * Get a url to an org that should be accessible by all signed-in users. For now, this + * returns the base URL of the personal org (typically docs[-s]). + */ + public getMergedOrgUrl(req: RequestWithLogin, pathname: string = '/'): string { + return this._getOrgRedirectUrl(req, this._dbManager.mergedOrgDomain(), pathname); + } + public getPermitStore(): IPermitStore { if (!this._internalPermitStore) { throw new Error('no permit store available'); } return this._internalPermitStore; @@ -805,22 +814,50 @@ export class FlexServer implements GristServer { // should be factored out of it. this.addComm(); + /** + * Gets the URL to redirect back to after successful sign-up or login. + * + * Note that in the test env, this will redirect further. + */ + const getNextUrl = async (mreq: RequestWithLogin): Promise => { + // If a "next" query param is present, check if it's safe to use, and return it if so. + const next = optStringParam(mreq.query.next); + if (next) { + if (!(await this._hosts.isSafeRedirectUrl(next))) { + throw new ApiError('Invalid redirect URL', 400); + } + + return next; + } + + // Otherwise, return the "/" path on the request host. If the host is the Grist + // login host, return the "/" path on the merged org instead. + const loginSubdomain = getLoginSubdomain(); + return !loginSubdomain || mreq.org !== loginSubdomain ? + getOrgUrl(mreq) : this.getMergedOrgUrl(mreq); + }; + async function redirectToLoginOrSignup( this: FlexServer, signUp: boolean|null, req: express.Request, resp: express.Response, ) { const mreq = req as RequestWithLogin; + + // If sign-up request is to the Grist login host, serve a sign-up page instead of redirecting. + const loginSubdomain = getLoginSubdomain(); + if (signUp && loginSubdomain && mreq.org === loginSubdomain) { + return this._sendAppPage(req, resp, {path: 'login.html', status: 200, config: {}}); + } + // This will ensure that express-session will set our cookie if it hasn't already - // we'll need it when we come back from Cognito. forceSessionChange(mreq.session); - // Redirect to "/" on our requested hostname (in test env, this will redirect further) - const next = getOrgUrl(req); if (signUp === null) { // Like redirectToLogin in Authorizer, redirect to sign up if it doesn't look like the // user has ever logged in on this browser. signUp = (mreq.session.users === undefined); } const getRedirectUrl = signUp ? this._getSignUpRedirectUrl : this._getLoginRedirectUrl; - resp.redirect(await getRedirectUrl(req, new URL(next))); + resp.redirect(await getRedirectUrl(req, new URL(await getNextUrl(mreq)))); } this.app.get('/login', expressWrap(redirectToLoginOrSignup.bind(this, false))); @@ -899,10 +936,9 @@ export class FlexServer implements GristServer { this.app.get('/signed-out', expressWrap((req, resp) => this._sendAppPage(req, resp, {path: 'error.html', status: 200, config: {errPage: 'signed-out'}}))); - // Add a static "verified" page. This is where an email verification link will land, - // on success. TODO: rename simple static pages from "error" to something more generic. + // Add a static "verified" page. This is where an email verification link will land, on success. this.app.get('/verified', expressWrap((req, resp) => - this._sendAppPage(req, resp, {path: 'error.html', status: 200, config: {errPage: 'verified'}}))); + this._sendAppPage(req, resp, {path: 'login.html', status: 200, config: {}}))); const comment = await this._loginMiddleware.addEndpoints(this.app); this.info.push(['loginMiddlewareComment', comment]); @@ -1127,8 +1163,7 @@ export class FlexServer implements GristServer { redirectPath = '/welcome/teams'; } - const mergedOrgDomain = this._dbManager.mergedOrgDomain(); - const redirectUrl = this._getOrgRedirectUrl(mreq, mergedOrgDomain, redirectPath); + const redirectUrl = this.getMergedOrgUrl(mreq, redirectPath); resp.json({redirectUrl}); }), // Add a final error handler that reports errors as JSON. @@ -1466,7 +1501,7 @@ export class FlexServer implements GristServer { // Redirect anonymous users to the merged org. if (!mreq.userIsAuthorized) { - const redirectUrl = this._getOrgRedirectUrl(mreq, this._dbManager.mergedOrgDomain()); + const redirectUrl = this.getMergedOrgUrl(mreq); log.debug(`Redirecting anonymous user to: ${redirectUrl}`); return resp.redirect(redirectUrl); } diff --git a/app/server/lib/GristServer.ts b/app/server/lib/GristServer.ts index 84a4ff45..075a8927 100644 --- a/app/server/lib/GristServer.ts +++ b/app/server/lib/GristServer.ts @@ -4,6 +4,7 @@ import { Document } from 'app/gen-server/entity/Document'; import { Organization } from 'app/gen-server/entity/Organization'; import { Workspace } from 'app/gen-server/entity/Workspace'; import { HomeDBManager } from 'app/gen-server/lib/HomeDBManager'; +import { RequestWithLogin } from 'app/server/lib/Authorizer'; import * as Comm from 'app/server/lib/Comm'; import { Hosts } from 'app/server/lib/extractOrg'; import { ICreate } from 'app/server/lib/ICreate'; @@ -23,6 +24,7 @@ export interface GristServer { getHomeUrlByDocId(docId: string, relPath?: string): Promise; getDocUrl(docId: string): Promise; getOrgUrl(orgKey: string|number): Promise; + getMergedOrgUrl(req: RequestWithLogin, pathname?: string): string; getResourceUrl(resource: Organization|Workspace|Document): Promise; getGristConfig(): GristLoadConfig; getPermitStore(): IPermitStore; diff --git a/app/server/lib/expressWrap.ts b/app/server/lib/expressWrap.ts index 79098e3c..6e634c6e 100644 --- a/app/server/lib/expressWrap.ts +++ b/app/server/lib/expressWrap.ts @@ -29,8 +29,8 @@ const buildJsonErrorHandler = (options: JsonErrorHandlerOptions = {}): express.E return (err, req, res, _next) => { const mreq = req as RequestWithLogin; log.warn( - "Error during api call to %s: (%s) user %d%s%s", - req.path, err.message, mreq.userId, + "Error during api call to %s: (%s)%s%s%s", + req.path, err.message, mreq.userId !== undefined ? ` user ${mreq.userId}` : '', options.shouldLogParams !== false ? ` params ${JSON.stringify(req.params)}` : '', options.shouldLogBody !== false ? ` body ${JSON.stringify(req.body)}` : '', ); diff --git a/app/server/lib/extractOrg.ts b/app/server/lib/extractOrg.ts index 9e53c8af..b03478db 100644 --- a/app/server/lib/extractOrg.ts +++ b/app/server/lib/extractOrg.ts @@ -131,6 +131,27 @@ export class Hosts { return this._redirectHost.bind(this); } + /** + * Returns true if `url` either points to a native Grist host (e.g. foo.getgrist.com), + * or a custom host for an existing Grist org. + */ + public async isSafeRedirectUrl(url: string): Promise { + const {host, protocol} = new URL(url); + if (!['http:', 'https:'].includes(protocol)) { return false; } + + switch (this._getHostType(host)) { + case 'native': { return true; } + case 'custom': { + const org = await mapGetOrSet(this._host2org, host, async () => { + const o = await this._dbManager.connection.manager.findOne(Organization, {host}); + return o?.domain ?? undefined; + }); + return org !== undefined; + } + default: { return false; } + } + } + public close() { this._host2org.clear(); this._org2host.clear(); diff --git a/static/account.html b/static/account.html index bec529b3..65a09718 100644 --- a/static/account.html +++ b/static/account.html @@ -3,12 +3,12 @@ - + Grist - + diff --git a/static/icons/icons.css b/static/icons/icons.css index adfe975e..c1dd6c73 100644 --- a/static/icons/icons.css +++ b/static/icons/icons.css @@ -31,6 +31,10 @@ --icon-FieldText: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTcuNSwxMyBMNy41LDMgTDMsMyBMMyw0LjUgQzMsNC43NzYxNDIzNyAyLjc3NjE0MjM3LDUgMi41LDUgQzIuMjIzODU3NjMsNSAyLDQuNzc2MTQyMzcgMiw0LjUgTDIsMi41IEMyLDIuMjIzODU3NjMgMi4yMjM4NTc2MywyIDIuNSwyIEwxMy41LDIgQzEzLjc3NjE0MjQsMiAxNCwyLjIyMzg1NzYzIDE0LDIuNSBMMTQsNC41IEMxNCw0Ljc3NjE0MjM3IDEzLjc3NjE0MjQsNSAxMy41LDUgQzEzLjIyMzg1NzYsNSAxMyw0Ljc3NjE0MjM3IDEzLDQuNSBMMTMsMyBMOC41LDMgTDguNSwxMyBMMTAuNSwxMyBDMTAuNzc2MTQyNCwxMyAxMSwxMy4yMjM4NTc2IDExLDEzLjUgQzExLDEzLjc3NjE0MjQgMTAuNzc2MTQyNCwxNCAxMC41LDE0IEw1LjUsMTQgQzUuMjIzODU3NjMsMTQgNSwxMy43NzYxNDI0IDUsMTMuNSBDNSwxMy4yMjM4NTc2IDUuMjIzODU3NjMsMTMgNS41LDEzIEw3LjUsMTMgWiIgZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJub256ZXJvIi8+PC9zdmc+'); --icon-FieldTextbox: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTIuNSwxMyBDMi4yMjM4NTc2MywxMyAyLDEyLjc3NjE0MjQgMiwxMi41IEMyLDEyLjIyMzg1NzYgMi4yMjM4NTc2MywxMiAyLjUsMTIgTDEzLjUsMTIgQzEzLjc3NjE0MjQsMTIgMTQsMTIuMjIzODU3NiAxNCwxMi41IEMxNCwxMi43NzYxNDI0IDEzLjc3NjE0MjQsMTMgMTMuNSwxMyBMMi41LDEzIFogTTIuNSw0IEMyLjIyMzg1NzYzLDQgMiwzLjc3NjE0MjM3IDIsMy41IEMyLDMuMjIzODU3NjMgMi4yMjM4NTc2MywzIDIuNSwzIEwxMy41LDMgQzEzLjc3NjE0MjQsMyAxNCwzLjIyMzg1NzYzIDE0LDMuNSBDMTQsMy43NzYxNDIzNyAxMy43NzYxNDI0LDQgMTMuNSw0IEwyLjUsNCBaIE0yLjUsOC41IEMyLjIyMzg1NzYzLDguNSAyLDguMjc2MTQyMzcgMiw4IEMyLDcuNzIzODU3NjMgMi4yMjM4NTc2Myw3LjUgMi41LDcuNSBMMTMuNSw3LjUgQzEzLjc3NjE0MjQsNy41IDE0LDcuNzIzODU3NjMgMTQsOCBDMTQsOC4yNzYxNDIzNyAxMy43NzYxNDI0LDguNSAxMy41LDguNSBMMi41LDguNSBaIiBmaWxsPSIjMDAwIiBmaWxsLXJ1bGU9Im5vbnplcm8iLz48L3N2Zz4='); --icon-FieldToggle: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTguNjQ1ODI0NCwxMSBMMTAsMTEgQzExLjY1Njg1NDIsMTEgMTMsOS42NTY4NTQyNSAxMyw4IEMxMyw2LjM0MzE0NTc1IDExLjY1Njg1NDIsNSAxMCw1IEw4LjY0NTgyNDQsNSBDOS40NzYyNDUxMSw1LjczMjk0NDQ1IDEwLDYuODA1MzA3NDcgMTAsOCBDMTAsOS4xOTQ2OTI1MyA5LjQ3NjI0NTExLDEwLjI2NzA1NTUgOC42NDU4MjQ0LDExIFogTTYsNCBMMTAsNCBDMTIuMjA5MTM5LDQgMTQsNS43OTA4NjEgMTQsOCBDMTQsMTAuMjA5MTM5IDEyLjIwOTEzOSwxMiAxMCwxMiBMNiwxMiBDMy43OTA4NjEsMTIgMiwxMC4yMDkxMzkgMiw4IEMyLDUuNzkwODYxIDMuNzkwODYxLDQgNiw0IFogTTYsMTEgQzcuNjU2ODU0MjUsMTEgOSw5LjY1Njg1NDI1IDksOCBDOSw2LjM0MzE0NTc1IDcuNjU2ODU0MjUsNSA2LDUgQzQuMzQzMTQ1NzUsNSAzLDYuMzQzMTQ1NzUgMyw4IEMzLDkuNjU2ODU0MjUgNC4zNDMxNDU3NSwxMSA2LDExIFoiIGZpbGw9IiMwMDAiIGZpbGwtcnVsZT0ibm9uemVybyIvPjwvc3ZnPg=='); + --icon-LoginStreamline: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0zOS41NDAyIDBIMTguMDA0N0M4LjA2NzUyIDAgMCA4Ljk3NzQgMCAyMC4wMzUzVjQwSDIxLjcwNTFDMzEuNTQ4NyA0MCAzOS41NDAyIDMxLjEwNjggMzkuNTQwMiAyMC4xNTMxVjBaIiBmaWxsPSIjRjlBRTQxIiBvcGFjaXR5PSIuMSIvPjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMjIuNDkxNyAyNy45MzA2TDI4LjQ1MyAxOC44MzA3QzI4LjY1MiAxOC41MjY4IDI4LjU3NzYgMTguMTExOSAyOC4yODY2IDE3LjkwNEMyOC4xODA1IDE3LjgyODEgMjguMDU0OCAxNy43ODc1IDI3LjkyNjIgMTcuNzg3NUgyMy4yNzlWMTEuOTk0OEMyMy4yNzkgMTEuNjI2NiAyMi45OTMyIDExLjMyODEgMjIuNjQwNyAxMS4zMjgxQzIyLjQzIDExLjMyODEgMjIuMjMyOSAxMS40MzY3IDIyLjExMzkgMTEuNjE4M0wxNi4xNTI2IDIwLjcxODNDMTUuOTUzNSAyMS4wMjIxIDE2LjAyOCAyMS40MzcgMTYuMzE4OSAyMS42NDQ5QzE2LjQyNTEgMjEuNzIwOCAxNi41NTA3IDIxLjc2MTQgMTYuNjc5NCAyMS43NjE0SDIxLjMyNjZWMjcuNTU0MkMyMS4zMjY2IDI3LjkyMjMgMjEuNjEyMyAyOC4yMjA4IDIxLjk2NDkgMjguMjIwOEMyMi4xNzU2IDI4LjIyMDggMjIuMzcyNyAyOC4xMTIyIDIyLjQ5MTcgMjcuOTMwNloiIGZpbGw9IiNGOUFFNDEiLz48cGF0aCBvcGFjaXR5PSIuNDQiIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTYuMTQ1MyAyNC43NDE5QzE2Ljg1MDMgMjQuNzQxOSAxNy40MjE5IDI1LjMzODggMTcuNDIxOSAyNi4wNzUyVjI2LjM4ODlDMTcuNDIxOSAyNy4xMjUzIDE2Ljg1MDMgMjcuNzIyMyAxNi4xNDUzIDI3LjcyMjNIMTAuODg4N0MxMC4xODM3IDI3LjcyMjMgOS42MTIxMSAyNy4xMjUzIDkuNjEyMTEgMjYuMzg4OVYyNi4wNzUyQzkuNjEyMTEgMjUuMzM4OCAxMC4xODM3IDI0Ljc0MTkgMTAuODg4NyAyNC43NDE5SDE2LjE0NTNaTTEzLjIxNjYgMTguNzgxMUMxMy45MjE3IDE4Ljc4MTEgMTQuNDkzMiAxOS4zNzgxIDE0LjQ5MzIgMjAuMTE0NFYyMC40MjgyQzE0LjQ5MzIgMjEuMTY0NSAxMy45MjE3IDIxLjc2MTUgMTMuMjE2NiAyMS43NjE1SDguOTM2MjZDOC4yMzEyMiAyMS43NjE1IDcuNjU5NjcgMjEuMTY0NSA3LjY1OTY3IDIwLjQyODJWMjAuMTE0NEM3LjY1OTY3IDE5LjM3ODEgOC4yMzEyMiAxOC43ODExIDguOTM2MjYgMTguNzgxMUgxMy4yMTY2Wk0xNi4xNDUzIDEyLjgyMDNDMTYuODUwMyAxMi44MjAzIDE3LjQyMTkgMTMuNDE3MyAxNy40MjE5IDE0LjE1MzZWMTQuNDY3NEMxNy40MjE5IDE1LjIwMzggMTYuODUwMyAxNS44MDA3IDE2LjE0NTMgMTUuODAwN0gxMC44ODg3QzEwLjE4MzcgMTUuODAwNyA5LjYxMjExIDE1LjIwMzggOS42MTIxMSAxNC40Njc0VjE0LjE1MzZDOS42MTIxMSAxMy40MTczIDEwLjE4MzcgMTIuODIwMyAxMC44ODg3IDEyLjgyMDNIMTYuMTQ1M1oiIGZpbGw9IiNGOUFFNDEiLz48L3N2Zz4='); + --icon-LoginUnify: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggb3BhY2l0eT0iLjEiIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMzkuNTQwMiAwSDE4LjAwNDdDOC4wNjc1MiAwIDAgOC45Nzc0IDAgMjAuMDM1M1Y0MEgyMS43MDUxQzMxLjU0ODcgNDAgMzkuNTQwMiAzMS4xMDY4IDM5LjU0MDIgMjAuMTUzMVYwWiIgZmlsbD0iIzcxNDFGOSIvPjxwYXRoIG9wYWNpdHk9Ii40NCIgZD0iTTEzLjgyOTcgMTYuNzc4NlYyMy4yMjNDMTMuODI5NyAyNS4wMDI2IDE1LjIxMSAyNi40NDUyIDE2LjkxNDggMjYuNDQ1MkgyMy4wODUxVjI3LjMyNEMyMy4wODUxIDI4Ljg0MzEgMjIuMjk1OCAyOS42Njc1IDIwLjg0MTMgMjkuNjY3NUgxMi45ODgzQzExLjUzMzkgMjkuNjY3NSAxMC43NDQ2IDI4Ljg0MzEgMTAuNzQ0NiAyNy4zMjRWMTkuMTIyQzEwLjc0NDYgMTcuNjAyOSAxMS41MzM5IDE2Ljc3ODYgMTIuOTg4MyAxNi43Nzg2SDEzLjgyOTdaTTI1Ljk4MzIgMTEuNDA4MkMyNy40Mzc2IDExLjQwODIgMjguMjI2OSAxMi4yMzI2IDI4LjIyNjkgMTMuNzUxNlYyMS45NTM3QzI4LjIyNjkgMjMuNDcyNyAyNy40Mzc2IDI0LjI5NzEgMjUuOTgzMiAyNC4yOTcxSDI1LjE0MThWMTcuODUyNkMyNS4xNDE4IDE2LjA3MzEgMjMuNzYwNSAxNC42MzA0IDIyLjA1NjcgMTQuNjMwNEgxNS44ODY1VjEzLjc1MTZDMTUuODg2NSAxMi4yMzI2IDE2LjY3NTggMTEuNDA4MiAxOC4xMzAyIDExLjQwODJIMjUuOTgzMloiIGZpbGw9IiM3MTQxRjkiLz48cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTE3LjE5NTYgMTYuNzc5M0gyMS43NzY1QzIyLjYyNDkgMTYuNzc5MyAyMy4wODUzIDE3LjI2MDIgMjMuMDg1MyAxOC4xNDYzVjIyLjkzMDhDMjMuMDg1MyAyMy44MTY5IDIyLjYyNDkgMjQuMjk3OCAyMS43NzY1IDI0LjI5NzhIMTcuMTk1NkMxNi4zNDcxIDI0LjI5NzggMTUuODg2NyAyMy44MTY5IDE1Ljg4NjcgMjIuOTMwOFYxOC4xNDYzQzE1Ljg4NjcgMTcuMjYwMiAxNi4zNDcxIDE2Ljc3OTMgMTcuMTk1NiAxNi43NzkzWiIgZmlsbD0iIzcxNDFGOSIvPjwvc3ZnPg=='); + --icon-LoginVisualize: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggb3BhY2l0eT0iLjEiIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMzkuNTQwMiAwSDE4LjAwNDdDOC4wNjc1MiAwIDAgOC45Nzc0IDAgMjAuMDM1M1Y0MEgyMS43MDUxQzMxLjU0ODcgNDAgMzkuNTQwMiAzMS4xMDY4IDM5LjU0MDIgMjAuMTUzMVYwWiIgZmlsbD0iIzE2QjM3OCIvPjxwYXRoIG9wYWNpdHk9Ii40NCIgZD0iTTIzLjk3MTggMTIuMjIxNkMyMy45NzE4IDExLjM2MjUgMjMuMzA1IDEwLjY2NiAyMi40ODI0IDEwLjY2NkMyMS42NTk5IDEwLjY2NiAyMC45OTMgMTEuMzYyNSAyMC45OTMgMTIuMjIxNlYyNi43NDAxQzIwLjk5MyAyNy41OTkyIDIxLjY1OTkgMjguMjk1NiAyMi40ODI0IDI4LjI5NTZDMjMuMzA1IDI4LjI5NTYgMjMuOTcxOCAyNy41OTkyIDIzLjk3MTggMjYuNzQwMVYxMi4yMjE2WiIgZmlsbD0iIzE2QjM3OCIvPjxwYXRoIGQ9Ik0xOS4wMDcgMTcuNzMwNEMxOS4wMDcgMTYuODcxMyAxOC4zNDAyIDE2LjE3NDggMTcuNTE3NyAxNi4xNzQ4IDE2LjY5NTEgMTYuMTc0OCAxNi4wMjgzIDE2Ljg3MTMgMTYuMDI4MyAxNy43MzA0VjI2LjczOTZDMTYuMDI4MyAyNy41OTg3IDE2LjY5NTEgMjguMjk1MiAxNy41MTc3IDI4LjI5NTIgMTguMzQwMiAyOC4yOTUyIDE5LjAwNyAyNy41OTg3IDE5LjAwNyAyNi43Mzk2VjE3LjczMDR6TTI4LjkzNjIgMTkuOTM0NUMyOC45MzYyIDE5LjA3NTQgMjguMjY5NCAxOC4zNzg5IDI3LjQ0NjkgMTguMzc4OSAyNi42MjQzIDE4LjM3ODkgMjUuOTU3NSAxOS4wNzU0IDI1Ljk1NzUgMTkuOTM0NVYyNi43NEMyNS45NTc1IDI3LjU5OTEgMjYuNjI0MyAyOC4yOTU2IDI3LjQ0NjkgMjguMjk1NiAyOC4yNjk0IDI4LjI5NTYgMjguOTM2MiAyNy41OTkxIDI4LjkzNjIgMjYuNzRWMTkuOTM0NXpNMTQuMDQyNyAyMi4xMzg2QzE0LjA0MjcgMjEuMjc5NSAxMy4zNzU5IDIwLjU4MyAxMi41NTMzIDIwLjU4MyAxMS43MzA4IDIwLjU4MyAxMS4wNjQgMjEuMjc5NSAxMS4wNjQgMjIuMTM4NlYyNi43NDA0QzExLjA2NCAyNy41OTk1IDExLjczMDggMjguMjk2IDEyLjU1MzMgMjguMjk2IDEzLjM3NTkgMjguMjk2IDE0LjA0MjcgMjcuNTk5NSAxNC4wNDI3IDI2Ljc0MDRWMjIuMTM4NnoiIGZpbGw9IiMxNkIzNzgiLz48L3N2Zz4='); + --icon-GoogleLogo: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZmlsbD0iIzQyODVGNCIgZD0iTSAtMy4yNjQgNTEuNTA5IEMgLTMuMjY0IDUwLjcxOSAtMy4zMzQgNDkuOTY5IC0zLjQ1NCA0OS4yMzkgTCAtMTQuNzU0IDQ5LjIzOSBMIC0xNC43NTQgNTMuNzQ5IEwgLTguMjg0IDUzLjc0OSBDIC04LjU3NCA1NS4yMjkgLTkuNDI0IDU2LjQ3OSAtMTAuNjg0IDU3LjMyOSBMIC0xMC42ODQgNjAuMzI5IEwgLTYuODI0IDYwLjMyOSBDIC00LjU2NCA1OC4yMzkgLTMuMjY0IDU1LjE1OSAtMy4yNjQgNTEuNTA5IFoiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDI3LjAwOSAtMzkuMjM5KSIvPjxwYXRoIGZpbGw9IiMzNEE4NTMiIGQ9Ik0gLTE0Ljc1NCA2My4yMzkgQyAtMTEuNTE0IDYzLjIzOSAtOC44MDQgNjIuMTU5IC02LjgyNCA2MC4zMjkgTCAtMTAuNjg0IDU3LjMyOSBDIC0xMS43NjQgNTguMDQ5IC0xMy4xMzQgNTguNDg5IC0xNC43NTQgNTguNDg5IEMgLTE3Ljg4NCA1OC40ODkgLTIwLjUzNCA1Ni4zNzkgLTIxLjQ4NCA1My41MjkgTCAtMjUuNDY0IDUzLjUyOSBMIC0yNS40NjQgNTYuNjE5IEMgLTIzLjQ5NCA2MC41MzkgLTE5LjQ0NCA2My4yMzkgLTE0Ljc1NCA2My4yMzkgWiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjcuMDA5IC0zOS4yMzkpIi8+PHBhdGggZmlsbD0iI0ZCQkMwNSIgZD0iTSAtMjEuNDg0IDUzLjUyOSBDIC0yMS43MzQgNTIuODA5IC0yMS44NjQgNTIuMDM5IC0yMS44NjQgNTEuMjM5IEMgLTIxLjg2NCA1MC40MzkgLTIxLjcyNCA0OS42NjkgLTIxLjQ4NCA0OC45NDkgTCAtMjEuNDg0IDQ1Ljg1OSBMIC0yNS40NjQgNDUuODU5IEMgLTI2LjI4NCA0Ny40NzkgLTI2Ljc1NCA0OS4yOTkgLTI2Ljc1NCA1MS4yMzkgQyAtMjYuNzU0IDUzLjE3OSAtMjYuMjg0IDU0Ljk5OSAtMjUuNDY0IDU2LjYxOSBMIC0yMS40ODQgNTMuNTI5IFoiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDI3LjAwOSAtMzkuMjM5KSIvPjxwYXRoIGZpbGw9IiNFQTQzMzUiIGQ9Ik0gLTE0Ljc1NCA0My45ODkgQyAtMTIuOTg0IDQzLjk4OSAtMTEuNDA0IDQ0LjU5OSAtMTAuMTU0IDQ1Ljc4OSBMIC02LjczNCA0Mi4zNjkgQyAtOC44MDQgNDAuNDI5IC0xMS41MTQgMzkuMjM5IC0xNC43NTQgMzkuMjM5IEMgLTE5LjQ0NCAzOS4yMzkgLTIzLjQ5NCA0MS45MzkgLTI1LjQ2NCA0NS44NTkgTCAtMjEuNDg0IDQ4Ljk0OSBDIC0yMC41MzQgNDYuMDk5IC0xNy44ODQgNDMuOTg5IC0xNC43NTQgNDMuOTg5IFoiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDI3LjAwOSAtMzkuMjM5KSIvPjwvc3ZnPg=='); --icon-GristLogo: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDEiIGhlaWdodD0iMzgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48cGF0aCBkPSJNMTAuODYyNDc0NSwwLjA5ODU5MTczNDcgTDUuMDE1NTg3NDMsMC4wOTg1OTE3MzQ3IEMyLjMxNzYzNDkxLDAuMDk4NTkxNzM0NyAwLjEyNzMwMTg2LDIuMjg4OTIwMDkgMC4xMjczMDE4Niw0Ljk4Njg0MTgzIEwwLjEyNzMwMTg2LDkuODU3ODg5NzIgTDYuMDIwMjM5OTUsOS44NTc4ODk3MiBDOC42OTI3ODUwMyw5Ljg1Nzg4OTcyIDEwLjg2MjQ3NDUsNy42ODgxMDEzMSAxMC44NjI0NzQ1LDUuMDE1NTk3NzUgTDEwLjg2MjQ3NDUsMC4wOTg1OTE3MzQ3IFoiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDI4LjEzNCAxLjE2MikiIGZpbGw9IiMyQ0IwQUYiLz48cGF0aCBkPSJNMTAuODYyNDc0NSwwLjA5ODU5MTczNDcgTDUuMDE1NTg3NDMsMC4wOTg1OTE3MzQ3IEMyLjMxNzYzNDkxLDAuMDk4NTkxNzM0NyAwLjEyNzMwMTg2LDIuMjg4OTIwMDkgMC4xMjczMDE4Niw0Ljk4Njg0MTgzIEwwLjEyNzMwMTg2LDkuODU3ODg5NzIgTDYuMDIwMjM5OTUsOS44NTc4ODk3MiBDOC42OTI3ODUwMyw5Ljg1Nzg4OTcyIDEwLjg2MjQ3NDUsNy42ODgxMDEzMSAxMC44NjI0NzQ1LDUuMDE1NTk3NzUgTDEwLjg2MjQ3NDUsMC4wOTg1OTE3MzQ3IFoiIHRyYW5zZm9ybT0ibWF0cml4KC0xIDAgMCAxIDI1LjU3IDEuMTYyKSIgZmlsbD0iIzJDQjBBRiIvPjxwYXRoIGQ9Ik0xMC44NjI0NzQ1LDAuMDk4NTkxNzM0NyBMNS4wMTU1ODc0MywwLjA5ODU5MTczNDcgQzIuMzE3NjM0OTEsMC4wOTg1OTE3MzQ3IDAuMTI3MzAxODYsMi4yODg5MjAwOSAwLjEyNzMwMTg2LDQuOTg2ODQxODMgTDAuMTI3MzAxODYsOS44NTc4ODk3MiBMNi4wMjAyMzk5NSw5Ljg1Nzg4OTcyIEM4LjY5Mjc4NTAzLDkuODU3ODg5NzIgMTAuODYyNDc0NSw3LjY4ODEwMTMxIDEwLjg2MjQ3NDUsNS4wMTU1OTc3NSBMMTAuODYyNDc0NSwwLjA5ODU5MTczNDcgWiIgdHJhbnNmb3JtPSJtYXRyaXgoLTEgMCAwIDEgMTIuMDE1IDEzLjc0KSIgZmlsbD0iI0Y5QUU0MSIvPjxwYXRoIGQ9Ik0xMC44NjI0NzQ1LDAuMDk4NTkxNzM0NyBMNS4wMTU1ODc0MywwLjA5ODU5MTczNDcgQzIuMzE3NjM0OTEsMC4wOTg1OTE3MzQ3IDAuMTI3MzAxODYsMi4yODg5MjAwOSAwLjEyNzMwMTg2LDQuOTg2ODQxODMgTDAuMTI3MzAxODYsOS44NTc4ODk3MiBMNi4wMjAyMzk5NSw5Ljg1Nzg4OTcyIEM4LjY5Mjc4NTAzLDkuODU3ODg5NzIgMTAuODYyNDc0NSw3LjY4ODEwMTMxIDEwLjg2MjQ3NDUsNS4wMTU1OTc3NSBMMTAuODYyNDc0NSwwLjA5ODU5MTczNDcgWiIgdHJhbnNmb3JtPSJtYXRyaXgoLTEgMCAwIDEgMTIuMDE1IDI2LjMxOSkiIGZpbGw9IiNGOUFFNDEiLz48cGF0aCBkPSJNMTAuODYyNDc0NSwwLjA5ODU5MTczNDcgTDUuMDE1NTg3NDMsMC4wOTg1OTE3MzQ3IEMyLjMxNzYzNDkxLDAuMDk4NTkxNzM0NyAwLjEyNzMwMTg2LDIuMjg4OTIwMDkgMC4xMjczMDE4Niw0Ljk4Njg0MTgzIEwwLjEyNzMwMTg2LDkuODU3ODg5NzIgTDYuMDIwMjM5OTUsOS44NTc4ODk3MiBDOC42OTI3ODUwMyw5Ljg1Nzg4OTcyIDEwLjg2MjQ3NDUsNy42ODgxMDEzMSAxMC44NjI0NzQ1LDUuMDE1NTk3NzUgTDEwLjg2MjQ3NDUsMC4wOTg1OTE3MzQ3IFoiIHRyYW5zZm9ybT0ibWF0cml4KC0xIDAgMCAxIDI1LjU3IDEzLjc0KSIgZmlsbD0iI0QyRDJEMiIvPjxwYXRoIGQ9Ik0xMC44NjI0NzQ1LDAuMDk4NTkxNzM0NyBMNS4wMTU1ODc0MywwLjA5ODU5MTczNDcgQzIuMzE3NjM0OTEsMC4wOTg1OTE3MzQ3IDAuMTI3MzAxODYsMi4yODg5MjAwOSAwLjEyNzMwMTg2LDQuOTg2ODQxODMgTDAuMTI3MzAxODYsOS44NTc4ODk3MiBMNi4wMjAyMzk5NSw5Ljg1Nzg4OTcyIEM4LjY5Mjc4NTAzLDkuODU3ODg5NzIgMTAuODYyNDc0NSw3LjY4ODEwMTMxIDEwLjg2MjQ3NDUsNS4wMTU1OTc3NSBMMTAuODYyNDc0NSwwLjA5ODU5MTczNDcgWiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjguMTM0IDEzLjc0KSIgZmlsbD0iI0QyRDJEMiIvPjxwYXRoIGQ9Ik0xMC44NjI0NzQ1LDAuMDk4NTkxNzM0NyBMNS4wMTU1ODc0MywwLjA5ODU5MTczNDcgQzIuMzE3NjM0OTEsMC4wOTg1OTE3MzQ3IDAuMTI3MzAxODYsMi4yODg5MjAwOSAwLjEyNzMwMTg2LDQuOTg2ODQxODMgTDAuMTI3MzAxODYsOS44NTc4ODk3MiBMNi4wMjAyMzk5NSw5Ljg1Nzg4OTcyIEM4LjY5Mjc4NTAzLDkuODU3ODg5NzIgMTAuODYyNDc0NSw3LjY4ODEwMTMxIDEwLjg2MjQ3NDUsNS4wMTU1OTc3NSBMMTAuODYyNDc0NSwwLjA5ODU5MTczNDcgWiIgdHJhbnNmb3JtPSJtYXRyaXgoLTEgMCAwIDEgMjUuNTcgMjYuMzE5KSIgZmlsbD0iI0QyRDJEMiIvPjxwYXRoIGQ9Ik0xMC44NjI0NzQ1LDAuMDk4NTkxNzM0NyBMNS4wMTU1ODc0MywwLjA5ODU5MTczNDcgQzIuMzE3NjM0OTEsMC4wOTg1OTE3MzQ3IDAuMTI3MzAxODYsMi4yODg5MjAwOSAwLjEyNzMwMTg2LDQuOTg2ODQxODMgTDAuMTI3MzAxODYsOS44NTc4ODk3MiBMNi4wMjAyMzk5NSw5Ljg1Nzg4OTcyIEM4LjY5Mjc4NTAzLDkuODU3ODg5NzIgMTAuODYyNDc0NSw3LjY4ODEwMTMxIDEwLjg2MjQ3NDUsNS4wMTU1OTc3NSBMMTAuODYyNDc0NSwwLjA5ODU5MTczNDcgWiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjguMTM0IDI2LjMxOSkiIGZpbGw9IiNEMkQyRDIiLz48L2c+PC9zdmc+'); --icon-ThumbPreview: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDYiIGhlaWdodD0iNDYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPjxkZWZzPjxwYXRoIGQ9Ik00LjA1NzYxNzE5LDYuMzY4MTY0MDYgTDQuMDU3NjE3MTksNS4xNjIxMDkzOCBMNy4xNzI4NTE1Niw1LjE2MjEwOTM4IEw3LjE3Mjg1MTU2LDguMDEzNjcxODggQzYuODcwMTE1NjcsOC4zMDY2NDIwOSA2LjQzMTQ4MDc0LDguNTY0NjE0NzcgNS44NTY5MzM1OSw4Ljc4NzU5NzY2IEM1LjI4MjM4NjQ1LDkuMDEwNTgwNTQgNC43MDA1MjM3OCw5LjEyMjA3MDMxIDQuMTExMzI4MTIsOS4xMjIwNzAzMSBDMy4zNjI2MjY0Niw5LjEyMjA3MDMxIDIuNzA5OTYzNzIsOC45NjUwMDgwOCAyLjE1MzMyMDMxLDguNjUwODc4OTEgQzEuNTk2Njc2OSw4LjMzNjc0OTczIDEuMTc4Mzg2ODIsNy44ODc1MzU0NyAwLjg5ODQzNzUsNy4zMDMyMjI2NiBDMC42MTg0ODgxODQsNi43MTg5MDk4NCAwLjQ3ODUxNTYyNSw2LjA4MzMzNjc3IDAuNDc4NTE1NjI1LDUuMzk2NDg0MzggQzAuNDc4NTE1NjI1LDQuNjUxMDM3OTQgMC42MzQ3NjQwNjIsMy45ODg2MDk2NyAwLjk0NzI2NTYyNSwzLjQwOTE3OTY5IEMxLjI1OTc2NzE5LDIuODI5NzQ5NzEgMS43MTcxMTkzOCwyLjM4NTQxODIxIDIuMzE5MzM1OTQsMi4wNzYxNzE4OCBDMi43NzgzMjI2MSwxLjgzODU0MDQ4IDMuMzQ5NjA1OTYsMS43MTk3MjY1NiA0LjAzMzIwMzEyLDEuNzE5NzI2NTYgQzQuOTIxODc5NDQsMS43MTk3MjY1NiA1LjYxNjA0NTY4LDEuOTA2MDg1MzggNi4xMTU3MjI2NiwyLjI3ODgwODU5IEM2LjYxNTM5OTYzLDIuNjUxNTMxODEgNi45MzY4NDgyNCwzLjE2NjY2MzM4IDcuMDgwMDc4MTIsMy44MjQyMTg3NSBMNS42NDQ1MzEyNSw0LjA5Mjc3MzQ0IEM1LjU0MzYxOTI5LDMuNzQxMjA5MTggNS4zNTQwMDUzLDMuNDYzNzA1NDQgNS4wNzU2ODM1OSwzLjI2MDI1MzkxIEM0Ljc5NzM2MTg5LDMuMDU2ODAyMzcgNC40NDk4NzE4NywyLjk1NTA3ODEyIDQuMDMzMjAzMTIsMi45NTUwNzgxMiBDMy40MDE2ODk1NSwyLjk1NTA3ODEyIDIuODk5NTc4NjksMy4xNTUyNzE0NCAyLjUyNjg1NTQ3LDMuNTU1NjY0MDYgQzIuMTU0MTMyMjUsMy45NTYwNTY2OSAxLjk2Nzc3MzQ0LDQuNTUwMTI2MjcgMS45Njc3NzM0NCw1LjMzNzg5MDYyIEMxLjk2Nzc3MzQ0LDYuMTg3NTA0MjUgMi4xNTY1NzM2Myw2LjgyNDcwNDkxIDIuNTM0MTc5NjksNy4yNDk1MTE3MiBDMi45MTE3ODU3NCw3LjY3NDMxODUzIDMuNDA2NTcyNDYsNy44ODY3MTg3NSA0LjAxODU1NDY5LDcuODg2NzE4NzUgQzQuMzIxMjkwNTgsNy44ODY3MTg3NSA0LjYyNDgzNTcyLDcuODI3MzExNzkgNC45MjkxOTkyMiw3LjcwODQ5NjA5IEM1LjIzMzU2MjcyLDcuNTg5NjgwNCA1LjQ5NDc5MDU4LDcuNDQ1NjM4ODcgNS43MTI4OTA2Miw3LjI3NjM2NzE5IEw1LjcxMjg5MDYyLDYuMzY4MTY0MDYgTDQuMDU3NjE3MTksNi4zNjgxNjQwNiBaIE05LjIxMDc0MjE4LDkgTDkuMjEwNzQyMTgsMS44NDE3OTY4OCBMMTIuMjUyNzM0NCwxLjg0MTc5Njg4IEMxMy4wMTc3MTIxLDEuODQxNzk2ODggMTMuNTczNTMzNCwxLjkwNjA4NjYgMTMuOTIwMjE0OCwyLjAzNDY2Nzk3IEMxNC4yNjY4OTYzLDIuMTYzMjQ5MzQgMTQuNTQ0NCwyLjM5MTkyNTQ0IDE0Ljc1MjczNDQsMi43MjA3MDMxMiBDMTQuOTYxMDY4NywzLjA0OTQ4MDgxIDE1LjA2NTIzNDQsMy40MjU0NTM2MSAxNS4wNjUyMzQ0LDMuODQ4NjMyODEgQzE1LjA2NTIzNDQsNC4zODU3NDQ4NyAxNC45MDczNTgzLDQuODI5MjYyNTcgMTQuNTkxNjAxNiw1LjE3OTE5OTIyIEMxNC4yNzU4NDQ4LDUuNTI5MTM1ODYgMTMuODAzODQ0Myw1Ljc0OTY3NDAyIDEzLjE3NTU4NTksNS44NDA4MjAzMSBDMTMuNDg4MDg3NSw2LjAyMzExMjg5IDEzLjc0NjA2MDIsNi4yMjMzMDYyIDEzLjk0OTUxMTcsNi40NDE0MDYyNSBDMTQuMTUyOTYzMiw2LjY1OTUwNjMgMTQuNDI3MjExOCw3LjA0Njg3MjIyIDE0Ljc3MjI2NTYsNy42MDM1MTU2MiBMMTUuNjQ2Mjg5MSw5IEwxMy45MTc3NzM0LDkgTDEyLjg3Mjg1MTYsNy40NDIzODI4MSBDMTIuNTAxNzU1OSw2Ljg4NTczOTQgMTIuMjQ3ODUyMiw2LjUzNDk5NDIxIDEyLjExMTEzMjgsNi4zOTAxMzY3MiBDMTEuOTc0NDEzNCw2LjI0NTI3OTIyIDExLjgyOTU1OCw2LjE0NTk5NjM2IDExLjY3NjU2MjUsNi4wOTIyODUxNiBDMTEuNTIzNTY2OSw2LjAzODU3Mzk1IDExLjI4MTA1NjMsNi4wMTE3MTg3NSAxMC45NDkwMjM0LDYuMDExNzE4NzUgTDEwLjY1NjA1NDcsNi4wMTE3MTg3NSBMMTAuNjU2MDU0Nyw5IEw5LjIxMDc0MjE4LDkgWiBNMTAuNjU2MDU0Nyw0Ljg2OTE0MDYyIEwxMS43MjUzOTA2LDQuODY5MTQwNjIgQzEyLjQxODc1MzUsNC44NjkxNDA2MiAxMi44NTE2OTE4LDQuODM5ODQ0MDQgMTMuMDI0MjE4Nyw0Ljc4MTI1IEMxMy4xOTY3NDU2LDQuNzIyNjU1OTYgMTMuMzMxODM1NCw0LjYyMTc0NTUxIDEzLjQyOTQ5MjIsNC40Nzg1MTU2MiBDMTMuNTI3MTQ4OSw0LjMzNTI4NTc0IDEzLjU3NTk3NjYsNC4xNTYyNTEwNyAxMy41NzU5NzY2LDMuOTQxNDA2MjUgQzEzLjU3NTk3NjYsMy43MDA1MTk2MyAxMy41MTE2ODY4LDMuNTA2MDIyODggMTMuMzgzMTA1NSwzLjM1NzkxMDE2IEMxMy4yNTQ1MjQxLDMuMjA5Nzk3NDQgMTMuMDczMDQ4LDMuMTE2MjExMTMgMTIuODM4NjcxOSwzLjA3NzE0ODQ0IEMxMi43MjE0ODM4LDMuMDYwODcyMzEgMTIuMzY5OTI0OCwzLjA1MjczNDM4IDExLjc4Mzk4NDQsMy4wNTI3MzQzOCBMMTAuNjU2MDU0NywzLjA1MjczNDM4IEwxMC42NTYwNTQ3LDQuODY5MTQwNjIgWiBNMTcuMDgzNTkzNyw5IEwxNy4wODM1OTM3LDEuODQxNzk2ODggTDE4LjUyODkwNjIsMS44NDE3OTY4OCBMMTguNTI4OTA2Miw5IEwxNy4wODM1OTM3LDkgWiBNMjAuMjM5NjQ4NCw2LjY3MDg5ODQ0IEwyMS42NDU4OTg0LDYuNTM0MTc5NjkgQzIxLjczMDUzNDIsNy4wMDYxODcyNiAyMS45MDIyNDQ4LDcuMzUyODYzNDggMjIuMTYxMDM1MSw3LjU3NDIxODc1IEMyMi40MTk4MjU1LDcuNzk1NTc0MDIgMjIuNzY4OTQzMSw3LjkwNjI1IDIzLjIwODM5ODQsNy45MDYyNSBDMjMuNjczODk1NSw3LjkwNjI1IDI0LjAyNDY0MDcsNy44MDc3ODA5MyAyNC4yNjA2NDQ1LDcuNjEwODM5ODQgQzI0LjQ5NjY0ODMsNy40MTM4OTg3NSAyNC42MTQ2NDg0LDcuMTgzNTk1MDcgMjQuNjE0NjQ4NCw2LjkxOTkyMTg4IEMyNC42MTQ2NDg0LDYuNzUwNjUwMiAyNC41NjUwMDcsNi42MDY2MDg2NyAyNC40NjU3MjI2LDYuNDg3NzkyOTcgQzI0LjM2NjQzODMsNi4zNjg5NzcyNyAyNC4xOTMxMDAyLDYuMjY1NjI1NDQgMjMuOTQ1NzAzMSw2LjE3NzczNDM4IEMyMy43NzY0MzE0LDYuMTE5MTQwMzMgMjMuMzkwNjkzMSw2LjAxNDk3NDcxIDIyLjc4ODQ3NjUsNS44NjUyMzQzOCBDMjIuMDEzNzMzMSw1LjY3MzE3NjEyIDIxLjQ3MDExODcsNS40MzcxNzU4OCAyMS4xNTc2MTcyLDUuMTU3MjI2NTYgQzIwLjcxODE2MTgsNC43NjMzNDQzOCAyMC40OTg0Mzc1LDQuMjgzMjA1OTYgMjAuNDk4NDM3NSwzLjcxNjc5Njg4IEMyMC40OTg0Mzc1LDMuMzUyMjExNzIgMjAuNjAxNzg5MywzLjAxMTIzMjA2IDIwLjgwODQ5NjEsMi42OTM4NDc2NiBDMjEuMDE1MjAyOCwyLjM3NjQ2MzI2IDIxLjMxMzA1MTQsMi4xMzQ3NjY0NiAyMS43MDIwNTA3LDEuOTY4NzUgQzIyLjA5MTA1MDEsMS44MDI3MzM1NCAyMi41NjA2MDkyLDEuNzE5NzI2NTYgMjMuMTEwNzQyMiwxLjcxOTcyNjU2IEMyNC4wMDkxODQxLDEuNzE5NzI2NTYgMjQuNjg1NDQ2OSwxLjkxNjY2NDcgMjUuMTM5NTUwNywyLjMxMDU0Njg4IEMyNS41OTM2NTQ2LDIuNzA0NDI5MDUgMjUuODMyMDk2MiwzLjIzMDEzOTk0IDI1Ljg1NDg4MjgsMy44ODc2OTUzMSBMMjQuNDA5NTcwMywzLjk1MTE3MTg4IEMyNC4zNDc3MjEsMy41ODMzMzE0OSAyNC4yMTUwNzI2LDMuMzE4ODQ4NDYgMjQuMDExNjIxMSwzLjE1NzcxNDg0IEMyMy44MDgxNjk1LDIuOTk2NTgxMjMgMjMuNTAyOTk2OCwyLjkxNjAxNTYyIDIzLjA5NjA5MzcsMi45MTYwMTU2MiBDMjIuNjc2MTY5NywyLjkxNjAxNTYyIDIyLjM0NzM5NywzLjAwMjI3Nzc4IDIyLjEwOTc2NTYsMy4xNzQ4MDQ2OSBDMjEuOTU2NzcsMy4yODU0ODIzMiAyMS44ODAyNzM0LDMuNDMzNTkyODIgMjEuODgwMjczNCwzLjYxOTE0MDYyIEMyMS44ODAyNzM0LDMuNzg4NDEyMyAyMS45NTE4ODczLDMuOTMzMjY3NjMgMjIuMDk1MTE3Miw0LjA1MzcxMDk0IEMyMi4yNzc0MDk3LDQuMjA2NzA2NDkgMjIuNzIwMTEzNiw0LjM2NjIxMDExIDIzLjQyMzI0MjIsNC41MzIyMjY1NiBDMjQuMTI2MzcwNyw0LjY5ODI0MzAyIDI0LjY0NjM4NSw0Ljg2OTk1MzU0IDI0Ljk4MzMwMDcsNS4wNDczNjMyOCBDMjUuMzIwMjE2NSw1LjIyNDc3MzAyIDI1LjU4Mzg4NTcsNS40NjcyODM2MiAyNS43NzQzMTY0LDUuNzc0OTAyMzQgQzI1Ljk2NDc0Nyw2LjA4MjUyMTA3IDI2LjA1OTk2MDksNi40NjI1NjI4NCAyNi4wNTk5NjA5LDYuOTE1MDM5MDYgQzI2LjA1OTk2MDksNy4zMjUxOTczNiAyNS45NDYwMjk3LDcuNzA5MzA4MTEgMjUuNzE4MTY0LDguMDY3MzgyODEgQzI1LjQ5MDI5ODMsOC40MjU0NTc1MiAyNS4xNjgwMzU5LDguNjkxNTY4MTQgMjQuNzUxMzY3Miw4Ljg2NTcyMjY2IEMyNC4zMzQ2OTg0LDkuMDM5ODc3MTcgMjMuODE1NDk3OSw5LjEyNjk1MzEyIDIzLjE5Mzc1LDkuMTI2OTUzMTIgQzIyLjI4ODc5NzUsOS4xMjY5NTMxMiAyMS41OTM4MTc1LDguOTE3ODA4MDggMjEuMTA4Nzg5LDguNDk5NTExNzIgQzIwLjYyMzc2MDYsOC4wODEyMTUzNiAyMC4zMzQwNDk5LDcuNDcxNjgzNjkgMjAuMjM5NjQ4NCw2LjY3MDg5ODQ0IFogTTI5LjU4NzEwOTMsOSBMMjkuNTg3MTA5MywzLjA1MjczNDM4IEwyNy40NjMwODU5LDMuMDUyNzM0MzggTDI3LjQ2MzA4NTksMS44NDE3OTY4OCBMMzMuMTUxNTYyNSwxLjg0MTc5Njg4IEwzMy4xNTE1NjI1LDMuMDUyNzM0MzggTDMxLjAzMjQyMTgsMy4wNTI3MzQzOCBMMzEuMDMyNDIxOCw5IEwyOS41ODcxMDkzLDkgWiIgaWQ9ImEiLz48L2RlZnM+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBvcGFjaXR5PSIuOSI+PHBhdGggZD0iTTAgMEg0OFY0OEgweiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTEgLTEpIi8+PHBhdGggZmlsbD0iIzBFNTEyQiIgZD0iTTQzIDI2TDMgMjYgMCAyMiA0IDE4IDQyIDE4IDQ2IDIyeiIvPjxwYXRoIGQ9Ik00MCw0NiBMNiw0NiBDNC44OTUsNDYgNCw0NS4xMDUgNCw0NCBMNCwyIEM0LDAuODk1IDQuODk1LDAgNiwwIEwzMCwwIEw0MiwxMiBMNDIsNDQgQzQyLDQ1LjEwNSA0MS4xMDUsNDYgNDAsNDYgWiIgZmlsbD0iI0U2RTZFNiIvPjxwYXRoIGQ9Ik0zMCwwIEwzMCwxMCBDMzAsMTEuMTA1IDMwLjg5NSwxMiAzMiwxMiBMNDIsMTIgTDMwLDAgWiIgZmlsbD0iI0IzQjNCMyIvPjxwYXRoIGQ9Ik00NCw0MCBMMiw0MCBDMC44OTUsNDAgMCwzOS4xMDUgMCwzOCBMMCwyMiBMNDYsMjIgTDQ2LDM4IEM0NiwzOS4xMDUgNDUuMTA1LDQwIDQ0LDQwIFoiIGZpbGw9IiMxNkIzNzgiLz48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg2IDI1KSI+PHVzZSBmaWxsPSIjMDAwIiB4bGluazpocmVmPSIjYSIvPjx1c2UgZmlsbD0iI0ZGRiIgeGxpbms6aHJlZj0iI2EiLz48L2c+PC9nPjwvc3ZnPg=='); --icon-BarcodeQR: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTYuNS41SC41VjYuNUg2LjVWLjV6TTYuNSA5LjVILjVWMTUuNUg2LjVWOS41ek0xNS41LjVIOS41VjYuNUgxNS41Vi41ek0xNS41IDE1LjVIOS41TTExLjUgOS41SDE1LjVWMTIuNSIgc3Ryb2tlPSIjMjEyMTIxIiBzdHJva2UtbWl0ZXJsaW1pdD0iMTAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPjwvc3ZnPg=='); diff --git a/static/ui-icons/Login/LoginStreamline.svg b/static/ui-icons/Login/LoginStreamline.svg new file mode 100644 index 00000000..6a7c849a --- /dev/null +++ b/static/ui-icons/Login/LoginStreamline.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/static/ui-icons/Login/LoginUnify.svg b/static/ui-icons/Login/LoginUnify.svg new file mode 100644 index 00000000..48d7d260 --- /dev/null +++ b/static/ui-icons/Login/LoginUnify.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/static/ui-icons/Login/LoginVisualize.svg b/static/ui-icons/Login/LoginVisualize.svg new file mode 100644 index 00000000..f9884240 --- /dev/null +++ b/static/ui-icons/Login/LoginVisualize.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/static/ui-icons/Logo/GoogleLogo.svg b/static/ui-icons/Logo/GoogleLogo.svg new file mode 100644 index 00000000..276052d3 --- /dev/null +++ b/static/ui-icons/Logo/GoogleLogo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/stubs/app/server/lib/logins.ts b/stubs/app/server/lib/logins.ts index bceeac2b..c60f0168 100644 --- a/stubs/app/server/lib/logins.ts +++ b/stubs/app/server/lib/logins.ts @@ -7,3 +7,7 @@ export async function getLoginSystem(): Promise { if (saml) { return saml; } return getMinimalLoginSystem(); } + +export function getLoginSubdomain(): string | null { + return null; +} diff --git a/test/nbrowser/homeUtil.ts b/test/nbrowser/homeUtil.ts index 8d0a6c95..89346224 100644 --- a/test/nbrowser/homeUtil.ts +++ b/test/nbrowser/homeUtil.ts @@ -77,11 +77,18 @@ export class HomeUtil { await this.driver.get('about:blank'); // When running against an external server, we log in through Cognito. await this.driver.get(this.server.getUrl(org, "")); - if (!(await this.isOnLoginPage())) { + // Check if we got redirected to the Grist sign-up page. + if (await this.isOnSignupPage()) { + await this.driver.findWait('a[href*="login?"]', 4000).click(); + } else if (!(await this.isOnLoginPage())) { // Explicitly click sign-in link if necessary. await this.driver.findWait('.test-user-signin', 4000).click(); await this.driver.findContentWait('.grist-floating-menu a', 'Sign in', 500).click(); } + // Check if we need to switch to Cognito login from the Grist sign-up page. + if (await this.isOnSignupPage()) { + await this.driver.findWait('a[href*="login?"]', 4000).click(); + } await this.checkLoginPage(); await this.fillLoginForm(email); if (!(await this.isWelcomePage()) && (options.freshAccount || options.isFirstLogin)) { @@ -255,6 +262,13 @@ export class HomeUtil { return /gristlogin/.test(await this.driver.getCurrentUrl()); } + /** + * Returns whether we are currently on the Grist sign-up page. + */ + public async isOnSignupPage() { + return /login(-s)?\.getgrist\.com\/signup/.test(await this.driver.getCurrentUrl()); + } + /** * Waits for browser to navigate to Cognito login page. */