2020-07-21 13:20:51 +00:00
|
|
|
/**
|
|
|
|
* AppServer serves up the main app.html file to the browser. It is the first point of contact of
|
|
|
|
* a browser with Grist. It handles sessions, redirect-to-login, and serving up a suitable version
|
|
|
|
* of the client-side code.
|
|
|
|
*/
|
|
|
|
import * as express from 'express';
|
|
|
|
|
|
|
|
import {ApiError} from 'app/common/ApiError';
|
(core) add initial support for special shares
Summary:
This gives a mechanism for controlling access control within a document that is distinct from (though implemented with the same machinery as) granular access rules.
It was hard to find a good way to insert this that didn't dissolve in a soup of complications, so here's what I went with:
* When reading rules, if there are shares, extra rules are added.
* If there are shares, all rules are made conditional on a "ShareRef" user property.
* "ShareRef" is null when a doc is accessed in normal way, and the row id of a share when accessed via a share.
There's no UI for controlling shares (George is working on it for forms), but you can do it by editing a `_grist_Shares` table in a document. Suppose you make a fresh document with a single page/table/widget, then to create an empty share you can do:
```
gristDocPageModel.gristDoc.get().docData.sendAction(['AddRecord', '_grist_Shares', null, {linkId: 'xyz', options: '{"publish": true}'}])
```
If you look at the home db now there should be something in the `shares` table:
```
$ sqlite3 -table landing.db "select * from shares"
+----+------------------------+------------------------+--------------+---------+
| id | key | doc_id | link_id | options |
+----+------------------------+------------------------+--------------+---------+
| 1 | gSL4g38PsyautLHnjmXh2K | 4qYuace1xP2CTcPunFdtan | xyz | ... |
+----+------------------------+------------------------+--------------+---------+
```
If you take the key from that (gSL4g38PsyautLHnjmXh2K in this case) and replace the document's urlId in its URL with `s.<key>` (in this case `s.gSL4g38PsyautLHnjmXh2K` then you can use the regular document landing page (it will be quite blank initially) or API endpoint via the share.
E.g. for me `http://localhost:8080/o/docs/s0gSL4g38PsyautLHnjmXh2K/share-inter-3` accesses the doc.
To actually share some material - useful commands:
```
gristDocPageModel.gristDoc.get().docData.getMetaTable('_grist_Views_section').getRecords()
gristDocPageModel.gristDoc.get().docData.sendAction(['UpdateRecord', '_grist_Views_section', 1, {shareOptions: '{"publish": true, "form": true}'}])
gristDocPageModel.gristDoc.get().docData.getMetaTable('_grist_Pages').getRecords()
gristDocPageModel.gristDoc.get().docData.sendAction(['UpdateRecord', '_grist_Pages', 1, {shareRef: 1}])
```
For a share to be effective, at least one page needs to have its shareRef set to the rowId of the share, and at least one widget on one of those pages needs to have its shareOptions set to {"publish": "true", "form": "true"} (meaning turn on sharing, and include form sharing), and the share itself needs {"publish": true} on its options.
I think special shares are kind of incompatible with public sharing, since by their nature (allowing access to all endpoints) they easily expose the docId, and changing that would be hard.
Test Plan: tests added
Reviewers: dsagal, georgegevoian
Reviewed By: dsagal, georgegevoian
Subscribers: jarek, dsagal
Differential Revision: https://phab.getgrist.com/D4144
2024-01-03 16:53:20 +00:00
|
|
|
import {getSlugIfNeeded, parseUrlId, SHARE_KEY_PREFIX} from 'app/common/gristUrls';
|
2021-08-05 15:12:46 +00:00
|
|
|
import {LocalPlugin} from "app/common/plugin";
|
2023-06-06 17:08:50 +00:00
|
|
|
import {TELEMETRY_TEMPLATE_SIGNUP_COOKIE_NAME} from 'app/common/Telemetry';
|
2024-05-14 16:58:41 +00:00
|
|
|
import {Document as APIDocument, PublicDocWorkerUrlInfo} from 'app/common/UserAPI';
|
2020-07-21 13:20:51 +00:00
|
|
|
import {Document} from "app/gen-server/entity/Document";
|
|
|
|
import {HomeDBManager} from 'app/gen-server/lib/HomeDBManager';
|
2020-08-19 20:25:42 +00:00
|
|
|
import {assertAccess, getTransitiveHeaders, getUserId, isAnonymousUser,
|
|
|
|
RequestWithLogin} from 'app/server/lib/Authorizer';
|
2020-07-21 13:20:51 +00:00
|
|
|
import {DocStatus, IDocWorkerMap} from 'app/server/lib/DocWorkerMap';
|
2024-05-14 16:58:41 +00:00
|
|
|
import {
|
|
|
|
customizeDocWorkerUrl, getDocWorkerInfoOrSelfPrefix, getWorker, useWorkerPool
|
|
|
|
} from 'app/server/lib/DocWorkerUtils';
|
2020-07-21 13:20:51 +00:00
|
|
|
import {expressWrap} from 'app/server/lib/expressWrap';
|
(core) make Grist easier to run with a single server
Summary:
This makes many small changes so that Grist is less fussy to run as a single instance behind a reverse proxy. Some users had difficulty with the self-connections Grist would make, due to internal network setup, and since these are unnecessary in any case in this scenario, they are now optimized away. Likewise some users had difficulties related to doc worker urls, which are now also optimized away. With these changes, users should be able to get a lot further on first try, at least far enough to open and edit documents.
The `GRIST_SINGLE_ORG` setting was proving a bit confusing, since it appeared to only work when set to `docs`. This diff
adds a check for whether the specified org exists, and if not, it creates it. This still depends on having a user email to make as the owner of the team, so there could be remaining difficulties there.
Test Plan: tested manually with nginx
Reviewers: jarek
Reviewed By: jarek
Differential Revision: https://phab.getgrist.com/D3299
2022-03-02 19:07:26 +00:00
|
|
|
import {DocTemplate, GristServer} from 'app/server/lib/GristServer';
|
2023-04-06 15:10:29 +00:00
|
|
|
import {getCookieDomain} from 'app/server/lib/gristSessions';
|
2022-07-04 14:14:55 +00:00
|
|
|
import log from 'app/server/lib/log';
|
2023-09-06 18:35:46 +00:00
|
|
|
import {addOrgToPathIfNeeded, pruneAPIResult, trustOrigin} from 'app/server/lib/requestUtils';
|
2023-07-31 20:10:59 +00:00
|
|
|
import {ISendAppPageOptions} from 'app/server/lib/sendAppPage';
|
2020-07-21 13:20:51 +00:00
|
|
|
|
|
|
|
export interface AttachOptions {
|
2024-02-01 16:47:53 +00:00
|
|
|
app: express.Application; // Express app to which to add endpoints
|
|
|
|
middleware: express.RequestHandler[]; // Middleware to apply for all endpoints except docs and forms
|
|
|
|
docMiddleware: express.RequestHandler[]; // Middleware to apply for doc landing pages
|
|
|
|
formMiddleware: express.RequestHandler[]; // Middleware to apply for form landing pages
|
|
|
|
forceLogin: express.RequestHandler|null; // Method to force user to login (if logins are possible)
|
2020-07-21 13:20:51 +00:00
|
|
|
docWorkerMap: IDocWorkerMap|null;
|
|
|
|
sendAppPage: (req: express.Request, resp: express.Response, options: ISendAppPageOptions) => Promise<void>;
|
|
|
|
dbManager: HomeDBManager;
|
2021-08-05 15:12:46 +00:00
|
|
|
plugins: LocalPlugin[];
|
(core) make Grist easier to run with a single server
Summary:
This makes many small changes so that Grist is less fussy to run as a single instance behind a reverse proxy. Some users had difficulty with the self-connections Grist would make, due to internal network setup, and since these are unnecessary in any case in this scenario, they are now optimized away. Likewise some users had difficulties related to doc worker urls, which are now also optimized away. With these changes, users should be able to get a lot further on first try, at least far enough to open and edit documents.
The `GRIST_SINGLE_ORG` setting was proving a bit confusing, since it appeared to only work when set to `docs`. This diff
adds a check for whether the specified org exists, and if not, it creates it. This still depends on having a user email to make as the owner of the team, so there could be remaining difficulties there.
Test Plan: tested manually with nginx
Reviewers: jarek
Reviewed By: jarek
Differential Revision: https://phab.getgrist.com/D3299
2022-03-02 19:07:26 +00:00
|
|
|
gristServer: GristServer;
|
2020-07-21 13:20:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export function attachAppEndpoint(options: AttachOptions): void {
|
2024-02-01 16:47:53 +00:00
|
|
|
const {app, middleware, docMiddleware, formMiddleware, docWorkerMap,
|
|
|
|
forceLogin, sendAppPage, dbManager, plugins, gristServer} = options;
|
2020-07-21 13:20:51 +00:00
|
|
|
// Per-workspace URLs open the same old Home page, and it's up to the client to notice and
|
|
|
|
// render the right workspace.
|
|
|
|
app.get(['/', '/ws/:wsId', '/p/:page'], ...middleware, expressWrap(async (req, res) =>
|
2021-08-05 15:12:46 +00:00
|
|
|
sendAppPage(req, res, {path: 'app.html', status: 200, config: {plugins}, googleTagManager: 'anon'})));
|
2020-07-21 13:20:51 +00:00
|
|
|
|
2023-12-27 12:19:56 +00:00
|
|
|
app.get('/apiconsole', expressWrap(async (req, res) =>
|
|
|
|
sendAppPage(req, res, {path: 'apiconsole.html', status: 200, config: {}})));
|
|
|
|
|
2024-05-14 16:58:41 +00:00
|
|
|
app.get('/api/worker/:docId([^/]+)/?*', expressWrap(async (req, res) => {
|
2020-07-21 13:20:51 +00:00
|
|
|
if (!trustOrigin(req, res)) { throw new Error('Unrecognized origin'); }
|
|
|
|
res.header("Access-Control-Allow-Credentials", "true");
|
|
|
|
|
2024-05-14 16:58:41 +00:00
|
|
|
const {selfPrefix, docWorker} = await getDocWorkerInfoOrSelfPrefix(
|
|
|
|
req.params.docId, docWorkerMap, gristServer.getTag()
|
|
|
|
);
|
|
|
|
const info: PublicDocWorkerUrlInfo = selfPrefix ?
|
|
|
|
{ docWorkerUrl: null, selfPrefix } :
|
|
|
|
{ docWorkerUrl: customizeDocWorkerUrl(docWorker!.publicUrl, req), selfPrefix: null };
|
|
|
|
|
|
|
|
return res.json(info);
|
2020-07-21 13:20:51 +00:00
|
|
|
}));
|
|
|
|
|
|
|
|
// Handler for serving the document landing pages. Expects the following parameters:
|
|
|
|
// urlId, slug (optional), remainder
|
|
|
|
// This handler is used for both "doc/urlId" and "urlId/slug" style endpoints.
|
|
|
|
const docHandler = expressWrap(async (req, res, next) => {
|
|
|
|
if (req.params.slug && req.params.slug === 'app.html') {
|
|
|
|
// This can happen on a single-port configuration, since "docId/app.html" matches
|
|
|
|
// the "urlId/slug" pattern. Luckily the "." character is not allowed in slugs.
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
if (!docWorkerMap) {
|
2021-08-05 15:12:46 +00:00
|
|
|
return await sendAppPage(req, res, {path: 'app.html', status: 200, config: {plugins},
|
2020-07-21 13:20:51 +00:00
|
|
|
googleTagManager: 'anon'});
|
|
|
|
}
|
|
|
|
const mreq = req as RequestWithLogin;
|
|
|
|
const urlId = req.params.urlId;
|
|
|
|
let doc: Document|null = null;
|
|
|
|
try {
|
|
|
|
const userId = getUserId(mreq);
|
|
|
|
|
|
|
|
// Query DB for the doc metadata, to include in the page (as a pre-fetch of getDoc() call),
|
|
|
|
// and to get fresh (uncached) access info.
|
|
|
|
doc = await dbManager.getDoc({userId, org: mreq.org, urlId});
|
2023-03-22 13:48:50 +00:00
|
|
|
if (isAnonymousUser(mreq) && doc.type === 'tutorial') {
|
|
|
|
// Tutorials require users to be signed in.
|
|
|
|
throw new ApiError('You must be signed in to access a tutorial.', 403);
|
|
|
|
}
|
2020-07-21 13:20:51 +00:00
|
|
|
|
2023-03-22 13:48:50 +00:00
|
|
|
const slug = getSlugIfNeeded(doc);
|
2020-07-21 13:20:51 +00:00
|
|
|
const slugMismatch = (req.params.slug || null) !== (slug || null);
|
|
|
|
const preferredUrlId = doc.urlId || doc.id;
|
(core) add initial support for special shares
Summary:
This gives a mechanism for controlling access control within a document that is distinct from (though implemented with the same machinery as) granular access rules.
It was hard to find a good way to insert this that didn't dissolve in a soup of complications, so here's what I went with:
* When reading rules, if there are shares, extra rules are added.
* If there are shares, all rules are made conditional on a "ShareRef" user property.
* "ShareRef" is null when a doc is accessed in normal way, and the row id of a share when accessed via a share.
There's no UI for controlling shares (George is working on it for forms), but you can do it by editing a `_grist_Shares` table in a document. Suppose you make a fresh document with a single page/table/widget, then to create an empty share you can do:
```
gristDocPageModel.gristDoc.get().docData.sendAction(['AddRecord', '_grist_Shares', null, {linkId: 'xyz', options: '{"publish": true}'}])
```
If you look at the home db now there should be something in the `shares` table:
```
$ sqlite3 -table landing.db "select * from shares"
+----+------------------------+------------------------+--------------+---------+
| id | key | doc_id | link_id | options |
+----+------------------------+------------------------+--------------+---------+
| 1 | gSL4g38PsyautLHnjmXh2K | 4qYuace1xP2CTcPunFdtan | xyz | ... |
+----+------------------------+------------------------+--------------+---------+
```
If you take the key from that (gSL4g38PsyautLHnjmXh2K in this case) and replace the document's urlId in its URL with `s.<key>` (in this case `s.gSL4g38PsyautLHnjmXh2K` then you can use the regular document landing page (it will be quite blank initially) or API endpoint via the share.
E.g. for me `http://localhost:8080/o/docs/s0gSL4g38PsyautLHnjmXh2K/share-inter-3` accesses the doc.
To actually share some material - useful commands:
```
gristDocPageModel.gristDoc.get().docData.getMetaTable('_grist_Views_section').getRecords()
gristDocPageModel.gristDoc.get().docData.sendAction(['UpdateRecord', '_grist_Views_section', 1, {shareOptions: '{"publish": true, "form": true}'}])
gristDocPageModel.gristDoc.get().docData.getMetaTable('_grist_Pages').getRecords()
gristDocPageModel.gristDoc.get().docData.sendAction(['UpdateRecord', '_grist_Pages', 1, {shareRef: 1}])
```
For a share to be effective, at least one page needs to have its shareRef set to the rowId of the share, and at least one widget on one of those pages needs to have its shareOptions set to {"publish": "true", "form": "true"} (meaning turn on sharing, and include form sharing), and the share itself needs {"publish": true} on its options.
I think special shares are kind of incompatible with public sharing, since by their nature (allowing access to all endpoints) they easily expose the docId, and changing that would be hard.
Test Plan: tests added
Reviewers: dsagal, georgegevoian
Reviewed By: dsagal, georgegevoian
Subscribers: jarek, dsagal
Differential Revision: https://phab.getgrist.com/D4144
2024-01-03 16:53:20 +00:00
|
|
|
if (!req.params.viaShare && // Don't bother canonicalizing for shares yet.
|
|
|
|
(urlId !== preferredUrlId || slugMismatch)) {
|
2020-07-21 13:20:51 +00:00
|
|
|
// Prepare to redirect to canonical url for document.
|
|
|
|
// Preserve any query parameters or fragments.
|
|
|
|
const queryOrFragmentCheck = req.originalUrl.match(/([#?].*)/);
|
|
|
|
const queryOrFragment = (queryOrFragmentCheck && queryOrFragmentCheck[1]) || '';
|
2020-09-11 20:27:09 +00:00
|
|
|
const target = slug ?
|
|
|
|
`/${preferredUrlId}/${slug}${req.params.remainder}${queryOrFragment}` :
|
|
|
|
`/doc/${preferredUrlId}${req.params.remainder}${queryOrFragment}`;
|
|
|
|
res.redirect(addOrgToPathIfNeeded(req, target));
|
2020-07-21 13:20:51 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// The docAuth value will be cached from the getDoc() above (or could be derived from doc).
|
|
|
|
const docAuth = await dbManager.getDocAuthCached({userId, org: mreq.org, urlId});
|
|
|
|
assertAccess('viewers', docAuth);
|
|
|
|
|
|
|
|
} catch (err) {
|
|
|
|
if (err.status === 404) {
|
|
|
|
log.info("/:urlId/app.html did not find doc", mreq.userId, urlId, doc && doc.access, mreq.org);
|
|
|
|
throw new ApiError('Document not found.', 404);
|
|
|
|
} else if (err.status === 403) {
|
|
|
|
log.info("/:urlId/app.html denied access", mreq.userId, urlId, doc && doc.access, mreq.org);
|
2020-08-19 20:25:42 +00:00
|
|
|
// If the user does not have access to the document, and is anonymous, and we
|
|
|
|
// have a login system, we may wish to redirect them to login process.
|
|
|
|
if (isAnonymousUser(mreq) && forceLogin) {
|
|
|
|
// First check if anonymous user has access to this org. If so, we don't propose
|
|
|
|
// that they log in. This is the same check made in redirectToLogin() middleware.
|
|
|
|
const result = await dbManager.getOrg({userId: getUserId(mreq)}, mreq.org || null);
|
2023-03-22 13:48:50 +00:00
|
|
|
if (result.status !== 200 || doc?.type === 'tutorial') {
|
|
|
|
// Anonymous user does not have any access to this org, doc, or tutorial.
|
2020-08-19 20:25:42 +00:00
|
|
|
// Redirect to log in.
|
|
|
|
return forceLogin(req, res, next);
|
|
|
|
}
|
|
|
|
}
|
2020-07-21 13:20:51 +00:00
|
|
|
throw new ApiError('You do not have access to this document.', 403);
|
|
|
|
}
|
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
|
(core) make Grist easier to run with a single server
Summary:
This makes many small changes so that Grist is less fussy to run as a single instance behind a reverse proxy. Some users had difficulty with the self-connections Grist would make, due to internal network setup, and since these are unnecessary in any case in this scenario, they are now optimized away. Likewise some users had difficulties related to doc worker urls, which are now also optimized away. With these changes, users should be able to get a lot further on first try, at least far enough to open and edit documents.
The `GRIST_SINGLE_ORG` setting was proving a bit confusing, since it appeared to only work when set to `docs`. This diff
adds a check for whether the specified org exists, and if not, it creates it. This still depends on having a user email to make as the owner of the team, so there could be remaining difficulties there.
Test Plan: tested manually with nginx
Reviewers: jarek
Reviewed By: jarek
Differential Revision: https://phab.getgrist.com/D3299
2022-03-02 19:07:26 +00:00
|
|
|
let body: DocTemplate;
|
|
|
|
let docStatus: DocStatus|undefined;
|
2020-07-21 13:20:51 +00:00
|
|
|
const docId = doc.id;
|
(core) make Grist easier to run with a single server
Summary:
This makes many small changes so that Grist is less fussy to run as a single instance behind a reverse proxy. Some users had difficulty with the self-connections Grist would make, due to internal network setup, and since these are unnecessary in any case in this scenario, they are now optimized away. Likewise some users had difficulties related to doc worker urls, which are now also optimized away. With these changes, users should be able to get a lot further on first try, at least far enough to open and edit documents.
The `GRIST_SINGLE_ORG` setting was proving a bit confusing, since it appeared to only work when set to `docs`. This diff
adds a check for whether the specified org exists, and if not, it creates it. This still depends on having a user email to make as the owner of the team, so there could be remaining difficulties there.
Test Plan: tested manually with nginx
Reviewers: jarek
Reviewed By: jarek
Differential Revision: https://phab.getgrist.com/D3299
2022-03-02 19:07:26 +00:00
|
|
|
if (!useWorkerPool()) {
|
|
|
|
body = await gristServer.getDocTemplate();
|
|
|
|
} else {
|
|
|
|
// The reason to pass through app.html fetched from docWorker is in case it is a different
|
|
|
|
// version of Grist (could be newer or older).
|
|
|
|
// TODO: More must be done for correct version tagging of URLs: <base href> assumes all
|
|
|
|
// links and static resources come from the same host, but we'll have Home API, DocWorker,
|
|
|
|
// and static resources all at hostnames different from where this page is served.
|
|
|
|
// TODO docWorkerMain needs to serve app.html, perhaps with correct base-href already set.
|
|
|
|
const headers = {
|
|
|
|
Accept: 'application/json',
|
2024-05-14 16:58:41 +00:00
|
|
|
...getTransitiveHeaders(req, { includeOrigin: true }),
|
(core) make Grist easier to run with a single server
Summary:
This makes many small changes so that Grist is less fussy to run as a single instance behind a reverse proxy. Some users had difficulty with the self-connections Grist would make, due to internal network setup, and since these are unnecessary in any case in this scenario, they are now optimized away. Likewise some users had difficulties related to doc worker urls, which are now also optimized away. With these changes, users should be able to get a lot further on first try, at least far enough to open and edit documents.
The `GRIST_SINGLE_ORG` setting was proving a bit confusing, since it appeared to only work when set to `docs`. This diff
adds a check for whether the specified org exists, and if not, it creates it. This still depends on having a user email to make as the owner of the team, so there could be remaining difficulties there.
Test Plan: tested manually with nginx
Reviewers: jarek
Reviewed By: jarek
Differential Revision: https://phab.getgrist.com/D3299
2022-03-02 19:07:26 +00:00
|
|
|
};
|
|
|
|
const workerInfo = await getWorker(docWorkerMap, docId, `/${docId}/app.html`, {headers});
|
|
|
|
docStatus = workerInfo.docStatus;
|
|
|
|
body = await workerInfo.resp.json();
|
|
|
|
}
|
2020-07-21 13:20:51 +00:00
|
|
|
|
2023-04-06 15:10:29 +00:00
|
|
|
const isPublic = ((doc as unknown) as APIDocument).public ?? false;
|
2023-07-04 21:21:34 +00:00
|
|
|
const isSnapshot = Boolean(parseUrlId(urlId).snapshotId);
|
2023-11-01 13:54:19 +00:00
|
|
|
const isTemplate = doc.type === 'template';
|
2023-04-06 15:10:29 +00:00
|
|
|
if (isPublic || isTemplate) {
|
2023-11-01 13:54:19 +00:00
|
|
|
gristServer.getTelemetry().logEvent(mreq, 'documentOpened', {
|
2023-06-06 17:08:50 +00:00
|
|
|
limited: {
|
2023-07-04 21:21:34 +00:00
|
|
|
docIdDigest: docId,
|
2023-06-06 17:08:50 +00:00
|
|
|
access: doc.access,
|
|
|
|
isPublic,
|
|
|
|
isSnapshot,
|
|
|
|
isTemplate,
|
|
|
|
lastUpdated: doc.updatedAt,
|
|
|
|
},
|
|
|
|
full: {
|
|
|
|
siteId: doc.workspace.org.id,
|
|
|
|
siteType: doc.workspace.org.billingAccount.product.name,
|
|
|
|
userId: mreq.userId,
|
|
|
|
altSessionId: mreq.altSessionId,
|
|
|
|
},
|
2023-11-01 13:54:19 +00:00
|
|
|
});
|
2023-04-06 15:10:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (isTemplate) {
|
|
|
|
// Keep track of the last template a user visited in the last hour.
|
|
|
|
// If a sign-up occurs within that time period, we'll know which
|
|
|
|
// template, if any, was viewed most recently.
|
|
|
|
const value = {
|
|
|
|
isAnonymous: isAnonymousUser(mreq),
|
|
|
|
templateId: docId,
|
|
|
|
};
|
2023-06-06 17:08:50 +00:00
|
|
|
res.cookie(TELEMETRY_TEMPLATE_SIGNUP_COOKIE_NAME, JSON.stringify(value), {
|
2023-04-06 15:10:29 +00:00
|
|
|
maxAge: 1000 * 60 * 60,
|
|
|
|
httpOnly: true,
|
|
|
|
path: '/',
|
|
|
|
domain: getCookieDomain(req),
|
|
|
|
sameSite: 'lax',
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-05-14 16:58:41 +00:00
|
|
|
// Without a public URL, we're in single server mode.
|
|
|
|
// Use a null workerPublicURL, to signify that the URL prefix serving the
|
|
|
|
// current endpoint is the only one available.
|
|
|
|
const publicUrl = docStatus?.docWorker?.publicUrl;
|
|
|
|
const workerPublicUrl = publicUrl !== undefined ? customizeDocWorkerUrl(publicUrl, req) : null;
|
|
|
|
|
2020-07-21 13:20:51 +00:00
|
|
|
await sendAppPage(req, res, {path: "", content: body.page, tag: body.tag, status: 200,
|
|
|
|
googleTagManager: 'anon', config: {
|
|
|
|
assignmentId: docId,
|
2024-05-14 16:58:41 +00:00
|
|
|
getWorker: {[docId]: workerPublicUrl },
|
2020-07-21 13:20:51 +00:00
|
|
|
getDoc: {[docId]: pruneAPIResult(doc as unknown as APIDocument)},
|
2021-08-05 15:12:46 +00:00
|
|
|
plugins
|
2020-07-21 13:20:51 +00:00
|
|
|
}});
|
|
|
|
});
|
2024-02-21 19:22:01 +00:00
|
|
|
// Handlers for form preview URLs: one with a slug and one without.
|
|
|
|
app.get('/doc/:urlId([^/]+)/f/:vsId', ...docMiddleware, expressWrap(async (req, res) => {
|
|
|
|
return sendAppPage(req, res, {path: 'form.html', status: 200, config: {}, googleTagManager: 'anon'});
|
|
|
|
}));
|
|
|
|
app.get('/:urlId([^-/]{12,})/:slug([^/]+)/f/:vsId', ...docMiddleware, expressWrap(async (req, res) => {
|
|
|
|
return sendAppPage(req, res, {path: 'form.html', status: 200, config: {}, googleTagManager: 'anon'});
|
|
|
|
}));
|
|
|
|
// Handler for form URLs that include a share key.
|
|
|
|
app.get('/forms/:shareKey([^/]+)/:vsId', ...formMiddleware, expressWrap(async (req, res) => {
|
|
|
|
return sendAppPage(req, res, {path: 'form.html', status: 200, config: {}, googleTagManager: 'anon'});
|
|
|
|
}));
|
2020-07-21 13:20:51 +00:00
|
|
|
// The * is a wildcard in express 4, rather than a regex symbol.
|
|
|
|
// See https://expressjs.com/en/guide/routing.html
|
2020-08-19 20:25:42 +00:00
|
|
|
app.get('/doc/:urlId([^/]+):remainder(*)', ...docMiddleware, docHandler);
|
(core) add initial support for special shares
Summary:
This gives a mechanism for controlling access control within a document that is distinct from (though implemented with the same machinery as) granular access rules.
It was hard to find a good way to insert this that didn't dissolve in a soup of complications, so here's what I went with:
* When reading rules, if there are shares, extra rules are added.
* If there are shares, all rules are made conditional on a "ShareRef" user property.
* "ShareRef" is null when a doc is accessed in normal way, and the row id of a share when accessed via a share.
There's no UI for controlling shares (George is working on it for forms), but you can do it by editing a `_grist_Shares` table in a document. Suppose you make a fresh document with a single page/table/widget, then to create an empty share you can do:
```
gristDocPageModel.gristDoc.get().docData.sendAction(['AddRecord', '_grist_Shares', null, {linkId: 'xyz', options: '{"publish": true}'}])
```
If you look at the home db now there should be something in the `shares` table:
```
$ sqlite3 -table landing.db "select * from shares"
+----+------------------------+------------------------+--------------+---------+
| id | key | doc_id | link_id | options |
+----+------------------------+------------------------+--------------+---------+
| 1 | gSL4g38PsyautLHnjmXh2K | 4qYuace1xP2CTcPunFdtan | xyz | ... |
+----+------------------------+------------------------+--------------+---------+
```
If you take the key from that (gSL4g38PsyautLHnjmXh2K in this case) and replace the document's urlId in its URL with `s.<key>` (in this case `s.gSL4g38PsyautLHnjmXh2K` then you can use the regular document landing page (it will be quite blank initially) or API endpoint via the share.
E.g. for me `http://localhost:8080/o/docs/s0gSL4g38PsyautLHnjmXh2K/share-inter-3` accesses the doc.
To actually share some material - useful commands:
```
gristDocPageModel.gristDoc.get().docData.getMetaTable('_grist_Views_section').getRecords()
gristDocPageModel.gristDoc.get().docData.sendAction(['UpdateRecord', '_grist_Views_section', 1, {shareOptions: '{"publish": true, "form": true}'}])
gristDocPageModel.gristDoc.get().docData.getMetaTable('_grist_Pages').getRecords()
gristDocPageModel.gristDoc.get().docData.sendAction(['UpdateRecord', '_grist_Pages', 1, {shareRef: 1}])
```
For a share to be effective, at least one page needs to have its shareRef set to the rowId of the share, and at least one widget on one of those pages needs to have its shareOptions set to {"publish": "true", "form": "true"} (meaning turn on sharing, and include form sharing), and the share itself needs {"publish": true} on its options.
I think special shares are kind of incompatible with public sharing, since by their nature (allowing access to all endpoints) they easily expose the docId, and changing that would be hard.
Test Plan: tests added
Reviewers: dsagal, georgegevoian
Reviewed By: dsagal, georgegevoian
Subscribers: jarek, dsagal
Differential Revision: https://phab.getgrist.com/D4144
2024-01-03 16:53:20 +00:00
|
|
|
app.get('/s/:urlId([^/]+):remainder(*)',
|
|
|
|
(req, res, next) => {
|
|
|
|
// /s/<key> is another way of writing /doc/<prefix><key> for shares.
|
|
|
|
req.params.urlId = SHARE_KEY_PREFIX + req.params.urlId;
|
|
|
|
req.params.viaShare = "1";
|
|
|
|
next();
|
|
|
|
},
|
|
|
|
...docMiddleware, docHandler);
|
2023-12-02 20:27:58 +00:00
|
|
|
app.get('/:urlId([^-/]{12,})(/:slug([^/]+):remainder(*))?',
|
2020-08-19 20:25:42 +00:00
|
|
|
...docMiddleware, docHandler);
|
2020-07-21 13:20:51 +00:00
|
|
|
}
|