1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-01-11 18:24:43 +02:00

Server: Moved Joplin-specific context properties under its own namespace

This commit is contained in:
Laurent Cozic 2021-07-02 18:53:45 +01:00
parent 8e789ee2ee
commit bfa7ea7871
32 changed files with 199 additions and 171 deletions

View File

@ -202,16 +202,16 @@ async function main() {
delete connectionCheckLogInfo.connection; delete connectionCheckLogInfo.connection;
appLogger().info('Connection check:', connectionCheckLogInfo); appLogger().info('Connection check:', connectionCheckLogInfo);
const appContext = app.context as AppContext; const ctx = app.context as AppContext;
await setupAppContext(appContext, env, connectionCheck.connection, appLogger); await setupAppContext(ctx, env, connectionCheck.connection, appLogger);
await initializeJoplinUtils(config(), appContext.models, appContext.services.mustache); await initializeJoplinUtils(config(), ctx.models, ctx.joplin.services.mustache);
appLogger().info('Migrating database...'); appLogger().info('Migrating database...');
await migrateDb(appContext.db); await migrateDb(ctx.db);
appLogger().info('Starting services...'); appLogger().info('Starting services...');
await startServices(appContext); await startServices(ctx);
appLogger().info(`Call this for testing: \`curl ${config().apiBaseUrl}/api/ping\``); appLogger().info(`Call this for testing: \`curl ${config().apiBaseUrl}/api/ping\``);

View File

@ -26,15 +26,15 @@ describe('notificationHandler', function() {
}); });
{ {
const context = await koaAppContext({ sessionId: session.id }); const ctx = await koaAppContext({ sessionId: session.id });
await notificationHandler(context, koaNext); await notificationHandler(ctx, koaNext);
const notifications: Notification[] = await models().notification().all(); const notifications: Notification[] = await models().notification().all();
expect(notifications.length).toBe(1); expect(notifications.length).toBe(1);
expect(notifications[0].key).toBe('change_admin_password'); expect(notifications[0].key).toBe('change_admin_password');
expect(notifications[0].read).toBe(0); expect(notifications[0].read).toBe(0);
expect(context.notifications.length).toBe(1); expect(ctx.joplin.notifications.length).toBe(1);
} }
{ {
@ -43,15 +43,15 @@ describe('notificationHandler', function() {
password: 'changed!', password: 'changed!',
}); });
const context = await koaAppContext({ sessionId: session.id }); const ctx = await koaAppContext({ sessionId: session.id });
await notificationHandler(context, koaNext); await notificationHandler(ctx, koaNext);
const notifications: Notification[] = await models().notification().all(); const notifications: Notification[] = await models().notification().all();
expect(notifications.length).toBe(1); expect(notifications.length).toBe(1);
expect(notifications[0].key).toBe('change_admin_password'); expect(notifications[0].key).toBe('change_admin_password');
expect(notifications[0].read).toBe(1); expect(notifications[0].read).toBe(1);
expect(context.notifications.length).toBe(0); expect(ctx.joplin.notifications.length).toBe(0);
} }
}); });

View File

