1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-11-27 08:21:03 +02:00

Merge branch 'dev' into release-2.0

This commit is contained in:
Laurent Cozic 2021-05-25 17:23:43 +02:00
commit d89bbc5571
26 changed files with 116 additions and 60 deletions

View File

@ -9,6 +9,8 @@ version: '3'
services:
db:
image: postgres:13.1
volumes:
- ./data/postgres:/var/lib/postgresql/data
ports:
- "5432:5432"
restart: unless-stopped

View File

@ -26,12 +26,12 @@ if [ "$RESET_ALL" == "1" ]; then
echo "config keychain.supported 0" >> "$CMD_FILE"
echo "config sync.target 9" >> "$CMD_FILE"
echo "config sync.9.path http://localhost:22300" >> "$CMD_FILE"
echo "config sync.9.path http://api-joplincloud.local:22300" >> "$CMD_FILE"
echo "config sync.9.username $USER_EMAIL" >> "$CMD_FILE"
echo "config sync.9.password 123456" >> "$CMD_FILE"
if [ "$1" == "1" ]; then
curl --data '{"action": "createTestUsers"}' -H 'Content-Type: application/json' http://localhost:22300/api/debug
curl --data '{"action": "createTestUsers"}' -H 'Content-Type: application/json' http://api-joplincloud.local:22300/api/debug
echo 'mkbook "shared"' >> "$CMD_FILE"
echo 'mkbook "other"' >> "$CMD_FILE"

View File

