(core) Adding latest version section to the admin panel.

Summary:
Update for the admin page to show the latest available version information.
- Latest version is read from docs.getgrist.com by default
- It sends basic information (installationId, deployment type, and version)
- Checks are done only on the page itself
- The actual request is routed through the API (to avoid CORS)

Test Plan: Added new test

Reviewers: paulfitz

Reviewed By: paulfitz

Subscribers: paulfitz

Differential Revision: https://phab.getgrist.com/D4238
This commit is contained in:
Jarosław Sadziński
2024-04-29 16:54:03 +02:00
parent a3442aee77
commit ecf242c6c6
10 changed files with 609 additions and 69 deletions

View File

@@ -2,7 +2,7 @@ import {ApiError} from 'app/common/ApiError';
import {ICustomWidget} from 'app/common/CustomWidget';
import {delay} from 'app/common/delay';
import {DocCreationInfo} from 'app/common/DocListAPI';
import {encodeUrl, getSlugIfNeeded, GristDeploymentType, GristDeploymentTypes,
import {commonUrls, encodeUrl, getSlugIfNeeded, GristDeploymentType, GristDeploymentTypes,
GristLoadConfig, IGristUrlState, isOrgInPathOnly, parseSubdomain,
sanitizePathTail} from 'app/common/gristUrls';
import {getOrgUrlInfo} from 'app/common/gristUrls';
@@ -1853,6 +1853,35 @@ export class FlexServer implements GristServer {
return resp.status(200).send();
}));
// GET api/checkUpdates
// Retrieves the latest version of the client from Grist SAAS endpoint.
this.app.get('/api/install/updates', adminPageMiddleware, expressWrap(async (req, res) => {
// Prepare data for the telemetry that endpoint might expect.
const installationId = (await this.getActivations().current()).id;
const deploymentType = this.getDeploymentType();
const currentVersion = version.version;
const response = await fetch(process.env.GRIST_TEST_VERSION_CHECK_URL || commonUrls.versionCheck, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
installationId,
deploymentType,
currentVersion,
}),
});
if (!response.ok) {
res.status(response.status);
if (response.headers.get('content-type')?.includes('application/json')) {
const data = await response.json();
res.json(data);
} else {
res.send(await response.text());
}
} else {
res.json(await response.json());
}
}));
}
// Get the HTML template sent for document pages.

View File

@@ -1,14 +1,16 @@
import { ApiError } from "app/common/ApiError";
import { MapWithTTL } from "app/common/AsyncCreate";
import { GristDeploymentType } from "app/common/gristUrls";
import { LatestVersion } from 'app/common/InstallAPI';
import { naturalCompare } from "app/common/SortFunc";
import { RequestWithLogin } from "app/server/lib/Authorizer";
import { expressWrap } from 'app/server/lib/expressWrap';
import { GristServer } from "app/server/lib/GristServer";
import { optIntegerParam, optStringParam } from "app/server/lib/requestUtils";
import { rateLimit } from 'express-rate-limit';
import { AbortController, AbortSignal } from 'node-abort-controller';
import type * as express from "express";
import fetch from "node-fetch";
import {expressWrap} from 'app/server/lib/expressWrap';
// URL to show to the client where the new version for docker based deployments can be found.
@@ -67,8 +69,18 @@ export class UpdateManager {
}
}
// Rate limit the requests to the version API, so that we don't get spammed.
// 30 requests per second, per IP. The requests are cached so, we should be fine, but make
// sure it doesn't get out of hand. On dev laptop I could go up to 600 requests per second.
// (30 was picked by hand, to not hit the limit during tests).
const limiter = rateLimit({
windowMs: 1000,
limit: 30,
legacyHeaders: true,
});
// Support both POST and GET requests.
this._app.use("/api/version", expressWrap(async (req, res) => {
this._app.use("/api/version", limiter, expressWrap(async (req, res) => {
// Get some telemetry from the body request.
const payload = (name: string) => req.body?.[name] ?? req.query[name];
@@ -132,29 +144,6 @@ export class UpdateManager {
}
}
/**
* JSON returned to the client (exported for tests).
*/
export interface LatestVersion {
/**
* Latest version of core component of the client.
*/
latestVersion: string;
/**
* If there were any critical updates after client's version. Undefined if
* we don't know client version or couldn't figure this out for some other reason.
*/
isCritical?: boolean;
/**
* Url where the client can download the latest version (if applicable)
*/
updateURL?: string;
/**
* When the latest version was updated (in ISO format).
*/
updatedAt?: string;
}
type VersionChecker = (signal: AbortSignal) => Promise<LatestVersion>;