@ -10,31 +10,31 @@ import { NotificationKey } from '../models/NotificationModel';
const logger = Logger.create('notificationHandler'); const logger = Logger.create('notificationHandler');
async function handleChangeAdminPasswordNotification(ctx: AppContext) { async function handleChangeAdminPasswordNotification(ctx: AppContext) {
if (!ctx.owner.is_admin) return; if (!ctx.joplin.owner.is_admin) return;
const defaultAdmin = await ctx.models.user().login(defaultAdminEmail, defaultAdminPassword); const defaultAdmin = await ctx.joplin.models.user().login(defaultAdminEmail, defaultAdminPassword);
const notificationModel = ctx.models.notification(); const notificationModel = ctx.joplin.models.notification();
if (defaultAdmin) { if (defaultAdmin) {
await notificationModel.add( await notificationModel.add(
ctx.owner.id, ctx.joplin.owner.id,
NotificationKey.ChangeAdminPassword, NotificationKey.ChangeAdminPassword,
NotificationLevel.Important, NotificationLevel.Important,
_('The default admin password is insecure and has not been changed! [Change it now](%s)', ctx.models.user().profileUrl()) _('The default admin password is insecure and has not been changed! [Change it now](%s)', ctx.joplin.models.user().profileUrl())
); );
} else { } else {
await notificationModel.markAsRead(ctx.owner.id, NotificationKey.ChangeAdminPassword); await notificationModel.markAsRead(ctx.joplin.owner.id, NotificationKey.ChangeAdminPassword);
} }
} }
async function handleSqliteInProdNotification(ctx: AppContext) { async function handleSqliteInProdNotification(ctx: AppContext) {
if (!ctx.owner.is_admin) return; if (!ctx.joplin.owner.is_admin) return;
const notificationModel = ctx.models.notification(); const notificationModel = ctx.joplin.models.notification();
if (config().database.client === 'sqlite3' && ctx.env === 'prod') { if (config().database.client === 'sqlite3' && ctx.joplin.env === 'prod') {
await notificationModel.add( await notificationModel.add(
ctx.owner.id, ctx.joplin.owner.id,
NotificationKey.UsingSqliteInProd NotificationKey.UsingSqliteInProd
); );
} }
@ -43,8 +43,8 @@ async function handleSqliteInProdNotification(ctx: AppContext) {
async function makeNotificationViews(ctx: AppContext): Promise<NotificationView[]> { async function makeNotificationViews(ctx: AppContext): Promise<NotificationView[]> {
const markdownIt = new MarkdownIt(); const markdownIt = new MarkdownIt();
const notificationModel = ctx.models.notification(); const notificationModel = ctx.joplin.models.notification();
const notifications = await notificationModel.allUnreadByUserId(ctx.owner.id); const notifications = await notificationModel.allUnreadByUserId(ctx.joplin.owner.id);
const views: NotificationView[] = []; const views: NotificationView[] = [];
for (const n of notifications) { for (const n of notifications) {
views.push({ views.push({
@ -62,15 +62,15 @@ async function makeNotificationViews(ctx: AppContext): Promise<NotificationView[
// notifications for any issue it finds. It is only active for logged in users // notifications for any issue it finds. It is only active for logged in users
// on the website. It is inactive for API calls. // on the website. It is inactive for API calls.
export default async function(ctx: AppContext, next: KoaNext): Promise<void> { export default async function(ctx: AppContext, next: KoaNext): Promise<void> {
ctx.notifications = []; ctx.joplin.notifications = [];
try { try {
if (isApiRequest(ctx)) return next(); if (isApiRequest(ctx)) return next();
if (!ctx.owner) return next(); if (!ctx.joplin.owner) return next();
await handleChangeAdminPasswordNotification(ctx); await handleChangeAdminPasswordNotification(ctx);
await handleSqliteInProdNotification(ctx); await handleSqliteInProdNotification(ctx);
ctx.notifications = await makeNotificationViews(ctx); ctx.joplin.notifications = await makeNotificationViews(ctx);
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
} }

View File

@ -22,12 +22,12 @@ describe('ownerHandler', function() {
sessionId: session.id, sessionId: session.id,
}); });
context.owner = null; context.joplin.owner = null;
await ownerHandler(context, koaNext); await ownerHandler(context, koaNext);
expect(!!context.owner).toBe(true); expect(!!context.joplin.owner).toBe(true);
expect(context.owner.id).toBe(user.id); expect(context.joplin.owner.id).toBe(user.id);
}); });
test('should not login user with invalid session ID', async function() { test('should not login user with invalid session ID', async function() {
@ -37,11 +37,11 @@ describe('ownerHandler', function() {
sessionId: 'ihack', sessionId: 'ihack',
}); });
context.owner = null; context.joplin.owner = null;
await ownerHandler(context, koaNext); await ownerHandler(context, koaNext);
expect(!!context.owner).toBe(false); expect(!!context.joplin.owner).toBe(false);
}); });
}); });

View File

@ -3,6 +3,6 @@ import { contextSessionId } from '../utils/requestUtils';
export default async function(ctx: AppContext, next: KoaNext): Promise<void> { export default async function(ctx: AppContext, next: KoaNext): Promise<void> {
const sessionId = contextSessionId(ctx, false); const sessionId = contextSessionId(ctx, false);
if (sessionId) ctx.owner = await ctx.models.session().sessionUser(sessionId); if (sessionId) ctx.joplin.owner = await ctx.joplin.models.session().sessionUser(sessionId);
return next(); return next();
} }

View File

@ -7,17 +7,17 @@ export default async function(ctx: AppContext) {
const requestStartTime = Date.now(); const requestStartTime = Date.now();
try { try {
const responseObject = await execRequest(ctx.routes, ctx); const responseObject = await execRequest(ctx.joplin.routes, ctx);
if (responseObject instanceof Response) { if (responseObject instanceof Response) {
ctx.response = responseObject.response; ctx.response = responseObject.response;
} else if (isView(responseObject)) { } else if (isView(responseObject)) {
const view = responseObject as View; const view = responseObject as View;
ctx.response.status = view?.content?.error ? view?.content?.error?.httpCode || 500 : 200; ctx.response.status = view?.content?.error ? view?.content?.error?.httpCode || 500 : 200;
ctx.response.body = await ctx.services.mustache.renderView(view, { ctx.response.body = await ctx.joplin.services.mustache.renderView(view, {
notifications: ctx.notifications || [], notifications: ctx.joplin.notifications || [],
hasNotifications: !!ctx.notifications && !!ctx.notifications.length, hasNotifications: !!ctx.joplin.notifications && !!ctx.joplin.notifications.length,
owner: ctx.owner, owner: ctx.joplin.owner,
}); });
} else { } else {
ctx.response.status = 200; ctx.response.status = 200;
@ -25,9 +25,9 @@ export default async function(ctx: AppContext) {
} }
} catch (error) { } catch (error) {
if (error.httpCode >= 400 && error.httpCode < 500) { if (error.httpCode >= 400 && error.httpCode < 500) {
ctx.appLogger().error(`${error.httpCode}: ` + `${ctx.request.method} ${ctx.path}` + ` : ${error.message}`); ctx.joplin.appLogger().error(`${error.httpCode}: ` + `${ctx.request.method} ${ctx.path}` + ` : ${error.message}`);
} else { } else {
ctx.appLogger().error(error); ctx.joplin.appLogger().error(error);
} }
// Uncomment this when getting HTML blobs as errors while running tests. // Uncomment this when getting HTML blobs as errors while running tests.
@ -47,15 +47,15 @@ export default async function(ctx: AppContext) {
content: { content: {
error, error,
stack: config().showErrorStackTraces ? error.stack : '', stack: config().showErrorStackTraces ? error.stack : '',
owner: ctx.owner, owner: ctx.joplin.owner,
}, },
title: 'Error', title: 'Error',
}; };
ctx.response.body = await ctx.services.mustache.renderView(view); ctx.response.body = await ctx.joplin.services.mustache.renderView(view);
} else { // JSON } else { // JSON
ctx.response.set('Content-Type', 'application/json'); ctx.response.set('Content-Type', 'application/json');
const r: any = { error: error.message }; const r: any = { error: error.message };
if (ctx.env === Env.Dev && error.stack) r.stack = error.stack; if (ctx.joplin.env === Env.Dev && error.stack) r.stack = error.stack;
if (error.code) r.code = error.code; if (error.code) r.code = error.code;
ctx.response.body = r; ctx.response.body = r;
} }
@ -63,6 +63,6 @@ export default async function(ctx: AppContext) {
// Technically this is not the total request duration because there are // Technically this is not the total request duration because there are
// other middlewares but that should give a good approximation // other middlewares but that should give a good approximation
const requestDuration = Date.now() - requestStartTime; const requestDuration = Date.now() - requestStartTime;
ctx.appLogger().info(`${ctx.request.method} ${ctx.path} (${requestDuration}ms)`); ctx.joplin.appLogger().info(`${ctx.request.method} ${ctx.path} (${requestDuration}ms)`);
} }
} }

View File

@ -44,13 +44,16 @@ function createSubRequestContext(ctx: AppContext, subRequest: SubRequest): AppCo
...subRequest.headers, ...subRequest.headers,
}, },
body: subRequest.body, body: subRequest.body,
appLogger: ctx.appLogger, joplin: {
...ctx.joplin,
appLogger: ctx.joplin.appLogger,
services: ctx.joplin.services,
db: ctx.joplin.db,
models: ctx.joplin.models,
routes: ctx.joplin.routes,
},
path: `/${subRequest.url}`, path: `/${subRequest.url}`,
url: fullUrl, url: fullUrl,
services: ctx.services,
db: ctx.db,
models: ctx.models,
routes: ctx.routes,
}; };
return newContext; return newContext;

View File

@ -20,7 +20,7 @@ router.post('api/debug', async (_path: SubPath, ctx: AppContext) => {
console.info(`Action: ${query.action}`); console.info(`Action: ${query.action}`);
if (query.action === 'createTestUsers') { if (query.action === 'createTestUsers') {
await createTestUsers(ctx.db, config()); await createTestUsers(ctx.joplin.db, config());
} }
}); });

View File

