Allow URLs with only a docID #768 (#771)

Co-authored-by: Florent FAYOLLE <florent.fayolle@beta.gouv.fr>
pull/783/head
Florent 6 months ago committed by GitHub
parent de13a2fd7a
commit cf0cbb404e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -41,7 +41,7 @@ export type PageType =
| "billing" | "billing"
| "welcome" | "welcome"
| "account" | "account"
| "support-grist" | "support"
| "activation"; | "activation";
const G = getBrowserGlobals('document', 'window'); const G = getBrowserGlobals('document', 'window');
@ -316,7 +316,7 @@ export class AppModelImpl extends Disposable implements AppModel {
} else if (state.account) { } else if (state.account) {
return 'account'; return 'account';
} else if (state.supportGrist) { } else if (state.supportGrist) {
return 'support-grist'; return 'support';
} else if (state.activation) { } else if (state.activation) {
return 'activation'; return 'activation';
} else { } else {

@ -226,7 +226,7 @@ export class AccountWidget extends Disposable {
return menuItemLink( return menuItemLink(
t('Support Grist'), t('Support Grist'),
cssHeartIcon('💛'), cssHeartIcon('💛'),
urlState().setLinkUrl({supportGrist: 'support-grist'}), urlState().setLinkUrl({supportGrist: 'support'}),
testId('usermenu-support-grist'), testId('usermenu-support-grist'),
); );
} }

