From 12f9567ff42abd40f03b8c33d7a3e1bc3b80f860 Mon Sep 17 00:00:00 2001 From: Paul Fitzpatrick Date: Tue, 21 Mar 2023 09:32:34 -0400 Subject: [PATCH] (core) add a /welcome/start endpoint that forwards sensibly Summary: This adds a nuanced redirecting endpoint. For example, on docs.getgrist.com it does: 1) If logged in and no team site -> https://docs.getgrist.com/ 2) If logged in and has team sites -> https://docs.getgrist.com/welcome/teams 3) If logged out but has a cookie -> /login, then 1 or 2 4) If entirely unknown -> /signup Test Plan: added a test; tested behavior through logins manually Reviewers: dsagal Reviewed By: dsagal Subscribers: dsagal Differential Revision: https://phab.getgrist.com/D3828 --- app/server/lib/FlexServer.ts | 101 +++++++++++++++++++++++++--------- static/locales/en.client.json | 10 ++-- 2 files changed, 80 insertions(+), 31 deletions(-) diff --git a/app/server/lib/FlexServer.ts b/app/server/lib/FlexServer.ts index ccb3d00e..198249b2 100644 --- a/app/server/lib/FlexServer.ts +++ b/app/server/lib/FlexServer.ts @@ -7,7 +7,7 @@ import {getOrgUrlInfo} from 'app/common/gristUrls'; import {UserProfile} from 'app/common/LoginSessionAPI'; import {tbind} from 'app/common/tbind'; import * as version from 'app/common/version'; -import {ApiServer} from 'app/gen-server/ApiServer'; +import {ApiServer, getOrgFromRequest} from 'app/gen-server/ApiServer'; import {Document} from "app/gen-server/entity/Document"; import {Organization} from "app/gen-server/entity/Organization"; import {Workspace} from 'app/gen-server/entity/Workspace'; @@ -20,8 +20,8 @@ import {Usage} from 'app/gen-server/lib/Usage'; import {AccessTokens, IAccessTokens} from 'app/server/lib/AccessTokens'; import {attachAppEndpoint} from 'app/server/lib/AppEndpoint'; import {appSettings} from 'app/server/lib/AppSettings'; -import {addRequestUser, getUser, getUserId, isSingleUserMode, - redirectToLoginUnconditionally} from 'app/server/lib/Authorizer'; +import {addRequestUser, getUser, getUserId, isAnonymousUser, + isSingleUserMode, redirectToLoginUnconditionally} from 'app/server/lib/Authorizer'; import {redirectToLogin, RequestWithLogin, signInStatusMiddleware} from 'app/server/lib/Authorizer'; import {forceSessionChange} from 'app/server/lib/BrowserSession'; import {Comm} from 'app/server/lib/Comm'; @@ -963,32 +963,16 @@ export class FlexServer implements GristServer { // should be factored out of it. this.addComm(); - async function redirectToLoginOrSignup( - this: FlexServer, signUp: boolean|null, req: express.Request, resp: express.Response, - ) { - const mreq = req as RequestWithLogin; - - // This will ensure that express-session will set our cookie if it hasn't already - - // we'll need it when we redirect back. - forceSessionChange(mreq.session); - // Redirect to the requested URL after successful login. - const nextPath = optStringParam(req.query.next); - const nextUrl = new URL(getOrgUrl(req, nextPath)); - 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, nextUrl)); - } - const signinMiddleware = this._loginMiddleware.getLoginOrSignUpMiddleware ? this._loginMiddleware.getLoginOrSignUpMiddleware() : []; - this.app.get('/login', ...signinMiddleware, expressWrap(redirectToLoginOrSignup.bind(this, false))); - this.app.get('/signup', ...signinMiddleware, expressWrap(redirectToLoginOrSignup.bind(this, true))); - this.app.get('/signin', ...signinMiddleware, expressWrap(redirectToLoginOrSignup.bind(this, null))); + this.app.get('/login', ...signinMiddleware, expressWrap(this._redirectToLoginOrSignup.bind(this, { + signUp: false, + }))); + this.app.get('/signup', ...signinMiddleware, expressWrap(this._redirectToLoginOrSignup.bind(this, { + signUp: true, + }))); + this.app.get('/signin', ...signinMiddleware, expressWrap(this._redirectToLoginOrSignup.bind(this, {}))); if (allowTestLogin()) { // This is an endpoint for the dev environment that lets you log in as anyone. @@ -1212,6 +1196,37 @@ export class FlexServer implements GristServer { return this._sendAppPage(req, resp, {path: 'app.html', status: 200, config: {}, googleTagManager: 'anon'}); })); + /** + * A nuanced redirecting endpoint. For example, on docs.getgrist.com it does: + * 1) If logged in and no team site -> https://docs.getgrist.com/ + * 2) If logged in and has team sites -> https://docs.getgrist.com/welcome/teams + * 3) If logged out but has a cookie -> /login, then 1 or 2 + * 4) If entirely unknown -> /signup + */ + this.app.get('/welcome/start', [ + this._redirectToHostMiddleware, + this._userIdMiddleware, + ], expressWrap(async (req, resp, next) => { + if (isAnonymousUser(req)) { + return this._redirectToLoginOrSignup({ + nextUrl: new URL(getOrgUrl(req, '/welcome/start')), + }, req, resp); + } else { + const userId = getUserId(req); + const domain = getOrgFromRequest(req); + const orgs = this._dbManager.unwrapQueryResult( + await this._dbManager.getOrgs(userId, domain, { + ignoreEveryoneShares: true, + }) + ); + if (orgs.length > 1) { + resp.redirect(getOrgUrl(req, '/welcome/teams')); + } else { + resp.redirect(getOrgUrl(req)); + } + } + })); + this.app.post('/welcome/info', ...middleware, expressWrap(async (req, resp, next) => { const userId = getUserId(req); const user = getUser(req); @@ -1726,6 +1741,40 @@ export class FlexServer implements GristServer { } } + /** + * If signUp is true, redirect to signUp. + * If signUp is false, redirect to login. + * If signUp is not set, redirect to signUp if no cookie found, else login. + * + * If nextUrl is not supplied, it will be constructed from a path in + * the "next" query parameter. + */ + private async _redirectToLoginOrSignup( + options: { + signUp?: boolean, nextUrl?: URL, + }, + req: express.Request, resp: express.Response, + ) { + let {nextUrl, signUp} = options; + + const mreq = req as RequestWithLogin; + + // This will ensure that express-session will set our cookie if it hasn't already - + // we'll need it when we redirect back. + forceSessionChange(mreq.session); + // Redirect to the requested URL after successful login. + if (!nextUrl) { + const nextPath = optStringParam(req.query.next); + nextUrl = new URL(getOrgUrl(req, nextPath)); + } + if (signUp === undefined) { + // 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, nextUrl)); + } } /** diff --git a/static/locales/en.client.json b/static/locales/en.client.json index 1c373ed5..1ab342af 100644 --- a/static/locales/en.client.json +++ b/static/locales/en.client.json @@ -2,7 +2,7 @@ "ACUserManager": { "Enter email address": "Enter e-mail address", "Invite new member": "Invite new member", - "We'll email an invite to {{email}}": "An invite will be e-mailed to {{email}}" + "We'll email an invite to {{email}}": "We'll email an invite to {{email}}" }, "AccessRules": { "Add Column Rule": "Add Column Rule", @@ -366,7 +366,7 @@ "Hide {{count}} columns_one": "Hide column", "Hide {{count}} columns_other": "Hide {{count}} columns", "Insert column to the {{to}}": "Insert column to the {{to}}", - "More sort options ...": "More sorting options…", + "More sort options ...": "More sort options…", "Rename column": "Rename column", "Reset {{count}} columns_one": "Reset column", "Reset {{count}} columns_other": "Reset {{count}} columns", @@ -534,7 +534,7 @@ "Select Widget": "Select Widget", "Series_one": "Series", "Series_other": "Series", - "Sort & Filter": "Sort and Filter", + "Sort & Filter": "Sort & Filter", "TRANSFORM": "TRANSFORM", "Theme": "Theme", "WIDGET TITLE": "WIDGET TITLE", @@ -637,7 +637,7 @@ "No Default Access": "No Default Access", "None": "None", "Owner": "Owner", - "View & Edit": "View and Edit", + "View & Edit": "View & Edit", "View Only": "View Only", "Viewer": "Viewer" }, @@ -661,7 +661,7 @@ "Unmark On-Demand": "Unmark On-Demand" }, "ViewLayoutMenu": { - "Advanced Sort & Filter": "Advanced Sorting and Filtering", + "Advanced Sort & Filter": "Advanced Sort & Filter", "Copy anchor link": "Copy anchor link", "Data selection": "Data selection", "Delete record": "Delete record",