@ -11,7 +11,7 @@ interface Event {
const supportedEvents: Record<string, Function> = { const supportedEvents: Record<string, Function> = {
syncStart: async (_ctx: AppContext) => { syncStart: async (_ctx: AppContext) => {
// await ctx.models.share().updateSharedItems2(ctx.owner.id); // await ctx.joplin.models.share().updateSharedItems2(ctx.joplin.owner.id);
}, },
}; };

View File

@ -17,7 +17,7 @@ const router = new Router(RouteType.Api);
const batchMaxSize = 1 * MB; const batchMaxSize = 1 * MB;
export async function putItemContents(path: SubPath, ctx: AppContext, isBatch: boolean) { export async function putItemContents(path: SubPath, ctx: AppContext, isBatch: boolean) {
if (!ctx.owner.can_upload) throw new ErrorForbidden('Uploading content is disabled'); if (!ctx.joplin.owner.can_upload) throw new ErrorForbidden('Uploading content is disabled');
const parsedBody = await formParse(ctx.req); const parsedBody = await formParse(ctx.req);
const bodyFields = parsedBody.fields; const bodyFields = parsedBody.fields;
@ -49,12 +49,12 @@ export async function putItemContents(path: SubPath, ctx: AppContext, isBatch: b
// query parameter. // query parameter.
if (ctx.query['share_id']) { if (ctx.query['share_id']) {
saveOptions.shareId = ctx.query['share_id']; saveOptions.shareId = ctx.query['share_id'];
await ctx.models.item().checkIfAllowed(ctx.owner, AclAction.Create, { jop_share_id: saveOptions.shareId }); await ctx.joplin.models.item().checkIfAllowed(ctx.joplin.owner, AclAction.Create, { jop_share_id: saveOptions.shareId });
} }
items = [ items = [
{ {
name: ctx.models.item().pathToName(path.id), name: ctx.joplin.models.item().pathToName(path.id),
body: buffer, body: buffer,
}, },
]; ];
@ -63,9 +63,9 @@ export async function putItemContents(path: SubPath, ctx: AppContext, isBatch: b
} }
} }
const output = await ctx.models.item().saveFromRawContent(ctx.owner, items, saveOptions); const output = await ctx.joplin.models.item().saveFromRawContent(ctx.joplin.owner, items, saveOptions);
for (const [name] of Object.entries(output)) { for (const [name] of Object.entries(output)) {
if (output[name].item) output[name].item = ctx.models.item().toApiOutput(output[name].item) as Item; if (output[name].item) output[name].item = ctx.joplin.models.item().toApiOutput(output[name].item) as Item;
} }
return output; return output;
} }
@ -89,8 +89,8 @@ async function itemFromPath(userId: Uuid, itemModel: ItemModel, path: SubPath, m
} }
router.get('api/items/:id', async (path: SubPath, ctx: AppContext) => { router.get('api/items/:id', async (path: SubPath, ctx: AppContext) => {
const itemModel = ctx.models.item(); const itemModel = ctx.joplin.models.item();
const item = await itemFromPath(ctx.owner.id, itemModel, path); const item = await itemFromPath(ctx.joplin.owner.id, itemModel, path);
return itemModel.toApiOutput(item); return itemModel.toApiOutput(item);
}); });
@ -99,12 +99,12 @@ router.del('api/items/:id', async (path: SubPath, ctx: AppContext) => {
if (path.id === 'root' || path.id === 'root:/:') { if (path.id === 'root' || path.id === 'root:/:') {
// We use this for testing only and for safety reasons it's probably // We use this for testing only and for safety reasons it's probably
// best to disable it on production. // best to disable it on production.
if (ctx.env !== 'dev') throw new ErrorMethodNotAllowed('Deleting the root is not allowed'); if (ctx.joplin.env !== 'dev') throw new ErrorMethodNotAllowed('Deleting the root is not allowed');
await ctx.models.item().deleteAll(ctx.owner.id); await ctx.joplin.models.item().deleteAll(ctx.joplin.owner.id);
} else { } else {
const item = await itemFromPath(ctx.owner.id, ctx.models.item(), path); const item = await itemFromPath(ctx.joplin.owner.id, ctx.joplin.models.item(), path);
await ctx.models.item().checkIfAllowed(ctx.owner, AclAction.Delete, item); await ctx.joplin.models.item().checkIfAllowed(ctx.joplin.owner, AclAction.Delete, item);
await ctx.models.item().deleteForUser(ctx.owner.id, item); await ctx.joplin.models.item().deleteForUser(ctx.joplin.owner.id, item);
} }
} catch (error) { } catch (error) {
if (error instanceof ErrorNotFound) { if (error instanceof ErrorNotFound) {
@ -116,8 +116,8 @@ router.del('api/items/:id', async (path: SubPath, ctx: AppContext) => {
}); });
router.get('api/items/:id/content', async (path: SubPath, ctx: AppContext) => { router.get('api/items/:id/content', async (path: SubPath, ctx: AppContext) => {
const itemModel = ctx.models.item(); const itemModel = ctx.joplin.models.item();
const item = await itemFromPath(ctx.owner.id, itemModel, path); const item = await itemFromPath(ctx.joplin.owner.id, itemModel, path);
const serializedContent = await itemModel.serializedContent(item.id); const serializedContent = await itemModel.serializedContent(item.id);
return respondWithItemContent(ctx.response, item, serializedContent); return respondWithItemContent(ctx.response, item, serializedContent);
}); });
@ -130,14 +130,14 @@ router.put('api/items/:id/content', async (path: SubPath, ctx: AppContext) => {
}); });
router.get('api/items/:id/delta', async (_path: SubPath, ctx: AppContext) => { router.get('api/items/:id/delta', async (_path: SubPath, ctx: AppContext) => {
const changeModel = ctx.models.change(); const changeModel = ctx.joplin.models.change();
return changeModel.delta(ctx.owner.id, requestDeltaPagination(ctx.query)); return changeModel.delta(ctx.joplin.owner.id, requestDeltaPagination(ctx.query));
}); });
router.get('api/items/:id/children', async (path: SubPath, ctx: AppContext) => { router.get('api/items/:id/children', async (path: SubPath, ctx: AppContext) => {
const itemModel = ctx.models.item(); const itemModel = ctx.joplin.models.item();
const parentName = itemModel.pathToName(path.id); const parentName = itemModel.pathToName(path.id);
const result = await itemModel.children(ctx.owner.id, parentName, requestPagination(ctx.query)); const result = await itemModel.children(ctx.joplin.owner.id, parentName, requestPagination(ctx.query));
return result; return result;
}); });

View File

@ -12,10 +12,10 @@ router.public = true;
router.post('api/sessions', async (_path: SubPath, ctx: AppContext) => { router.post('api/sessions', async (_path: SubPath, ctx: AppContext) => {
const fields: User = await bodyFields(ctx.req); const fields: User = await bodyFields(ctx.req);
const user = await ctx.models.user().login(fields.email, fields.password); const user = await ctx.joplin.models.user().login(fields.email, fields.password);
if (!user) throw new ErrorForbidden('Invalid username or password'); if (!user) throw new ErrorForbidden('Invalid username or password');
const session = await ctx.models.session().createUserSession(user.id); const session = await ctx.joplin.models.session().createUserSession(user.id);
return { id: session.id, user_id: session.user_id }; return { id: session.id, user_id: session.user_id };
}); });

View File

