You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-11-26 22:41:17 +02:00
All, Server: Add support for X-API-MIN-VERSION header
This commit is contained in:
@@ -142,6 +142,7 @@ export default class JoplinServerApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (sessionId) headers['X-API-AUTH'] = sessionId;
|
if (sessionId) headers['X-API-AUTH'] = sessionId;
|
||||||
|
headers['X-API-MIN-VERSION'] = '2.1.4';
|
||||||
|
|
||||||
const fetchOptions: any = {};
|
const fetchOptions: any = {};
|
||||||
fetchOptions.headers = headers;
|
fetchOptions.headers = headers;
|
||||||
|
|||||||
5
packages/server/package-lock.json
generated
5
packages/server/package-lock.json
generated
@@ -2436,6 +2436,11 @@
|
|||||||
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
|
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"compare-versions": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA=="
|
||||||
|
},
|
||||||
"component-emitter": {
|
"component-emitter": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"bulma": "^0.9.1",
|
"bulma": "^0.9.1",
|
||||||
"bulma-prefers-dark": "^0.1.0-beta.0",
|
"bulma-prefers-dark": "^0.1.0-beta.0",
|
||||||
|
"compare-versions": "^3.6.0",
|
||||||
"dayjs": "^1.9.8",
|
"dayjs": "^1.9.8",
|
||||||
"formidable": "^1.2.2",
|
"formidable": "^1.2.2",
|
||||||
"fs-extra": "^8.1.0",
|
"fs-extra": "^8.1.0",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import Logger, { LoggerWrapper, TargetType } from '@joplin/lib/Logger';
|
|||||||
import config, { initConfig, runningInDocker, EnvVariables } from './config';
|
import config, { initConfig, runningInDocker, EnvVariables } from './config';
|
||||||
import { createDb, dropDb } from './tools/dbTools';
|
import { createDb, dropDb } from './tools/dbTools';
|
||||||
import { dropTables, connectDb, disconnectDb, migrateDb, waitForConnection, sqliteDefaultDir } from './db';
|
import { dropTables, connectDb, disconnectDb, migrateDb, waitForConnection, sqliteDefaultDir } from './db';
|
||||||
import { AppContext, Env } from './utils/types';
|
import { AppContext, Env, KoaNext } from './utils/types';
|
||||||
import FsDriverNode from '@joplin/lib/fs-driver-node';
|
import FsDriverNode from '@joplin/lib/fs-driver-node';
|
||||||
import routeHandler from './middleware/routeHandler';
|
import routeHandler from './middleware/routeHandler';
|
||||||
import notificationHandler from './middleware/notificationHandler';
|
import notificationHandler from './middleware/notificationHandler';
|
||||||
@@ -17,6 +17,7 @@ import setupAppContext from './utils/setupAppContext';
|
|||||||
import { initializeJoplinUtils } from './utils/joplinUtils';
|
import { initializeJoplinUtils } from './utils/joplinUtils';
|
||||||
import startServices from './utils/startServices';
|
import startServices from './utils/startServices';
|
||||||
import { credentialFile } from './utils/testing/testUtils';
|
import { credentialFile } from './utils/testing/testUtils';
|
||||||
|
import apiVersionHandler from './middleware/apiVersionHandler';
|
||||||
|
|
||||||
const cors = require('@koa/cors');
|
const cors = require('@koa/cors');
|
||||||
const nodeEnvFile = require('node-env-file');
|
const nodeEnvFile = require('node-env-file');
|
||||||
@@ -119,6 +120,18 @@ async function main() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is used to catch any low level error thrown from a middleware. It
|
||||||
|
// won't deal with errors from routeHandler, which catches and handles its
|
||||||
|
// own errors.
|
||||||
|
app.use(async (ctx: AppContext, next: KoaNext) => {
|
||||||
|
try {
|
||||||
|
await next();
|
||||||
|
} catch (error) {
|
||||||
|
ctx.status = error.httpCode || 500;
|
||||||
|
ctx.body = JSON.stringify({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.use(cors({
|
app.use(cors({
|
||||||
// https://github.com/koajs/cors/issues/52#issuecomment-413887382
|
// https://github.com/koajs/cors/issues/52#issuecomment-413887382
|
||||||
origin: (ctx: AppContext) => {
|
origin: (ctx: AppContext) => {
|
||||||
@@ -132,6 +145,7 @@ async function main() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
app.use(apiVersionHandler);
|
||||||
app.use(ownerHandler);
|
app.use(ownerHandler);
|
||||||
app.use(notificationHandler);
|
app.use(notificationHandler);
|
||||||
app.use(routeHandler);
|
app.use(routeHandler);
|
||||||
|
|||||||
57
packages/server/src/middleware/apiVersionHandler.test.ts
Normal file
57
packages/server/src/middleware/apiVersionHandler.test.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import config from '../config';
|
||||||
|
import { ErrorPreconditionFailed } from '../utils/errors';
|
||||||
|
import { beforeAllDb, afterAllTests, beforeEachDb, koaAppContext, koaNext, expectNotThrow, expectHttpError } from '../utils/testing/testUtils';
|
||||||
|
import apiVersionHandler from './apiVersionHandler';
|
||||||
|
|
||||||
|
describe('apiVersionHandler', function() {
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await beforeAllDb('apiVersionHandler');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await afterAllTests();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await beforeEachDb();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should work if no version header is provided', async function() {
|
||||||
|
const context = await koaAppContext({});
|
||||||
|
await expectNotThrow(async () => apiVersionHandler(context, koaNext));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should work if the header version number is lower than the server version', async function() {
|
||||||
|
config().appVersion = '2.1.0';
|
||||||
|
|
||||||
|
const context = await koaAppContext({
|
||||||
|
request: {
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/ping',
|
||||||
|
headers: {
|
||||||
|
'x-api-min-version': '2.0.0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await expectNotThrow(async () => apiVersionHandler(context, koaNext));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not work if the header version number is greater than the server version', async function() {
|
||||||
|
config().appVersion = '2.1.0';
|
||||||
|
|
||||||
|
const context = await koaAppContext({
|
||||||
|
request: {
|
||||||
|
method: 'GET',
|
||||||
|
url: '/api/ping',
|
||||||
|
headers: {
|
||||||
|
'x-api-min-version': '2.2.0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await expectHttpError(async () => apiVersionHandler(context, koaNext), ErrorPreconditionFailed.httpCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
27
packages/server/src/middleware/apiVersionHandler.ts
Normal file
27
packages/server/src/middleware/apiVersionHandler.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { AppContext, KoaNext } from '../utils/types';
|
||||||
|
import { isApiRequest } from '../utils/requestUtils';
|
||||||
|
import config from '../config';
|
||||||
|
import { ErrorPreconditionFailed } from '../utils/errors';
|
||||||
|
const compareVersions = require('compare-versions');
|
||||||
|
|
||||||
|
export default async function(ctx: AppContext, next: KoaNext): Promise<void> {
|
||||||
|
if (!isApiRequest(ctx)) return;
|
||||||
|
|
||||||
|
const appVersion = config().appVersion;
|
||||||
|
const minVersion = ctx.headers['x-api-min-version'];
|
||||||
|
|
||||||
|
// For now we don't require this header to be set to keep compatibility with
|
||||||
|
// older clients.
|
||||||
|
if (!minVersion) return next();
|
||||||
|
|
||||||
|
const diff = compareVersions(appVersion, minVersion);
|
||||||
|
|
||||||
|
// We only throw an error if the client requires a version of Joplin Server
|
||||||
|
// that's ahead of what's installed. This is mostly to automatically notify
|
||||||
|
// those who self-host so that they know they need to upgrade Joplin Server.
|
||||||
|
if (diff < 0) {
|
||||||
|
throw new ErrorPreconditionFailed(`Joplin Server v${minVersion} is required but v${appVersion} is installed. Please upgrade Joplin Server.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
}
|
||||||
@@ -51,6 +51,16 @@ export class ErrorBadRequest extends ApiError {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ErrorPreconditionFailed extends ApiError {
|
||||||
|
public static httpCode: number = 412;
|
||||||
|
|
||||||
|
public constructor(message: string = 'Precondition Failed') {
|
||||||
|
super(message, ErrorPreconditionFailed.httpCode);
|
||||||
|
Object.setPrototypeOf(this, ErrorPreconditionFailed.prototype);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
export class ErrorUnprocessableEntity extends ApiError {
|
export class ErrorUnprocessableEntity extends ApiError {
|
||||||
public static httpCode: number = 422;
|
public static httpCode: number = 422;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user