@ -130,6 +130,8 @@ async function main() {
appLogger().info(`Starting server (${env}) on port ${config().port} and PID ${process.pid}...`);
appLogger().info('Running in Docker:', runningInDocker());
appLogger().info('Public base URL:', config().baseUrl);
appLogger().info('API base URL:', config().apiBaseUrl);
appLogger().info('User content base URL:', config().userContentBaseUrl);
appLogger().info('Log dir:', config().logDir);
appLogger().info('DB Config:', markPasswords(config().database));
@ -158,7 +160,7 @@ async function main() {
// }
// }
appLogger().info(`Call this for testing: \`curl ${config().baseUrl}/api/ping\``);
appLogger().info(`Call this for testing: \`curl ${config().apiBaseUrl}/api/ping\``);
// const tree: any = {
// '000000000000000000000000000000F1': {},

View File

@ -1,9 +1,12 @@
import { rtrimSlashes } from '@joplin/lib/path-utils';
import { Config, DatabaseConfig, DatabaseConfigClient, MailerConfig } from './utils/types';
import { Config, DatabaseConfig, DatabaseConfigClient, MailerConfig, RouteType } from './utils/types';
import * as pathUtils from 'path';
export interface EnvVariables {
APP_BASE_URL?: string;
USER_CONTENT_BASE_URL?: string;
API_BASE_URL?: string;
APP_PORT?: string;
DB_CLIENT?: string;
RUNNING_IN_DOCKER?: string;
@ -96,6 +99,7 @@ export function initConfig(env: EnvVariables, overrides: any = null) {
const rootDir = pathUtils.dirname(__dirname);
const viewDir = `${pathUtils.dirname(__dirname)}/src/views`;
const appPort = env.APP_PORT ? Number(env.APP_PORT) : 22300;
const baseUrl = baseUrlFromEnv(env, appPort);
config_ = {
rootDir: rootDir,
@ -106,11 +110,27 @@ export function initConfig(env: EnvVariables, overrides: any = null) {
database: databaseConfigFromEnv(runningInDocker_, env),
mailer: mailerConfigFromEnv(env),
port: appPort,
baseUrl: baseUrlFromEnv(env, appPort),
baseUrl,
apiBaseUrl: env.API_BASE_URL ? env.API_BASE_URL : baseUrl,
userContentBaseUrl: env.USER_CONTENT_BASE_URL ? env.USER_CONTENT_BASE_URL : baseUrl,
...overrides,
};
}
export function baseUrl(type: RouteType): string {
if (type === RouteType.Web) return config().baseUrl;
if (type === RouteType.Api) return config().apiBaseUrl;
if (type === RouteType.UserContent) return config().userContentBaseUrl;
throw new Error(`Unknown type: ${type}`);
}
// User content URL is not supported for now so only show the URL if the
// user content is hosted on the same domain. Needs to get cookie working
// across domains to get user content url working.
export function showItemUrls(config: Config): boolean {
return config.userContentBaseUrl === config.baseUrl;
}
function config(): Config {
if (!config_) throw new Error('Config has not been initialized!');
return config_;

View File

@ -1,15 +1,6 @@
import { routeResponseFormat, Response, RouteResponseFormat, execRequest } from '../utils/routeUtils';
import { AppContext, Env } from '../utils/types';
import { isView, View } from '../services/MustacheService';
// import config from '../config';
// let mustache_: MustacheService = null;
// function mustache(): MustacheService {
// if (!mustache_) {
// mustache_ = new MustacheService(config().viewDir, config().baseUrl);
// }
// return mustache_;
// }
export default async function(ctx: AppContext) {
ctx.appLogger().info(`${ctx.request.method} ${ctx.path}`);
@ -44,7 +35,9 @@ export default async function(ctx: AppContext) {
const responseFormat = routeResponseFormat(ctx);
if (responseFormat === RouteResponseFormat.Html) {
if (error.code === 'invalidOrigin') {
ctx.response.body = error.message;
} else if (responseFormat === RouteResponseFormat.Html) {
ctx.response.set('Content-Type', 'text/html');
const view: View = {
name: 'error',

View File

@ -2,10 +2,11 @@ import config from '../../config';
import { createTestUsers } from '../../tools/debugTools';
import { bodyFields } from '../../utils/requestUtils';
import Router from '../../utils/Router';
import { RouteType } from '../../utils/types';
import { SubPath } from '../../utils/routeUtils';
import { AppContext } from '../../utils/types';
const router = new Router();
const router = new Router(RouteType.Api);
router.public = true;

View File

@ -1,6 +1,7 @@
import { ErrorNotFound } from '../../utils/errors';
import { bodyFields } from '../../utils/requestUtils';
import Router from '../../utils/Router';
import { RouteType } from '../../utils/types';
import { SubPath } from '../../utils/routeUtils';
import { AppContext } from '../../utils/types';
@ -14,7 +15,7 @@ const supportedEvents: Record<string, Function> = {
},
};
const router = new Router();
const router = new Router(RouteType.Api);
router.post('api/events', async (_path: SubPath, ctx: AppContext) => {
const event = await bodyFields<Event>(ctx.req);

View File

@ -2,6 +2,7 @@ import { Item, Uuid } from '../../db';
import { formParse } from '../../utils/requestUtils';
import { respondWithItemContent, SubPath } from '../../utils/routeUtils';
import Router from '../../utils/Router';
import { RouteType } from '../../utils/types';
import { AppContext } from '../../utils/types';
import * as fs from 'fs-extra';
import { ErrorMethodNotAllowed, ErrorNotFound } from '../../utils/errors';
@ -10,7 +11,7 @@ import { requestDeltaPagination, requestPagination } from '../../models/utils/pa
import { AclAction } from '../../models/BaseModel';
import { safeRemove } from '../../utils/fileUtils';
const router = new Router();
const router = new Router(RouteType.Api);
// Note about access control:
//

View File

@ -1,6 +1,7 @@
import Router from '../../utils/Router';
import { RouteType } from '../../utils/types';
const router = new Router();
const router = new Router(RouteType.Api);
router.public = true;

View File

@ -1,11 +1,12 @@
import { SubPath } from '../../utils/routeUtils';
import Router from '../../utils/Router';
import { RouteType } from '../../utils/types';
import { ErrorForbidden } from '../../utils/errors';
import { AppContext } from '../../utils/types';
import { bodyFields } from '../../utils/requestUtils';
import { User } from '../../db';
const router = new Router();
const router = new Router(RouteType.Api);
router.public = true;

View File

@ -2,10 +2,11 @@ import { ErrorBadRequest, ErrorNotFound } from '../../utils/errors';
import { bodyFields } from '../../utils/requestUtils';
import { SubPath } from '../../utils/routeUtils';
import Router from '../../utils/Router';
import { RouteType } from '../../utils/types';
import { AppContext } from '../../utils/types';
import { AclAction } from '../../models/BaseModel';
const router = new Router();
const router = new Router(RouteType.Api);
router.patch('api/share_users/:id', async (path: SubPath, ctx: AppContext) => {
const shareUserModel = ctx.models.shareUser();

View File

@ -3,6 +3,7 @@ import { Share, ShareType } from '../../db';
import { bodyFields, ownerRequired } from '../../utils/requestUtils';
import { SubPath } from '../../utils/routeUtils';
import Router from '../../utils/Router';
import { RouteType } from '../../utils/types';
import { AppContext } from '../../utils/types';
import { AclAction } from '../../models/BaseModel';
@ -11,7 +12,7 @@ interface ShareApiInput extends Share {
note_id?: string;
}
const router = new Router();
const router = new Router(RouteType.Api);
router.public = true;

View File

@ -2,12 +2,13 @@ import { User } from '../../db';
import { bodyFields } from '../../utils/requestUtils';
import { SubPath } from '../../utils/routeUtils';
import Router from '../../utils/Router';
import { RouteType } from '../../utils/types';
import { AppContext } from '../../utils/types';
import { ErrorNotFound } from '../../utils/errors';
import { AclAction } from '../../models/BaseModel';
import uuidgen from '../../utils/uuidgen';
const router = new Router();
const router = new Router(RouteType.Api);
async function fetchUser(path: SubPath, ctx: AppContext): Promise<User> {
const user = await ctx.models.user().load(path.id);

View File

@ -4,7 +4,7 @@ import { ErrorNotFound, ErrorForbidden } from '../utils/errors';
import { dirname, normalize } from 'path';
import { pathExists } from 'fs-extra';
import * as fs from 'fs-extra';
import { AppContext } from '../utils/types';
import { AppContext, RouteType } from '../utils/types';
import { localFileFromUrl } from '../utils/joplinUtils';
const { mime } = require('@joplin/lib/mime-utils.js');
@ -44,7 +44,7 @@ async function findLocalFile(path: string): Promise<string> {
return localPath;
}
const router = new Router();
const router = new Router(RouteType.Web);
router.public = true;

View File

@ -1,5 +1,6 @@
import { SubPath } from '../../utils/routeUtils';
import Router from '../../utils/Router';
import { RouteType } from '../../utils/types';
import { AppContext } from '../../utils/types';
import { changeTypeToString } from '../../db';
import { PaginationOrderDir } from '../../models/utils/pagination';
@ -7,8 +8,9 @@ import { formatDateTime } from '../../utils/time';
import defaultView from '../../utils/defaultView';
import { View } from '../../services/MustacheService';
import { makeTablePagination, Table, Row, makeTableView } from '../../utils/views/table';
import config, { showItemUrls } from '../../config';
const router = new Router();
const router = new Router(RouteType.Web);
router.get('changes', async (_path: SubPath, ctx: AppContext) => {
const pagination = makeTablePagination(ctx.query, 'updated_time', PaginationOrderDir.DESC);
@ -40,7 +42,7 @@ router.get('changes', async (_path: SubPath, ctx: AppContext) => {
{
value: change.item_name,
stretch: true,
url: items.find(i => i.id === change.item_id) ? ctx.models.item().itemContentUrl(change.item_id) : '',
url: showItemUrls(config()) ? (items.find(i => i.id === change.item_id) ? ctx.models.item().itemContentUrl(change.item_id) : '') : null,
},
{
value: changeTypeToString(change.type),

View File

@ -1,11 +1,12 @@
import { SubPath } from '../../utils/routeUtils';
import Router from '../../utils/Router';
import { RouteType } from '../../utils/types';
import { AppContext } from '../../utils/types';
import { contextSessionId } from '../../utils/requestUtils';
import { ErrorMethodNotAllowed } from '../../utils/errors';
import defaultView from '../../utils/defaultView';
const router: Router = new Router();
const router: Router = new Router(RouteType.Web);
router.get('home', async (_path: SubPath, ctx: AppContext) => {
contextSessionId(ctx);

View File

@ -1,9 +1,10 @@
import { SubPath, redirect, respondWithItemContent } from '../../utils/routeUtils';
import Router from '../../utils/Router';
import { RouteType } from '../../utils/types';
import { AppContext } from '../../utils/types';
import { formParse } from '../../utils/requestUtils';
import { ErrorNotFound } from '../../utils/errors';
import config from '../../config';
import config, { showItemUrls } from '../../config';
import { formatDateTime } from '../../utils/time';
import defaultView from '../../utils/defaultView';
import { View } from '../../services/MustacheService';
@ -11,7 +12,7 @@ import { makeTablePagination, makeTableView, Row, Table } from '../../utils/view
import { PaginationOrderDir } from '../../models/utils/pagination';
const prettyBytes = require('pretty-bytes');
const router = new Router();
const router = new Router(RouteType.Web);
router.get('items', async (_path: SubPath, ctx: AppContext) => {
const pagination = makeTablePagination(ctx.query, 'name', PaginationOrderDir.ASC);
@ -46,7 +47,7 @@ router.get('items', async (_path: SubPath, ctx: AppContext) => {
{
value: item.name,
stretch: true,
url: `${config().baseUrl}/items/${item.id}/content`,
url: showItemUrls(config()) ? `${config().userContentBaseUrl}/items/${item.id}/content` : null,
},
{
value: prettyBytes(item.content_size),
@ -75,7 +76,7 @@ router.get('items/:id/content', async (path: SubPath, ctx: AppContext) => {
const item = await itemModel.loadWithContent(path.id);
if (!item) throw new ErrorNotFound();
return respondWithItemContent(ctx.response, item, item.content);
});
}, RouteType.UserContent);
router.post('items', async (_path: SubPath, ctx: AppContext) => {
const body = await formParse(ctx.req);

View File

@ -1,5 +1,6 @@
import { SubPath, redirect } from '../../utils/routeUtils';
import Router from '../../utils/Router';
import { RouteType } from '../../utils/types';
import { AppContext } from '../../utils/types';
import { formParse } from '../../utils/requestUtils';
import config from '../../config';
@ -13,7 +14,7 @@ function makeView(error: any = null): View {
return view;
}
const router: Router = new Router();
const router: Router = new Router(RouteType.Web);
router.public = true;

View File

@ -1,10 +1,11 @@
import { SubPath, redirect } from '../../utils/routeUtils';
import Router from '../../utils/Router';
import { RouteType } from '../../utils/types';
import { AppContext } from '../../utils/types';
import config from '../../config';
import { contextSessionId } from '../../utils/requestUtils';
const router = new Router();
const router = new Router(RouteType.Web);
router.post('logout', async (_path: SubPath, ctx: AppContext) => {
const sessionId = contextSessionId(ctx, false);

View File

@ -1,11 +1,12 @@
import { SubPath } from '../../utils/routeUtils';
import Router from '../../utils/Router';
import { RouteType } from '../../utils/types';
import { AppContext } from '../../utils/types';
import { bodyFields } from '../../utils/requestUtils';
import { ErrorNotFound } from '../../utils/errors';
import { Notification } from '../../db';
const router = new Router();
const router = new Router(RouteType.Web);
router.patch('notifications/:id', async (path: SubPath, ctx: AppContext) => {
const fields: Notification = await bodyFields(ctx.req);

View File

@ -1,5 +1,6 @@
import { SubPath, ResponseType, Response } from '../../utils/routeUtils';
import Router from '../../utils/Router';
import { RouteType } from '../../utils/types';
import { AppContext } from '../../utils/types';
import { ErrorNotFound } from '../../utils/errors';
import { Item, Share } from '../../db';
@ -18,7 +19,7 @@ async function renderItem(context: AppContext, item: Item, share: Share): Promis
};
}
const router: Router = new Router();
const router: Router = new Router(RouteType.Web);
router.public = true;

View File

@ -1,5 +1,6 @@
import { SubPath, redirect } from '../../utils/routeUtils';
import Router from '../../utils/Router';
import { RouteType } from '../../utils/types';
import { AppContext, HttpMethod } from '../../utils/types';
import { bodyFields, formParse } from '../../utils/requestUtils';
import { ErrorForbidden, ErrorUnprocessableEntity } from '../../utils/errors';
@ -52,7 +53,7 @@ function userIsMe(path: SubPath): boolean {
return path.id === 'me';
}
const router = new Router();
const router = new Router(RouteType.Web);
router.get('users', async (_path: SubPath, ctx: AppContext) => {
const userModel = ctx.models.user();
@ -152,7 +153,7 @@ router.post('users/:id/confirm', async (path: SubPath, ctx: AppContext) => {
return redirect(ctx, `${config().baseUrl}/home`);
} catch (error) {
const endPoint = router.findEndPoint(HttpMethod.GET, 'users/:id/confirm');
return endPoint(path, ctx, error);
return endPoint.handler(path, ctx, error);
}
});
@ -192,7 +193,7 @@ router.post('users', async (path: SubPath, ctx: AppContext) => {
} catch (error) {
if (error instanceof ErrorForbidden) throw error;
const endPoint = router.findEndPoint(HttpMethod.GET, 'users/:id');
return endPoint(path, ctx, user, error);
return endPoint.handler(path, ctx, user, error);
}
});

View File

@ -1,7 +1,12 @@
import { ErrorMethodNotAllowed, ErrorNotFound } from './errors';
import { HttpMethod } from './types';
import { HttpMethod, RouteType } from './types';
import { RouteResponseFormat, RouteHandler } from './routeUtils';
interface RouteInfo {
handler: RouteHandler;
type?: RouteType;
}
export default class Router {
// When the router is public, we do not check that a valid session is
@ -13,22 +18,29 @@ export default class Router {
public responseFormat: RouteResponseFormat = null;
private routes_: Record<string, Record<string, RouteHandler>> = {};
private routes_: Record<string, Record<string, RouteInfo>> = {};
private aliases_: Record<string, Record<string, string>> = {};
private type_: RouteType;
public findEndPoint(method: HttpMethod, schema: string): RouteHandler {
public constructor(type: RouteType) {
this.type_ = type;
}
public findEndPoint(method: HttpMethod, schema: string): RouteInfo {
if (this.aliases_[method]?.[schema]) { return this.findEndPoint(method, this.aliases_[method]?.[schema]); }
if (!this.routes_[method]) { throw new ErrorMethodNotAllowed(`Not allowed: ${method} ${schema}`); }
const endPoint = this.routes_[method][schema];
if (!endPoint) { throw new ErrorNotFound(`Not found: ${method} ${schema}`); }
let endPointFn = endPoint;
let endPointInfo = endPoint;
for (let i = 0; i < 1000; i++) {
if (typeof endPointFn === 'string') {
endPointFn = this.routes_[method]?.[endPointFn];
if (typeof endPointInfo === 'string') {
endPointInfo = this.routes_[method]?.[endPointInfo];
} else {
return endPointFn;
const output = { ...endPointInfo };
if (!output.type) output.type = this.type_;
return output;
}
}
@ -44,29 +56,29 @@ export default class Router {
this.aliases_[method][path] = target;
}
public get(path: string, handler: RouteHandler) {
public get(path: string, handler: RouteHandler, type: RouteType = null) {
if (!this.routes_.GET) { this.routes_.GET = {}; }
this.routes_.GET[path] = handler;
this.routes_.GET[path] = { handler, type };
}
public post(path: string, handler: RouteHandler) {
public post(path: string, handler: RouteHandler, type: RouteType = null) {
if (!this.routes_.POST) { this.routes_.POST = {}; }
this.routes_.POST[path] = handler;
this.routes_.POST[path] = { handler, type };
}
public patch(path: string, handler: RouteHandler) {
public patch(path: string, handler: RouteHandler, type: RouteType = null) {
if (!this.routes_.PATCH) { this.routes_.PATCH = {}; }
this.routes_.PATCH[path] = handler;
this.routes_.PATCH[path] = { handler, type };
}
public del(path: string, handler: RouteHandler) {
public del(path: string, handler: RouteHandler, type: RouteType = null) {
if (!this.routes_.DELETE) { this.routes_.DELETE = {}; }
this.routes_.DELETE[path] = handler;
this.routes_.DELETE[path] = { handler, type };
}
public put(path: string, handler: RouteHandler) {
public put(path: string, handler: RouteHandler, type: RouteType = null) {
if (!this.routes_.PUT) { this.routes_.PUT = {}; }
this.routes_.PUT[path] = handler;
this.routes_.PUT[path] = { handler, type };
}
}

View File

@ -26,8 +26,8 @@ export class ErrorMethodNotAllowed extends ApiError {
export class ErrorNotFound extends ApiError {
public static httpCode: number = 404;
public constructor(message: string = 'Not Found') {
super(message, ErrorNotFound.httpCode);
public constructor(message: string = 'Not Found', code: string = undefined) {
super(message, ErrorNotFound.httpCode, code);
Object.setPrototypeOf(this, ErrorNotFound.prototype);
}
}

View File

@ -1,3 +1,4 @@
import { baseUrl } from '../config';
import { Item, ItemAddressingType } from '../db';
import { ErrorBadRequest, ErrorForbidden, ErrorNotFound } from './errors';
import Router from './Router';
@ -166,14 +167,15 @@ export async function execRequest(routes: Routers, ctx: AppContext) {
const match = findMatchingRoute(ctx.path, routes);
if (!match) throw new ErrorNotFound();
const routeHandler = match.route.findEndPoint(ctx.request.method as HttpMethod, match.subPath.schema);
const endPoint = match.route.findEndPoint(ctx.request.method as HttpMethod, match.subPath.schema);
if (ctx.URL.origin !== baseUrl(endPoint.type)) throw new ErrorNotFound('Invalid origin', 'invalidOrigin');
// This is a generic catch-all for all private end points - if we
// couldn't get a valid session, we exit now. Individual end points
// might have additional permission checks depending on the action.
if (!match.route.isPublic(match.subPath.schema) && !ctx.owner) throw new ErrorForbidden();
return routeHandler(match.subPath, ctx);
return endPoint.handler(match.subPath, ctx);
}
// In a path such as "/api/files/SOME_ID/content" we want to find:

View File

@ -67,6 +67,8 @@ export interface Config {
logDir: string;
tempDir: string;
baseUrl: string;
apiBaseUrl: string;
userContentBaseUrl: string;
database: DatabaseConfig;
mailer: MailerConfig;
}
@ -79,4 +81,10 @@ export enum HttpMethod {
HEAD = 'HEAD',
}
export enum RouteType {
Web = 1,
Api = 2,
UserContent = 3,
}
export type KoaNext = ()=> Promise<void>;