(core) Add basic activation page to grist-ee

Summary:
Adds an activation page to grist-ee that currently shows activation status.

Follow-up diffs will introduce additional functionality, such as the ability to
enter activation keys directly from the activation page.

Test Plan: No grist-ee tests (yet).

Reviewers: paulfitz

Reviewed By: paulfitz

Subscribers: dsagal, paulfitz

Differential Revision: https://phab.getgrist.com/D3582
This commit is contained in:
George Gevoian
2022-08-22 12:46:25 -07:00
parent 5f17dd0a06
commit ed37401b2c
23 changed files with 192 additions and 1728 deletions

View File

@@ -4,11 +4,9 @@ import {encodeUrl, getSlugIfNeeded, GristLoadConfig, IGristUrlState, isOrgInPath
parseSubdomain, sanitizePathTail} from 'app/common/gristUrls';
import {getOrgUrlInfo} from 'app/common/gristUrls';
import {UserProfile} from 'app/common/LoginSessionAPI';
import {BillingTask} from 'app/common/BillingAPI';
import {TEAM_FREE_PLAN, TEAM_PLAN} from 'app/common/Features';
import {tbind} from 'app/common/tbind';
import * as version from 'app/common/version';
import {ApiServer, getOrgFromRequest} from 'app/gen-server/ApiServer';
import {ApiServer} 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';
@@ -48,8 +46,7 @@ import {IPermitStore} from 'app/server/lib/Permit';
import {getAppPathTo, getAppRoot, getUnpackedAppRoot} from 'app/server/lib/places';
import {addPluginEndpoints, limitToPlugins} from 'app/server/lib/PluginEndpoint';
import {PluginManager} from 'app/server/lib/PluginManager';
import {
adaptServerUrl, addOrgToPath, addPermit, getOrgUrl, getOriginUrl, getScope, optStringParam,
import {adaptServerUrl, addOrgToPath, getOrgUrl, getOriginUrl, getScope, optStringParam,
RequestWithGristInfo, stringParam, TEST_HTTPS_OFFSET, trustOrigin} from 'app/server/lib/requestUtils';
import {ISendAppPageOptions, makeGristConfig, makeMessagePage, makeSendAppPage} from 'app/server/lib/sendAppPage';
import {getDatabaseUrl, listenPromise} from 'app/server/lib/serverUtils';
@@ -616,7 +613,7 @@ export class FlexServer implements GristServer {
public addBillingApi() {
if (this._check('billing-api', 'homedb', 'json', 'api-mw')) { return; }
this._getBilling();
this._billing.addEndpoints(this.app, this);
this._billing.addEndpoints(this.app);
this._billing.addEventHandlers();
}
@@ -1147,55 +1144,8 @@ export class FlexServer implements GristServer {
this._redirectToLoginWithoutExceptionsMiddleware
];
function getPrefix(req: express.Request) {
const org = getOrgFromRequest(req);
if (!org) {
return getOriginUrl(req);
}
const prefix = isOrgInPathOnly(req.hostname) ? `/o/${org}` : '';
return prefix;
}
// Add billing summary page (.../billing)
this.app.get('/billing', ...middleware, expressWrap(async (req, resp, next) => {
const mreq = req as RequestWithLogin;
const orgDomain = mreq.org;
if (!orgDomain) {
return this._sendAppPage(req, resp, {path: 'error.html', status: 404, config: {errPage: 'not-found'}});
}
// Allow the support user access to billing pages.
const scope = addPermit(getScope(mreq), this._dbManager.getSupportUserId(), {org: orgDomain});
const query = await this._dbManager.getOrg(scope, orgDomain);
const org = this._dbManager.unwrapQueryResult(query);
// This page isn't available for personal site.
if (org.owner) {
return this._sendAppPage(req, resp, {path: 'error.html', status: 404, config: {errPage: 'not-found'}});
}
return this._sendAppPage(req, resp, {path: 'billing.html', status: 200, config: {}});
}));
this.app.get('/billing/payment', ...middleware, expressWrap(async (req, resp, next) => {
const task = (optStringParam(req.query.billingTask) || '') as BillingTask;
if (!BillingTask.guard(task)) {
// If the payment task are invalid, redirect to the summary page.
return resp.redirect(getOriginUrl(req) + `/billing`);
} else {
return this._sendAppPage(req, resp, {path: 'billing.html', status: 200, config: {}});
}
}));
// New landing page for the new NEW_DEAL.
this.app.get('/billing/create-team', ...middleware, expressWrap(async (req, resp, next) => {
const planType = optStringParam(req.query.planType) || '';
// Currently we have hardcoded support only for those two plans.
const supportedPlans = [TEAM_PLAN, TEAM_FREE_PLAN];
if (!supportedPlans.includes(planType)) {
return this._sendAppPage(req, resp, {path: 'error.html', status: 404, config: {errPage: 'not-found'}});
}
// Redirect to home page with url params
const url = `${getPrefix(req)}/?planType=${planType}#create-team`;
return resp.redirect(url);
}));
this._getBilling();
this._billing.addPages(this.app, middleware);
}
/**

View File

@@ -1,9 +1,9 @@
import * as express from 'express';
import {GristServer} from 'app/server/lib/GristServer';
export interface IBilling {
addEndpoints(app: express.Express, server: GristServer): void;
addEndpoints(app: express.Express): void;
addEventHandlers(): void;
addWebhooks(app: express.Express): void;
addMiddleware?(app: express.Express): Promise<void>;
addPages(app: express.Express, middleware: express.RequestHandler[]): void;
}

View File

@@ -7,7 +7,6 @@ import {INotifier} from 'app/server/lib/INotifier';
import {ISandbox, ISandboxCreationOptions} from 'app/server/lib/ISandbox';
import {IShell} from 'app/server/lib/IShell';
import {createSandbox} from 'app/server/lib/NSandbox';
import * as express from 'express';
export interface ICreate {
@@ -48,35 +47,37 @@ export interface ICreateNotifierOptions {
create(dbManager: HomeDBManager, gristConfig: GristServer): INotifier|undefined;
}
export interface ICreateBillingOptions {
create(dbManager: HomeDBManager, gristConfig: GristServer): IBilling|undefined;
}
export function makeSimpleCreator(opts: {
sessionSecret?: string,
storage?: ICreateStorageOptions[],
activationMiddleware?: (db: HomeDBManager, app: express.Express) => Promise<void>,
billing?: ICreateBillingOptions,
notifier?: ICreateNotifierOptions,
}): ICreate {
const {sessionSecret, storage, notifier, billing} = opts;
return {
Billing(db) {
return {
Billing(dbManager, gristConfig) {
return billing?.create(dbManager, gristConfig) ?? {
addEndpoints() { /* do nothing */ },
addEventHandlers() { /* do nothing */ },
addWebhooks() { /* do nothing */ },
async addMiddleware(app) {
// add activation middleware, if needed.
return opts?.activationMiddleware?.(db, app);
}
async addMiddleware() { /* do nothing */ },
addPages() { /* do nothing */ },
};
},
Notifier(dbManager, gristConfig) {
const {notifier} = opts;
return notifier?.create(dbManager, gristConfig) ?? {
get testPending() { return false; },
deleteUser() { throw new Error('deleteUser unavailable'); },
};
},
ExternalStorage(purpose, extraPrefix) {
for (const storage of opts.storage || []) {
if (storage.check()) {
return storage.create(purpose, extraPrefix);
for (const s of storage || []) {
if (s.check()) {
return s.create(purpose, extraPrefix);
}
}
return undefined;
@@ -85,15 +86,15 @@ export function makeSimpleCreator(opts: {
return createSandbox('unsandboxed', options);
},
sessionSecret() {
const secret = process.env.GRIST_SESSION_SECRET || opts.sessionSecret;
const secret = process.env.GRIST_SESSION_SECRET || sessionSecret;
if (!secret) {
throw new Error('need GRIST_SESSION_SECRET');
}
return secret;
},
async configure() {
for (const storage of opts.storage || []) {
if (storage.check()) { break; }
for (const s of storage || []) {
if (s.check()) { break; }
}
},
getExtraHeadHtml() {

View File

@@ -54,7 +54,7 @@ export function makeGristConfig(homeUrl: string|null, extra: Partial<GristLoadCo
enableWidgetRepository: Boolean(process.env.GRIST_WIDGET_LIST_URL),
survey: Boolean(process.env.DOC_ID_NEW_USER_INFO),
tagManagerId: process.env.GOOGLE_TAG_MANAGER_ID,
activation: (mreq as RequestWithLogin|undefined)?.activation,
activation: getActivation(req as RequestWithLogin | undefined),
...extra,
};
}
@@ -184,3 +184,11 @@ function getDocFromConfig(config: GristLoadConfig): Document | null {
return config.getDoc[config.assignmentId] ?? null;
}
function getActivation(mreq: RequestWithLogin|undefined) {
const defaultEmail = process.env.GRIST_DEFAULT_EMAIL;
return {
...mreq?.activation,
isManager: Boolean(defaultEmail && defaultEmail === mreq?.user?.loginEmail),
};
}