@ -9,11 +9,11 @@ import { AclAction } from '../../models/BaseModel';
const router = new Router(RouteType.Api); const router = new Router(RouteType.Api);
router.patch('api/share_users/:id', async (path: SubPath, ctx: AppContext) => { router.patch('api/share_users/:id', async (path: SubPath, ctx: AppContext) => {
const shareUserModel = ctx.models.shareUser(); const shareUserModel = ctx.joplin.models.shareUser();
const shareUser = await shareUserModel.load(path.id); const shareUser = await shareUserModel.load(path.id);
if (!shareUser) throw new ErrorNotFound(); if (!shareUser) throw new ErrorNotFound();
await shareUserModel.checkIfAllowed(ctx.owner, AclAction.Update, shareUser); await shareUserModel.checkIfAllowed(ctx.joplin.owner, AclAction.Update, shareUser);
const body = await bodyFields<any>(ctx.req); const body = await bodyFields<any>(ctx.req);
@ -25,20 +25,20 @@ router.patch('api/share_users/:id', async (path: SubPath, ctx: AppContext) => {
}); });
router.del('api/share_users/:id', async (path: SubPath, ctx: AppContext) => { router.del('api/share_users/:id', async (path: SubPath, ctx: AppContext) => {
const shareUser = await ctx.models.shareUser().load(path.id); const shareUser = await ctx.joplin.models.shareUser().load(path.id);
if (!shareUser) throw new ErrorNotFound(); if (!shareUser) throw new ErrorNotFound();
await ctx.models.shareUser().checkIfAllowed(ctx.owner, AclAction.Delete, shareUser); await ctx.joplin.models.shareUser().checkIfAllowed(ctx.joplin.owner, AclAction.Delete, shareUser);
await ctx.models.shareUser().delete(shareUser.id); await ctx.joplin.models.shareUser().delete(shareUser.id);
}); });
router.get('api/share_users', async (_path: SubPath, ctx: AppContext) => { router.get('api/share_users', async (_path: SubPath, ctx: AppContext) => {
const shareUsers = await ctx.models.shareUser().byUserId(ctx.owner.id); const shareUsers = await ctx.joplin.models.shareUser().byUserId(ctx.joplin.owner.id);
const items: any[] = []; const items: any[] = [];
for (const su of shareUsers) { for (const su of shareUsers) {
const share = await ctx.models.share().load(su.share_id); const share = await ctx.joplin.models.share().load(su.share_id);
const sharer = await ctx.models.user().load(share.owner_id); const sharer = await ctx.joplin.models.user().load(share.owner_id);
items.push({ items.push({
id: su.id, id: su.id,

View File

@ -19,7 +19,7 @@ router.public = true;
router.post('api/shares', async (_path: SubPath, ctx: AppContext) => { router.post('api/shares', async (_path: SubPath, ctx: AppContext) => {
ownerRequired(ctx); ownerRequired(ctx);
const shareModel = ctx.models.share(); const shareModel = ctx.joplin.models.share();
const fields = await bodyFields<any>(ctx.req); const fields = await bodyFields<any>(ctx.req);
const shareInput: ShareApiInput = shareModel.fromApiInput(fields) as ShareApiInput; const shareInput: ShareApiInput = shareModel.fromApiInput(fields) as ShareApiInput;
if (fields.folder_id) shareInput.folder_id = fields.folder_id; if (fields.folder_id) shareInput.folder_id = fields.folder_id;
@ -31,9 +31,9 @@ router.post('api/shares', async (_path: SubPath, ctx: AppContext) => {
// - Additionally, the App method is available, but not exposed via the API. // - Additionally, the App method is available, but not exposed via the API.
if (shareInput.folder_id) { if (shareInput.folder_id) {
return ctx.models.share().shareFolder(ctx.owner, shareInput.folder_id); return ctx.joplin.models.share().shareFolder(ctx.joplin.owner, shareInput.folder_id);
} else if (shareInput.note_id) { } else if (shareInput.note_id) {
return ctx.models.share().shareNote(ctx.owner, shareInput.note_id); return ctx.joplin.models.share().shareNote(ctx.joplin.owner, shareInput.note_id);
} else { } else {
throw new ErrorBadRequest('Either folder_id or note_id must be provided'); throw new ErrorBadRequest('Either folder_id or note_id must be provided');
} }
@ -47,28 +47,28 @@ router.post('api/shares/:id/users', async (path: SubPath, ctx: AppContext) => {
} }
const fields = await bodyFields(ctx.req) as UserInput; const fields = await bodyFields(ctx.req) as UserInput;
const user = await ctx.models.user().loadByEmail(fields.email); const user = await ctx.joplin.models.user().loadByEmail(fields.email);
if (!user) throw new ErrorNotFound('User not found'); if (!user) throw new ErrorNotFound('User not found');
const shareId = path.id; const shareId = path.id;
await ctx.models.shareUser().checkIfAllowed(ctx.owner, AclAction.Create, { await ctx.joplin.models.shareUser().checkIfAllowed(ctx.joplin.owner, AclAction.Create, {
share_id: shareId, share_id: shareId,
user_id: user.id, user_id: user.id,
}); });
return ctx.models.shareUser().addByEmail(shareId, user.email); return ctx.joplin.models.shareUser().addByEmail(shareId, user.email);
}); });
router.get('api/shares/:id/users', async (path: SubPath, ctx: AppContext) => { router.get('api/shares/:id/users', async (path: SubPath, ctx: AppContext) => {
ownerRequired(ctx); ownerRequired(ctx);
const shareId = path.id; const shareId = path.id;
const share = await ctx.models.share().load(shareId); const share = await ctx.joplin.models.share().load(shareId);
await ctx.models.share().checkIfAllowed(ctx.owner, AclAction.Read, share); await ctx.joplin.models.share().checkIfAllowed(ctx.joplin.owner, AclAction.Read, share);
const shareUsers = await ctx.models.shareUser().byShareId(shareId, null); const shareUsers = await ctx.joplin.models.shareUser().byShareId(shareId, null);
const users = await ctx.models.user().loadByIds(shareUsers.map(su => su.user_id)); const users = await ctx.joplin.models.user().loadByIds(shareUsers.map(su => su.user_id));
const items = shareUsers.map(su => { const items = shareUsers.map(su => {
const user = users.find(u => u.id === su.user_id); const user = users.find(u => u.id === su.user_id);
@ -90,7 +90,7 @@ router.get('api/shares/:id/users', async (path: SubPath, ctx: AppContext) => {
}); });
router.get('api/shares/:id', async (path: SubPath, ctx: AppContext) => { router.get('api/shares/:id', async (path: SubPath, ctx: AppContext) => {
const shareModel = ctx.models.share(); const shareModel = ctx.joplin.models.share();
const share = await shareModel.load(path.id); const share = await shareModel.load(path.id);
if (share && share.type === ShareType.Note) { if (share && share.type === ShareType.Note) {
@ -105,7 +105,7 @@ router.get('api/shares/:id', async (path: SubPath, ctx: AppContext) => {
router.get('api/shares', async (_path: SubPath, ctx: AppContext) => { router.get('api/shares', async (_path: SubPath, ctx: AppContext) => {
ownerRequired(ctx); ownerRequired(ctx);
const shares = ctx.models.share().toApiOutput(await ctx.models.share().sharesByUser(ctx.owner.id)) as Share[]; const shares = ctx.joplin.models.share().toApiOutput(await ctx.joplin.models.share().sharesByUser(ctx.joplin.owner.id)) as Share[];
// Fake paginated results so that it can be added later on, if needed. // Fake paginated results so that it can be added later on, if needed.
return { return {
items: shares.map(share => { items: shares.map(share => {
@ -123,9 +123,9 @@ router.get('api/shares', async (_path: SubPath, ctx: AppContext) => {
router.del('api/shares/:id', async (path: SubPath, ctx: AppContext) => { router.del('api/shares/:id', async (path: SubPath, ctx: AppContext) => {
ownerRequired(ctx); ownerRequired(ctx);
const share = await ctx.models.share().load(path.id); const share = await ctx.joplin.models.share().load(path.id);
await ctx.models.share().checkIfAllowed(ctx.owner, AclAction.Delete, share); await ctx.joplin.models.share().checkIfAllowed(ctx.joplin.owner, AclAction.Delete, share);
await ctx.models.share().delete(share.id); await ctx.joplin.models.share().delete(share.id);
}); });
export default router; export default router;

View File

@ -11,23 +11,23 @@ import uuidgen from '../../utils/uuidgen';
const router = new Router(RouteType.Api); const router = new Router(RouteType.Api);
async function fetchUser(path: SubPath, ctx: AppContext): Promise<User> { async function fetchUser(path: SubPath, ctx: AppContext): Promise<User> {
const user = await ctx.models.user().load(path.id); const user = await ctx.joplin.models.user().load(path.id);
if (!user) throw new ErrorNotFound(`No user with ID ${path.id}`); if (!user) throw new ErrorNotFound(`No user with ID ${path.id}`);
return user; return user;
} }
async function postedUserFromContext(ctx: AppContext): Promise<User> { async function postedUserFromContext(ctx: AppContext): Promise<User> {
return ctx.models.user().fromApiInput(await bodyFields<any>(ctx.req)); return ctx.joplin.models.user().fromApiInput(await bodyFields<any>(ctx.req));
} }
router.get('api/users/:id', async (path: SubPath, ctx: AppContext) => { router.get('api/users/:id', async (path: SubPath, ctx: AppContext) => {
const user = await fetchUser(path, ctx); const user = await fetchUser(path, ctx);
await ctx.models.user().checkIfAllowed(ctx.owner, AclAction.Read, user); await ctx.joplin.models.user().checkIfAllowed(ctx.joplin.owner, AclAction.Read, user);
return user; return user;
}); });
router.post('api/users', async (_path: SubPath, ctx: AppContext) => { router.post('api/users', async (_path: SubPath, ctx: AppContext) => {
await ctx.models.user().checkIfAllowed(ctx.owner, AclAction.Create); await ctx.joplin.models.user().checkIfAllowed(ctx.joplin.owner, AclAction.Create);
const user = await postedUserFromContext(ctx); const user = await postedUserFromContext(ctx);
// We set a random password because it's required, but user will have to // We set a random password because it's required, but user will have to
@ -35,30 +35,30 @@ router.post('api/users', async (_path: SubPath, ctx: AppContext) => {
user.password = uuidgen(); user.password = uuidgen();
user.must_set_password = 1; user.must_set_password = 1;
user.email_confirmed = 0; user.email_confirmed = 0;
const output = await ctx.models.user().save(user); const output = await ctx.joplin.models.user().save(user);
return ctx.models.user().toApiOutput(output); return ctx.joplin.models.user().toApiOutput(output);
}); });
router.get('api/users', async (_path: SubPath, ctx: AppContext) => { router.get('api/users', async (_path: SubPath, ctx: AppContext) => {
await ctx.models.user().checkIfAllowed(ctx.owner, AclAction.List); await ctx.joplin.models.user().checkIfAllowed(ctx.joplin.owner, AclAction.List);
return { return {
items: await ctx.models.user().all(), items: await ctx.joplin.models.user().all(),
has_more: false, has_more: false,
}; };
}); });
router.del('api/users/:id', async (path: SubPath, ctx: AppContext) => { router.del('api/users/:id', async (path: SubPath, ctx: AppContext) => {
const user = await fetchUser(path, ctx); const user = await fetchUser(path, ctx);
await ctx.models.user().checkIfAllowed(ctx.owner, AclAction.Delete, user); await ctx.joplin.models.user().checkIfAllowed(ctx.joplin.owner, AclAction.Delete, user);
await ctx.models.user().delete(user.id); await ctx.joplin.models.user().delete(user.id);
}); });
router.patch('api/users/:id', async (path: SubPath, ctx: AppContext) => { router.patch('api/users/:id', async (path: SubPath, ctx: AppContext) => {
const user = await fetchUser(path, ctx); const user = await fetchUser(path, ctx);
await ctx.models.user().checkIfAllowed(ctx.owner, AclAction.Update, user); await ctx.joplin.models.user().checkIfAllowed(ctx.joplin.owner, AclAction.Update, user);
const postedUser = await postedUserFromContext(ctx); const postedUser = await postedUserFromContext(ctx);
await ctx.models.user().save({ id: user.id, ...postedUser }); await ctx.joplin.models.user().save({ id: user.id, ...postedUser });
}); });
export default router; export default router;

View File

@ -53,7 +53,7 @@ router.public = true;
router.get('', async (path: SubPath, ctx: AppContext) => { router.get('', async (path: SubPath, ctx: AppContext) => {
// Redirect to either /login or /home when trying to access the root // Redirect to either /login or /home when trying to access the root
if (!path.id && !path.link) { if (!path.id && !path.link) {
if (ctx.owner) { if (ctx.joplin.owner) {
return redirect(ctx, 'home'); return redirect(ctx, 'home');
} else { } else {
return redirect(ctx, 'login'); return redirect(ctx, 'login');

View File

@ -14,11 +14,11 @@ const router = new Router(RouteType.Web);
router.get('changes', async (_path: SubPath, ctx: AppContext) => { router.get('changes', async (_path: SubPath, ctx: AppContext) => {
const pagination = makeTablePagination(ctx.query, 'updated_time', PaginationOrderDir.DESC); const pagination = makeTablePagination(ctx.query, 'updated_time', PaginationOrderDir.DESC);
const paginatedChanges = await ctx.models.change().allByUser(ctx.owner.id, pagination); const paginatedChanges = await ctx.joplin.models.change().allByUser(ctx.joplin.owner.id, pagination);
const items = await ctx.models.item().loadByIds(paginatedChanges.items.map(i => i.item_id), { fields: ['id'] }); const items = await ctx.joplin.models.item().loadByIds(paginatedChanges.items.map(i => i.item_id), { fields: ['id'] });
const table: Table = { const table: Table = {
baseUrl: ctx.models.change().changeUrl(), baseUrl: ctx.joplin.models.change().changeUrl(),
requestQuery: ctx.query, requestQuery: ctx.query,
pageCount: paginatedChanges.page_count, pageCount: paginatedChanges.page_count,
pagination, pagination,
@ -42,7 +42,7 @@ router.get('changes', async (_path: SubPath, ctx: AppContext) => {
{ {
value: change.item_name, value: change.item_name,
stretch: true, stretch: true,
url: showItemUrls(config()) ? (items.find(i => i.id === change.item_id) ? ctx.models.item().itemContentUrl(change.item_id) : '') : null, url: showItemUrls(config()) ? (items.find(i => i.id === change.item_id) ? ctx.joplin.models.item().itemContentUrl(change.item_id) : '') : null,
}, },
{ {
value: changeTypeToString(change.type), value: changeTypeToString(change.type),

View File

@ -15,7 +15,7 @@ router.get('home', async (_path: SubPath, ctx: AppContext) => {
contextSessionId(ctx); contextSessionId(ctx);
if (ctx.method === 'GET') { if (ctx.method === 'GET') {
const accountProps = accountTypeProperties(ctx.owner.account_type); const accountProps = accountTypeProperties(ctx.joplin.owner.account_type);
const view = defaultView('home', 'Home'); const view = defaultView('home', 'Home');
view.content = { view.content = {
@ -26,7 +26,7 @@ router.get('home', async (_path: SubPath, ctx: AppContext) => {
}, },
{ {
label: 'Is Admin', label: 'Is Admin',
value: yesOrNo(ctx.owner.is_admin), value: yesOrNo(ctx.joplin.owner.is_admin),
}, },
{ {
label: 'Max Item Size', label: 'Max Item Size',

View File

@ -16,12 +16,12 @@ const router = new Router(RouteType.Web);
router.get('items', async (_path: SubPath, ctx: AppContext) => { router.get('items', async (_path: SubPath, ctx: AppContext) => {
const pagination = makeTablePagination(ctx.query, 'name', PaginationOrderDir.ASC); const pagination = makeTablePagination(ctx.query, 'name', PaginationOrderDir.ASC);
const paginatedItems = await ctx.models.item().children(ctx.owner.id, '', pagination, { fields: ['id', 'name', 'updated_time', 'mime_type', 'content_size'] }); const paginatedItems = await ctx.joplin.models.item().children(ctx.joplin.owner.id, '', pagination, { fields: ['id', 'name', 'updated_time', 'mime_type', 'content_size'] });
const table: Table = { const table: Table = {
baseUrl: ctx.models.item().itemUrl(), baseUrl: ctx.joplin.models.item().itemUrl(),
requestQuery: ctx.query, requestQuery: ctx.query,
pageCount: Math.ceil((await ctx.models.item().childrenCount(ctx.owner.id, '')) / pagination.limit), pageCount: Math.ceil((await ctx.joplin.models.item().childrenCount(ctx.joplin.owner.id, '')) / pagination.limit),
pagination, pagination,
headers: [ headers: [
{ {
@ -72,7 +72,7 @@ router.get('items', async (_path: SubPath, ctx: AppContext) => {
}); });
router.get('items/:id/content', async (path: SubPath, ctx: AppContext) => { router.get('items/:id/content', async (path: SubPath, ctx: AppContext) => {
const itemModel = ctx.models.item(); const itemModel = ctx.joplin.models.item();
const item = await itemModel.loadWithContent(path.id); const item = await itemModel.loadWithContent(path.id);
if (!item) throw new ErrorNotFound(); if (!item) throw new ErrorNotFound();
return respondWithItemContent(ctx.response, item, item.content); return respondWithItemContent(ctx.response, item, item.content);
@ -83,13 +83,13 @@ router.post('items', async (_path: SubPath, ctx: AppContext) => {
const fields = body.fields; const fields = body.fields;
if (fields.delete_all_button) { if (fields.delete_all_button) {
const itemModel = ctx.models.item(); const itemModel = ctx.joplin.models.item();
await itemModel.deleteAll(ctx.owner.id); await itemModel.deleteAll(ctx.joplin.owner.id);
} else { } else {
throw new Error('Invalid form button'); throw new Error('Invalid form button');
} }
return redirect(ctx, await ctx.models.item().itemUrl()); return redirect(ctx, await ctx.joplin.models.item().itemUrl());
}); });
export default router; export default router;

View File

@ -28,7 +28,7 @@ router.post('login', async (_path: SubPath, ctx: AppContext) => {
try { try {
const body = await formParse(ctx.req); const body = await formParse(ctx.req);
const session = await ctx.models.session().authenticate(body.fields.email, body.fields.password); const session = await ctx.joplin.models.session().authenticate(body.fields.email, body.fields.password);
ctx.cookies.set('sessionId', session.id); ctx.cookies.set('sessionId', session.id);
return redirect(ctx, `${config().baseUrl}/home`); return redirect(ctx, `${config().baseUrl}/home`);
} catch (error) { } catch (error) {

View File

@ -10,7 +10,7 @@ const router = new Router(RouteType.Web);
router.post('logout', async (_path: SubPath, ctx: AppContext) => { router.post('logout', async (_path: SubPath, ctx: AppContext) => {
const sessionId = contextSessionId(ctx, false); const sessionId = contextSessionId(ctx, false);
ctx.cookies.set('sessionId', ''); ctx.cookies.set('sessionId', '');
await ctx.models.session().logout(sessionId); await ctx.joplin.models.session().logout(sessionId);
return redirect(ctx, `${config().baseUrl}/login`); return redirect(ctx, `${config().baseUrl}/login`);
}); });

View File

@ -11,7 +11,7 @@ const router = new Router(RouteType.Web);
router.patch('notifications/:id', async (path: SubPath, ctx: AppContext) => { router.patch('notifications/:id', async (path: SubPath, ctx: AppContext) => {
const fields: Notification = await bodyFields(ctx.req); const fields: Notification = await bodyFields(ctx.req);
const notificationId = path.id; const notificationId = path.id;
const model = ctx.models.notification(); const model = ctx.joplin.models.notification();
const existingNotification = await model.load(notificationId); const existingNotification = await model.load(notificationId);
if (!existingNotification) throw new ErrorNotFound(); if (!existingNotification) throw new ErrorNotFound();

View File

@ -24,19 +24,19 @@ const router: Router = new Router(RouteType.Web);
router.public = true; router.public = true;
router.get('shares/:id', async (path: SubPath, ctx: AppContext) => { router.get('shares/:id', async (path: SubPath, ctx: AppContext) => {
const shareModel = ctx.models.share(); const shareModel = ctx.joplin.models.share();
const share = await shareModel.load(path.id); const share = await shareModel.load(path.id);
if (!share) throw new ErrorNotFound(); if (!share) throw new ErrorNotFound();
const itemModel = ctx.models.item(); const itemModel = ctx.joplin.models.item();
const item = await itemModel.loadWithContent(share.item_id); const item = await itemModel.loadWithContent(share.item_id);
if (!item) throw new ErrorNotFound(); if (!item) throw new ErrorNotFound();
const result = await renderItem(ctx, item, share); const result = await renderItem(ctx, item, share);
ctx.models.share().checkShareUrl(share, ctx.URL.origin); ctx.joplin.models.share().checkShareUrl(share, ctx.URL.origin);
ctx.response.body = result.body; ctx.response.body = result.body;
ctx.response.set('Content-Type', result.mime); ctx.response.set('Content-Type', result.mime);

View File

@ -43,17 +43,17 @@ router.post('signup', async (_path: SubPath, ctx: AppContext) => {
const formUser = await bodyFields<FormUser>(ctx.req); const formUser = await bodyFields<FormUser>(ctx.req);
const password = checkPassword(formUser, true); const password = checkPassword(formUser, true);
const user = await ctx.models.user().save({ const user = await ctx.joplin.models.user().save({
...accountTypeProperties(AccountType.Basic), ...accountTypeProperties(AccountType.Basic),
email: formUser.email, email: formUser.email,
full_name: formUser.full_name, full_name: formUser.full_name,
password, password,
}); });
const session = await ctx.models.session().createUserSession(user.id); const session = await ctx.joplin.models.session().createUserSession(user.id);
ctx.cookies.set('sessionId', session.id); ctx.cookies.set('sessionId', session.id);
await ctx.models.notification().add(user.id, NotificationKey.ConfirmEmail); await ctx.joplin.models.notification().add(user.id, NotificationKey.ConfirmEmail);
return redirect(ctx, `${config().baseUrl}/home`); return redirect(ctx, `${config().baseUrl}/home`);
} catch (error) { } catch (error) {

View File

@ -137,7 +137,7 @@ const postHandlers: Record<string, StripeRouteHandler> = {
const stripeUserId = checkoutSession.customer as string; const stripeUserId = checkoutSession.customer as string;
const stripeSubscriptionId = checkoutSession.subscription as string; const stripeSubscriptionId = checkoutSession.subscription as string;
await ctx.models.subscription().saveUserAndSubscription( await ctx.joplin.models.subscription().saveUserAndSubscription(
checkoutSession.customer_details.email || checkoutSession.customer_email, checkoutSession.customer_details.email || checkoutSession.customer_email,
AccountType.Pro, AccountType.Pro,
stripeUserId, stripeUserId,
@ -158,7 +158,7 @@ const postHandlers: Record<string, StripeRouteHandler> = {
// saved in checkout.session.completed. // saved in checkout.session.completed.
const invoice = event.data.object as Stripe.Invoice; const invoice = event.data.object as Stripe.Invoice;
await ctx.models.subscription().handlePayment(invoice.subscription as string, true); await ctx.joplin.models.subscription().handlePayment(invoice.subscription as string, true);
}, },
'invoice.payment_failed': async () => { 'invoice.payment_failed': async () => {
@ -170,7 +170,7 @@ const postHandlers: Record<string, StripeRouteHandler> = {
const invoice = event.data.object as Stripe.Invoice; const invoice = event.data.object as Stripe.Invoice;
const subId = invoice.subscription as string; const subId = invoice.subscription as string;
await ctx.models.subscription().handlePayment(subId, false); await ctx.joplin.models.subscription().handlePayment(subId, false);
}, },
}; };
@ -202,9 +202,9 @@ const getHandlers: Record<string, StripeRouteHandler> = {
}, },
portal: async (stripe: Stripe, _path: SubPath, ctx: AppContext) => { portal: async (stripe: Stripe, _path: SubPath, ctx: AppContext) => {
if (!ctx.owner) throw new ErrorForbidden('Please login to access the subscription portal'); if (!ctx.joplin.owner) throw new ErrorForbidden('Please login to access the subscription portal');
const sub = await ctx.models.subscription().byUserId(ctx.owner.id); const sub = await ctx.joplin.models.subscription().byUserId(ctx.joplin.owner.id);
if (!sub) throw new ErrorNotFound('Could not find subscription'); if (!sub) throw new ErrorNotFound('Could not find subscription');
const billingPortalSession = await stripe.billingPortal.sessions.create({ customer: sub.stripe_user_id as string }); const billingPortalSession = await stripe.billingPortal.sessions.create({ customer: sub.stripe_user_id as string });

View File

@ -78,8 +78,8 @@ function userIsMe(path: SubPath): boolean {
const router = new Router(RouteType.Web); const router = new Router(RouteType.Web);
router.get('users', async (_path: SubPath, ctx: AppContext) => { router.get('users', async (_path: SubPath, ctx: AppContext) => {
const userModel = ctx.models.user(); const userModel = ctx.joplin.models.user();
await userModel.checkIfAllowed(ctx.owner, AclAction.List); await userModel.checkIfAllowed(ctx.joplin.owner, AclAction.List);
const users = await userModel.all(); const users = await userModel.all();
@ -101,16 +101,16 @@ router.get('users', async (_path: SubPath, ctx: AppContext) => {
}); });
router.get('users/:id', async (path: SubPath, ctx: AppContext, user: User = null, error: any = null) => { router.get('users/:id', async (path: SubPath, ctx: AppContext, user: User = null, error: any = null) => {
const owner = ctx.owner; const owner = ctx.joplin.owner;
const isMe = userIsMe(path); const isMe = userIsMe(path);
const isNew = userIsNew(path); const isNew = userIsNew(path);
const userModel = ctx.models.user(); const userModel = ctx.joplin.models.user();
const userId = userIsMe(path) ? owner.id : path.id; const userId = userIsMe(path) ? owner.id : path.id;
user = !isNew ? user || await userModel.load(userId) : null; user = !isNew ? user || await userModel.load(userId) : null;
if (isNew && !user) user = defaultUser(); if (isNew && !user) user = defaultUser();
await userModel.checkIfAllowed(ctx.owner, AclAction.Read, user); await userModel.checkIfAllowed(ctx.joplin.owner, AclAction.Read, user);
let postUrl = ''; let postUrl = '';
@ -147,9 +147,9 @@ router.publicSchemas.push('users/:id/confirm');
router.get('users/:id/confirm', async (path: SubPath, ctx: AppContext, error: Error = null) => { router.get('users/:id/confirm', async (path: SubPath, ctx: AppContext, error: Error = null) => {
const userId = path.id; const userId = path.id;
const token = ctx.query.token; const token = ctx.query.token;
if (token) await ctx.models.user().confirmEmail(userId, token); if (token) await ctx.joplin.models.user().confirmEmail(userId, token);
const user = await ctx.models.user().load(userId); const user = await ctx.joplin.models.user().load(userId);
if (user.must_set_password) { if (user.must_set_password) {
const view: View = { const view: View = {
@ -158,17 +158,17 @@ router.get('users/:id/confirm', async (path: SubPath, ctx: AppContext, error: Er
user, user,
error, error,
token, token,
postUrl: ctx.models.user().confirmUrl(userId, token), postUrl: ctx.joplin.models.user().confirmUrl(userId, token),
}, },
navbar: false, navbar: false,
}; };
return view; return view;
} else { } else {
await ctx.models.token().deleteByValue(userId, token); await ctx.joplin.models.token().deleteByValue(userId, token);
await ctx.models.notification().add(userId, NotificationKey.EmailConfirmed); await ctx.joplin.models.notification().add(userId, NotificationKey.EmailConfirmed);
if (ctx.owner) { if (ctx.joplin.owner) {
return redirect(ctx, `${config().baseUrl}/home`); return redirect(ctx, `${config().baseUrl}/home`);
} else { } else {
return redirect(ctx, `${config().baseUrl}/login`); return redirect(ctx, `${config().baseUrl}/login`);
@ -187,17 +187,17 @@ router.post('users/:id/confirm', async (path: SubPath, ctx: AppContext) => {
try { try {
const fields = await bodyFields<SetPasswordFormData>(ctx.req); const fields = await bodyFields<SetPasswordFormData>(ctx.req);
await ctx.models.token().checkToken(userId, fields.token); await ctx.joplin.models.token().checkToken(userId, fields.token);
const password = checkPassword(fields, true); const password = checkPassword(fields, true);
await ctx.models.user().save({ id: userId, password, must_set_password: 0 }); await ctx.joplin.models.user().save({ id: userId, password, must_set_password: 0 });
await ctx.models.token().deleteByValue(userId, fields.token); await ctx.joplin.models.token().deleteByValue(userId, fields.token);
const session = await ctx.models.session().createUserSession(userId); const session = await ctx.joplin.models.session().createUserSession(userId);
ctx.cookies.set('sessionId', session.id); ctx.cookies.set('sessionId', session.id);
await ctx.models.notification().add(userId, NotificationKey.PasswordSet); await ctx.joplin.models.notification().add(userId, NotificationKey.PasswordSet);
return redirect(ctx, `${config().baseUrl}/home`); return redirect(ctx, `${config().baseUrl}/home`);
} catch (error) { } catch (error) {
@ -217,7 +217,7 @@ interface FormFields {
router.post('users', async (path: SubPath, ctx: AppContext) => { router.post('users', async (path: SubPath, ctx: AppContext) => {
let user: User = {}; let user: User = {};
const userId = userIsMe(path) ? ctx.owner.id : path.id; const userId = userIsMe(path) ? ctx.joplin.owner.id : path.id;
try { try {
const body = await formParse(ctx.req); const body = await formParse(ctx.req);
@ -226,11 +226,11 @@ router.post('users', async (path: SubPath, ctx: AppContext) => {
if (userIsMe(path)) fields.id = userId; if (userIsMe(path)) fields.id = userId;
user = makeUser(isNew, fields); user = makeUser(isNew, fields);
const userModel = ctx.models.user(); const userModel = ctx.joplin.models.user();
if (fields.post_button) { if (fields.post_button) {
const userToSave: User = userModel.fromApiInput(user); const userToSave: User = userModel.fromApiInput(user);
await userModel.checkIfAllowed(ctx.owner, isNew ? AclAction.Create : AclAction.Update, userToSave); await userModel.checkIfAllowed(ctx.joplin.owner, isNew ? AclAction.Create : AclAction.Update, userToSave);
if (isNew) { if (isNew) {
await userModel.save(userToSave); await userModel.save(userToSave);
@ -239,7 +239,7 @@ router.post('users', async (path: SubPath, ctx: AppContext) => {
} }
} else if (fields.delete_button) { } else if (fields.delete_button) {
const user = await userModel.load(path.id); const user = await userModel.load(path.id);
await userModel.checkIfAllowed(ctx.owner, AclAction.Delete, user); await userModel.checkIfAllowed(ctx.joplin.owner, AclAction.Delete, user);
await userModel.delete(path.id); await userModel.delete(path.id);
} else if (fields.send_reset_password_email) { } else if (fields.send_reset_password_email) {
const user = await userModel.load(path.id); const user = await userModel.load(path.id);

View File

@ -10,7 +10,7 @@ interface RouteInfo {
export default class Router { export default class Router {
// When the router is public, we do not check that a valid session is // When the router is public, we do not check that a valid session is
// available (that ctx.owner is defined). It means by default any user, even // available (that ctx.joplin.owner is defined). It means by default any user, even
// not logged in, can access any route of this router. End points that // not logged in, can access any route of this router. End points that
// should not be publicly available should call ownerRequired(ctx); // should not be publicly available should call ownerRequired(ctx);
public public: boolean = false; public public: boolean = false;

View File

@ -43,7 +43,7 @@ export async function bodyFields<T>(req: any/* , filter:string[] = null*/): Prom
} }
export function ownerRequired(ctx: AppContext) { export function ownerRequired(ctx: AppContext) {
if (!ctx.owner) throw new ErrorForbidden(); if (!ctx.joplin.owner) throw new ErrorForbidden();
} }
export function headerSessionId(headers: any): string { export function headerSessionId(headers: any): string {

View File

@ -191,7 +191,7 @@ export async function execRequest(routes: Routers, ctx: AppContext) {
// This is a generic catch-all for all private end points - if we // 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 // couldn't get a valid session, we exit now. Individual end points
// might have additional permission checks depending on the action. // might have additional permission checks depending on the action.
if (!match.route.isPublic(match.subPath.schema) && !ctx.owner) throw new ErrorForbidden(); if (!match.route.isPublic(match.subPath.schema) && !ctx.joplin.owner) throw new ErrorForbidden();
return endPoint.handler(match.subPath, ctx); return endPoint.handler(match.subPath, ctx);
} }

View File

@ -24,14 +24,19 @@ async function setupServices(env: Env, models: Models, config: Config): Promise<
} }
export default async function(appContext: AppContext, env: Env, dbConnection: DbConnection, appLogger: ()=> LoggerWrapper): Promise<AppContext> { export default async function(appContext: AppContext, env: Env, dbConnection: DbConnection, appLogger: ()=> LoggerWrapper): Promise<AppContext> {
appContext.env = env; const models = newModelFactory(dbConnection, config());
appContext.db = dbConnection;
appContext.models = newModelFactory(appContext.db, config());
appContext.services = await setupServices(env, appContext.models, config());
appContext.appLogger = appLogger;
appContext.routes = { ...routes };
if (env === Env.Prod) delete appContext.routes['api/debug']; appContext.joplin = {
...appContext.joplin,
env: env,
db: dbConnection,
models: models,
services: await setupServices(env, models, config()),
appLogger: appLogger,
routes: { ...routes },
};
if (env === Env.Prod) delete appContext.joplin.routes['api/debug'];
return appContext; return appContext;
} }

View File

@ -1,7 +1,7 @@
import { AppContext } from './types'; import { AppContext } from './types';
export default async function startServices(appContext: AppContext) { export default async function startServices(appContext: AppContext) {
const services = appContext.services; const services = appContext.joplin.services;
void services.share.runInBackground(); void services.share.runInBackground();
void services.email.runInBackground(); void services.email.runInBackground();

View File

@ -175,16 +175,20 @@ export async function koaAppContext(options: AppContextTestOptions = null): Prom
const appLogger = Logger.create('AppTest'); const appLogger = Logger.create('AppTest');
const baseAppContext = await setupAppContext({} as any, Env.Dev, db_, () => appLogger);
// Set type to "any" because the Koa context has many properties and we // Set type to "any" because the Koa context has many properties and we
// don't need to mock all of them. // don't need to mock all of them.
const appContext: any = { const appContext: any = {
...await setupAppContext({} as any, Env.Dev, db_, () => appLogger), baseAppContext,
joplin: {
...baseAppContext.joplin,
env: Env.Dev, env: Env.Dev,
db: db_, db: db_,
models: models(), models: models(),
appLogger: () => appLogger,
path: req.url,
owner: owner, owner: owner,
},
path: req.url,
cookies: new FakeCookies(), cookies: new FakeCookies(),
request: new FakeRequest(req), request: new FakeRequest(req),
response: new FakeResponse(), response: new FakeResponse(),

View File

@ -18,7 +18,7 @@ export interface NotificationView {
closeUrl: string; closeUrl: string;
} }
export interface AppContext extends Koa.Context { interface AppContextJoplin {
env: Env; env: Env;
db: DbConnection; db: DbConnection;
models: Models; models: Models;
@ -29,6 +29,22 @@ export interface AppContext extends Koa.Context {
services: Services; services: Services;
} }
export interface AppContext extends Koa.Context {
joplin: AppContextJoplin;
// All the properties under `joplin` were previously at the root, so to make
// sure they are no longer used anywhere we set them to "never", as that
// would trigger the TypeScript compiler. Later on, all this can be removed.
env: never;
db: never;
models: never;
appLogger: never;
notifications: never;
owner: never;
routes: never;
services: never;
}
export enum DatabaseConfigClient { export enum DatabaseConfigClient {
PostgreSQL = 'pg', PostgreSQL = 'pg',
SQLite = 'sqlite3', SQLite = 'sqlite3',