@ -77,7 +77,7 @@ function createMainPage(appModel: AppModel, appObj: App) {
return dom.create(WelcomePage, appModel); return dom.create(WelcomePage, appModel);
} else if (pageType === 'account') { } else if (pageType === 'account') {
return domAsync(loadAccountPage().then(ap => dom.create(ap.AccountPage, appModel))); return domAsync(loadAccountPage().then(ap => dom.create(ap.AccountPage, appModel)));
} else if (pageType === 'support-grist') { } else if (pageType === 'support') {
return domAsync(loadSupportGristPage().then(sgp => dom.create(sgp.SupportGristPage, appModel))); return domAsync(loadSupportGristPage().then(sgp => dom.create(sgp.SupportGristPage, appModel)));
} else if (pageType === 'activation') { } else if (pageType === 'activation') {
return domAsync(loadActivationPage().then(ap => dom.create(ap.ActivationPage, appModel))); return domAsync(loadActivationPage().then(ap => dom.create(ap.ActivationPage, appModel)));

@ -22,7 +22,7 @@ type ButtonState =
| 'expanded'; | 'expanded';
type CardPage = type CardPage =
| 'support-grist' | 'support'
| 'opted-in'; | 'opted-in';
/** /**
@ -45,7 +45,7 @@ export class SupportGristNudge extends Disposable {
this._buttonState = localStorageObs( this._buttonState = localStorageObs(
`u=${this._appModel.currentValidUser?.id ?? 0};supportGristNudge`, 'expanded' `u=${this._appModel.currentValidUser?.id ?? 0};supportGristNudge`, 'expanded'
) as Observable<ButtonState>; ) as Observable<ButtonState>;
this._currentPage = Observable.create(null, 'support-grist'); this._currentPage = Observable.create(null, 'support');
this._isClosed = Observable.create(this, false); this._isClosed = Observable.create(this, false);
} }
@ -122,7 +122,7 @@ export class SupportGristNudge extends Disposable {
private _buildCard() { private _buildCard() {
return cssCard( return cssCard(
dom.domComputed(this._currentPage, page => { dom.domComputed(this._currentPage, page => {
if (page === 'support-grist') { if (page === 'support') {
return this._buildSupportGristCardContent(); return this._buildSupportGristCardContent();
} else { } else {
return this._buildOptedInCardContent(); return this._buildOptedInCardContent();
@ -205,7 +205,7 @@ function helpCenterLink() {
function supportGristLink() { function supportGristLink() {
return cssLink( return cssLink(
t('Support Grist page'), t('Support Grist page'),
{href: urlState().makeUrl({supportGrist: 'support-grist'}), target: '_blank'}, {href: urlState().makeUrl({supportGrist: 'support'}), target: '_blank'},
); );
} }

@ -194,7 +194,7 @@ export class SupportGristPage extends Disposable {
const suffix = getPageTitleSuffix(getGristConfig()); const suffix = getPageTitleSuffix(getGristConfig());
switch (page) { switch (page) {
case undefined: case undefined:
case 'support-grist': { case 'support': {
return document.title = `Support Grist${suffix}`; return document.title = `Support Grist${suffix}`;
} }
} }

@ -43,7 +43,7 @@ export type ActivationPage = typeof ActivationPage.type;
export const LoginPage = StringUnion('signup', 'login', 'verified', 'forgot-password'); export const LoginPage = StringUnion('signup', 'login', 'verified', 'forgot-password');
export type LoginPage = typeof LoginPage.type; export type LoginPage = typeof LoginPage.type;
export const SupportGristPage = StringUnion('support-grist'); export const SupportGristPage = StringUnion('support');
export type SupportGristPage = typeof SupportGristPage.type; export type SupportGristPage = typeof SupportGristPage.type;
// Overall UI style. "full" is normal, "singlePage" is a single page focused, panels hidden experience. // Overall UI style. "full" is normal, "singlePage" is a single page focused, panels hidden experience.
@ -408,8 +408,8 @@ export function decodeUrl(gristConfig: Partial<GristLoadConfig>, location: Locat
state.activation = ActivationPage.parse(map.get('activation')) || 'activation'; state.activation = ActivationPage.parse(map.get('activation')) || 'activation';
} }
if (map.has('welcome')) { state.welcome = WelcomePage.parse(map.get('welcome')); } if (map.has('welcome')) { state.welcome = WelcomePage.parse(map.get('welcome')); }
if (map.has('support-grist')) { if (map.has('support')) {
state.supportGrist = SupportGristPage.parse(map.get('support-grist')) || 'support-grist'; state.supportGrist = SupportGristPage.parse(map.get('support')) || 'support';
} }
if (sp.has('planType')) { state.params!.planType = sp.get('planType')!; } if (sp.has('planType')) { state.params!.planType = sp.get('planType')!; }
if (sp.has('billingPlan')) { state.params!.billingPlan = sp.get('billingPlan')!; } if (sp.has('billingPlan')) { state.params!.billingPlan = sp.get('billingPlan')!; }

@ -212,6 +212,6 @@ export function attachAppEndpoint(options: AttachOptions): void {
// The * is a wildcard in express 4, rather than a regex symbol. // The * is a wildcard in express 4, rather than a regex symbol.
// See https://expressjs.com/en/guide/routing.html // See https://expressjs.com/en/guide/routing.html
app.get('/doc/:urlId([^/]+):remainder(*)', ...docMiddleware, docHandler); app.get('/doc/:urlId([^/]+):remainder(*)', ...docMiddleware, docHandler);
app.get('/:urlId([^/]{12,})/:slug([^/]+):remainder(*)', app.get('/:urlId([^/]{12,})(/:slug([^/]+):remainder(*))?',
...docMiddleware, docHandler); ...docMiddleware, docHandler);
} }

@ -186,7 +186,7 @@ export class Telemetry implements ITelemetry {
public addPages(app: express.Application, middleware: express.RequestHandler[]) { public addPages(app: express.Application, middleware: express.RequestHandler[]) {
if (this._deploymentType === 'core') { if (this._deploymentType === 'core') {
app.get('/support-grist', ...middleware, expressWrap(async (req, resp) => { app.get('/support', ...middleware, expressWrap(async (req, resp) => {
return this._gristServer.sendAppPage(req, resp, return this._gristServer.sendAppPage(req, resp,
{path: 'app.html', status: 200, config: {}}); {path: 'app.html', status: 200, config: {}});
})); }));

@ -449,7 +449,7 @@ class Seed {
const d = new Document(); const d = new Document();
d.name = doc; d.name = doc;
d.workspace = w; d.workspace = w;
d.id = `sample_${docId}`; d.id = `sampledocid_${docId}`;
docId++; docId++;
await d.save(); await d.save();
const dgrps = await this.createGroups(w); const dgrps = await this.createGroups(w);

@ -101,40 +101,46 @@ describe('Authorizer', function() {
it.skip("viewer gets redirect by title", async function() { it.skip("viewer gets redirect by title", async function() {
const resp = await axios.get(`${serverUrl}/o/pr/doc/Bananas`, chimpy); const resp = await axios.get(`${serverUrl}/o/pr/doc/Bananas`, chimpy);
assert.equal(resp.status, 200); assert.equal(resp.status, 200);
assert.equal(getGristConfig(resp.data).assignmentId, 'sample_6'); assert.equal(getGristConfig(resp.data).assignmentId, 'sampledocid_6');
assert.match(resp.request.res.responseUrl, /\/doc\/sample_6$/); assert.match(resp.request.res.responseUrl, /\/doc\/sampledocid_6$/);
const resp2 = await axios.get(`${serverUrl}/o/nasa/doc/Pluto`, chimpy); const resp2 = await axios.get(`${serverUrl}/o/nasa/doc/Pluto`, chimpy);
assert.equal(resp2.status, 200); assert.equal(resp2.status, 200);
assert.equal(getGristConfig(resp2.data).assignmentId, 'sample_2'); assert.equal(getGristConfig(resp2.data).assignmentId, 'sampledocid_2');
assert.match(resp2.request.res.responseUrl, /\/doc\/sample_2$/); assert.match(resp2.request.res.responseUrl, /\/doc\/sampledocid_2$/);
});
it('viewer loads document without slug in the URL', async function () {
const docId = docs.Bananas.id;
const resp = await axios.get(`${serverUrl}/o/pr/${docId}`, chimpy);
assert.equal(resp.status, 200);
}); });
it("stranger gets consistent refusal regardless of title", async function() { it("stranger gets consistent refusal regardless of title", async function() {
const resp = await axios.get(`${serverUrl}/o/pr/doc/Bananas`, charon); const resp = await axios.get(`${serverUrl}/o/pr/doc/Bananas`, charon);
assert.equal(resp.status, 404); assert.equal(resp.status, 404);
assert.notMatch(resp.data, /sample_6/); assert.notMatch(resp.data, /sampledocid_6/);
const resp2 = await axios.get(`${serverUrl}/o/pr/doc/Bananas2`, charon); const resp2 = await axios.get(`${serverUrl}/o/pr/doc/Bananas2`, charon);
assert.equal(resp2.status, 404); assert.equal(resp2.status, 404);
assert.notMatch(resp.data, /sample_6/); assert.notMatch(resp.data, /sampledocid_6/);
assert.deepEqual(withoutTimestamp(resp.data), assert.deepEqual(withoutTimestamp(resp.data),
withoutTimestamp(resp2.data)); withoutTimestamp(resp2.data));
}); });
it("viewer can access title", async function() { it("viewer can access title", async function() {
const resp = await axios.get(`${serverUrl}/o/pr/doc/sample_6`, chimpy); const resp = await axios.get(`${serverUrl}/o/pr/doc/sampledocid_6`, chimpy);
assert.equal(resp.status, 200); assert.equal(resp.status, 200);
const config = getGristConfig(resp.data); const config = getGristConfig(resp.data);
assert.equal(config.getDoc![config.assignmentId!].name, 'Bananas'); assert.equal(config.getDoc![config.assignmentId!].name, 'Bananas');
}); });
it("stranger cannot access title", async function() { it("stranger cannot access title", async function() {
const resp = await axios.get(`${serverUrl}/o/pr/doc/sample_6`, charon); const resp = await axios.get(`${serverUrl}/o/pr/doc/sampledocid_6`, charon);
assert.equal(resp.status, 403); assert.equal(resp.status, 403);
assert.notMatch(resp.data, /Bananas/); assert.notMatch(resp.data, /Bananas/);
}); });
it("viewer cannot access document from wrong org", async function() { it("viewer cannot access document from wrong org", async function() {
const resp = await axios.get(`${serverUrl}/o/nasa/doc/sample_6`, chimpy); const resp = await axios.get(`${serverUrl}/o/nasa/doc/sampledocid_6`, chimpy);
assert.equal(resp.status, 404); assert.equal(resp.status, 404);
}); });
@ -142,7 +148,7 @@ describe('Authorizer', function() {
const cli = await openClient(server, 'chimpy@getgrist.com', 'pr'); const cli = await openClient(server, 'chimpy@getgrist.com', 'pr');
cli.ignoreTrivialActions(); cli.ignoreTrivialActions();
assert.equal((await cli.readMessage()).type, 'clientConnect'); assert.equal((await cli.readMessage()).type, 'clientConnect');
const openDoc = await cli.send("openDoc", "sample_6"); const openDoc = await cli.send("openDoc", "sampledocid_6");
assert.equal(openDoc.error, undefined); assert.equal(openDoc.error, undefined);
assert.match(JSON.stringify(openDoc.data), /Table1/); assert.match(JSON.stringify(openDoc.data), /Table1/);
await cli.close(); await cli.close();
@ -152,7 +158,7 @@ describe('Authorizer', function() {
const cli = await openClient(server, 'charon@getgrist.com', 'pr'); const cli = await openClient(server, 'charon@getgrist.com', 'pr');
cli.ignoreTrivialActions(); cli.ignoreTrivialActions();
assert.equal((await cli.readMessage()).type, 'clientConnect'); assert.equal((await cli.readMessage()).type, 'clientConnect');
const openDoc = await cli.send("openDoc", "sample_6"); const openDoc = await cli.send("openDoc", "sampledocid_6");
assert.match(openDoc.error!, /No view access/); assert.match(openDoc.error!, /No view access/);
assert.equal(openDoc.data, undefined); assert.equal(openDoc.data, undefined);
assert.match(openDoc.errorCode!, /AUTH_NO_VIEW/); assert.match(openDoc.errorCode!, /AUTH_NO_VIEW/);
@ -163,7 +169,7 @@ describe('Authorizer', function() {
const cli = await openClient(server, 'charon@getgrist.com', 'nasa'); const cli = await openClient(server, 'charon@getgrist.com', 'nasa');
cli.ignoreTrivialActions(); cli.ignoreTrivialActions();
assert.equal((await cli.readMessage()).type, 'clientConnect'); assert.equal((await cli.readMessage()).type, 'clientConnect');
const openDoc = await cli.openDocOnConnect("sample_2"); const openDoc = await cli.openDocOnConnect("sampledocid_2");
assert.equal(openDoc.error, undefined); assert.equal(openDoc.error, undefined);
const nonce = uuidv4(); const nonce = uuidv4();
const applyUserActions = await cli.send("applyUserActions", const applyUserActions = await cli.send("applyUserActions",
@ -182,7 +188,7 @@ describe('Authorizer', function() {
const cli = await openClient(server, 'chimpy@getgrist.com', 'nasa'); const cli = await openClient(server, 'chimpy@getgrist.com', 'nasa');
cli.ignoreTrivialActions(); cli.ignoreTrivialActions();
assert.equal((await cli.readMessage()).type, 'clientConnect'); assert.equal((await cli.readMessage()).type, 'clientConnect');
const openDoc = await cli.openDocOnConnect("sample_2"); const openDoc = await cli.openDocOnConnect("sampledocid_2");
assert.equal(openDoc.error, undefined); assert.equal(openDoc.error, undefined);
const nonce = uuidv4(); const nonce = uuidv4();
const applyUserActions = await cli.send("applyUserActions", const applyUserActions = await cli.send("applyUserActions",
@ -209,9 +215,9 @@ describe('Authorizer', function() {
editor.ignoreTrivialActions(); editor.ignoreTrivialActions();
viewer.ignoreTrivialActions(); viewer.ignoreTrivialActions();
stranger.ignoreTrivialActions(); stranger.ignoreTrivialActions();
assert.equal((await editor.send("openDoc", "sample_2")).error, undefined); assert.equal((await editor.send("openDoc", "sampledocid_2")).error, undefined);
assert.equal((await viewer.send("openDoc", "sample_2")).error, undefined); assert.equal((await viewer.send("openDoc", "sampledocid_2")).error, undefined);
assert.match((await stranger.send("openDoc", "sample_2")).error!, /No view access/); assert.match((await stranger.send("openDoc", "sampledocid_2")).error!, /No view access/);
const action = [0, [["UpdateRecord", "Table1", 1, {A: "foo"}]]]; const action = [0, [["UpdateRecord", "Table1", 1, {A: "foo"}]]];
assert.equal((await editor.send("applyUserActions", ...action)).error, undefined); assert.equal((await editor.send("applyUserActions", ...action)).error, undefined);
@ -224,7 +230,7 @@ describe('Authorizer', function() {
const cli = await openClient(server, 'thumbnail@getgrist.com', 'nasa'); const cli = await openClient(server, 'thumbnail@getgrist.com', 'nasa');
cli.ignoreTrivialActions(); cli.ignoreTrivialActions();
assert.equal((await cli.readMessage()).type, 'clientConnect'); assert.equal((await cli.readMessage()).type, 'clientConnect');
const openDoc = await cli.send("openDoc", "sample_2"); const openDoc = await cli.send("openDoc", "sampledocid_2");
assert.equal(openDoc.error, undefined); assert.equal(openDoc.error, undefined);
const nonce = uuidv4(); const nonce = uuidv4();
const applyUserActions = await cli.send("applyUserActions", const applyUserActions = await cli.send("applyUserActions",
@ -243,12 +249,12 @@ describe('Authorizer', function() {
const cli = await openClient(server, 'charon@getgrist.com', 'nasa'); const cli = await openClient(server, 'charon@getgrist.com', 'nasa');
cli.ignoreTrivialActions(); cli.ignoreTrivialActions();
assert.equal((await cli.readMessage()).type, 'clientConnect'); assert.equal((await cli.readMessage()).type, 'clientConnect');
const openDoc = await cli.send("openDoc", "sample_2"); const openDoc = await cli.send("openDoc", "sampledocid_2");
assert.equal(openDoc.error, undefined); assert.equal(openDoc.error, undefined);
const result = await cli.send("fork", 0); const result = await cli.send("fork", 0);
assert.equal(result.data.docId, result.data.urlId); assert.equal(result.data.docId, result.data.urlId);
const parts = parseUrlId(result.data.docId); const parts = parseUrlId(result.data.docId);
assert.equal(parts.trunkId, "sample_2"); assert.equal(parts.trunkId, "sampledocid_2");
assert.isAbove(parts.forkId!.length, 4); assert.isAbove(parts.forkId!.length, 4);
assert.equal(parts.forkUserId, await dbManager.testGetId('Charon') as number); assert.equal(parts.forkUserId, await dbManager.testGetId('Charon') as number);
}); });
@ -258,31 +264,31 @@ describe('Authorizer', function() {
const cli = await openClient(server, 'anon@getgrist.com', 'nasa'); const cli = await openClient(server, 'anon@getgrist.com', 'nasa');
cli.ignoreTrivialActions(); cli.ignoreTrivialActions();
assert.equal((await cli.readMessage()).type, 'clientConnect'); assert.equal((await cli.readMessage()).type, 'clientConnect');
let openDoc = await cli.send("openDoc", "sample_2"); let openDoc = await cli.send("openDoc", "sampledocid_2");
assert.match(openDoc.error!, /No view access/); assert.match(openDoc.error!, /No view access/);
// grant anon access to doc and retry // grant anon access to doc and retry
await dbManager.updateDocPermissions({ await dbManager.updateDocPermissions({
userId: await dbManager.testGetId('Chimpy') as number, userId: await dbManager.testGetId('Chimpy') as number,
urlId: 'sample_2', urlId: 'sampledocid_2',
org: 'nasa' org: 'nasa'
}, {users: {"anon@getgrist.com": "viewers"}}); }, {users: {"anon@getgrist.com": "viewers"}});
dbManager.flushDocAuthCache(); dbManager.flushDocAuthCache();
openDoc = await cli.send("openDoc", "sample_2"); openDoc = await cli.send("openDoc", "sampledocid_2");
assert.equal(openDoc.error, undefined); assert.equal(openDoc.error, undefined);
// make a fork // make a fork
const result = await cli.send("fork", 0); const result = await cli.send("fork", 0);
assert.equal(result.data.docId, result.data.urlId); assert.equal(result.data.docId, result.data.urlId);
const parts = parseUrlId(result.data.docId); const parts = parseUrlId(result.data.docId);
assert.equal(parts.trunkId, "sample_2"); assert.equal(parts.trunkId, "sampledocid_2");
assert.isAbove(parts.forkId!.length, 4); assert.isAbove(parts.forkId!.length, 4);
assert.equal(parts.forkUserId, undefined); assert.equal(parts.forkUserId, undefined);
}); });
it("can set user via GRIST_PROXY_AUTH_HEADER", async function() { it("can set user via GRIST_PROXY_AUTH_HEADER", async function() {
// User can access a doc by setting header. // User can access a doc by setting header.
const docUrl = `${serverUrl}/o/pr/api/docs/sample_6`; const docUrl = `${serverUrl}/o/pr/api/docs/sampledocid_6`;
const resp = await axios.get(docUrl, { const resp = await axios.get(docUrl, {
headers: {'X-email': 'chimpy@getgrist.com'} headers: {'X-email': 'chimpy@getgrist.com'}
}); });
@ -297,7 +303,7 @@ describe('Authorizer', function() {
let cli = await openClient(server, 'chimpy@getgrist.com', 'pr', 'X-email'); let cli = await openClient(server, 'chimpy@getgrist.com', 'pr', 'X-email');
cli.ignoreTrivialActions(); cli.ignoreTrivialActions();
assert.equal((await cli.readMessage()).type, 'clientConnect'); assert.equal((await cli.readMessage()).type, 'clientConnect');
let openDoc = await cli.send("openDoc", "sample_6"); let openDoc = await cli.send("openDoc", "sampledocid_6");
assert.equal(openDoc.error, undefined); assert.equal(openDoc.error, undefined);
assert.match(JSON.stringify(openDoc.data), /Table1/); assert.match(JSON.stringify(openDoc.data), /Table1/);
await cli.close(); await cli.close();
@ -306,7 +312,7 @@ describe('Authorizer', function() {
cli = await openClient(server, 'notchimpy@getgrist.com', 'pr', 'X-email'); cli = await openClient(server, 'notchimpy@getgrist.com', 'pr', 'X-email');
cli.ignoreTrivialActions(); cli.ignoreTrivialActions();
assert.equal((await cli.readMessage()).type, 'clientConnect'); assert.equal((await cli.readMessage()).type, 'clientConnect');
openDoc = await cli.send("openDoc", "sample_6"); openDoc = await cli.send("openDoc", "sampledocid_6");
assert.match(openDoc.error!, /No view access/); assert.match(openDoc.error!, /No view access/);
assert.equal(openDoc.data, undefined); assert.equal(openDoc.data, undefined);
assert.match(openDoc.errorCode!, /AUTH_NO_VIEW/); assert.match(openDoc.errorCode!, /AUTH_NO_VIEW/);

@ -49,10 +49,10 @@ const support = configForUser('support');
// some doc ids // some doc ids
const docIds: { [name: string]: string } = { const docIds: { [name: string]: string } = {
ApiDataRecordsTest: 'sample_7', ApiDataRecordsTest: 'sampledocid_7',
Timesheets: 'sample_13', Timesheets: 'sampledocid_13',
Bananas: 'sample_6', Bananas: 'sampledocid_6',
Antartic: 'sample_11' Antartic: 'sampledocid_11'
}; };
// A testDir of the form grist_test_{USER}_{SERVER_NAME} // A testDir of the form grist_test_{USER}_{SERVER_NAME}

@ -21,10 +21,10 @@ const chimpy = configForUser('Chimpy');
// some doc ids // some doc ids
const docIds: { [name: string]: string } = { const docIds: { [name: string]: string } = {
ApiDataRecordsTest: 'sample_7', ApiDataRecordsTest: 'sampledocid_7',
Timesheets: 'sample_13', Timesheets: 'sampledocid_13',
Bananas: 'sample_6', Bananas: 'sampledocid_6',
Antartic: 'sample_11' Antartic: 'sampledocid_11',
}; };
let dataDir: string; let dataDir: string;

Loading…
Cancel
Save