1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +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;
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 initializeJoplinUtils(config(), appContext.models, appContext.services.mustache);
await setupAppContext(ctx, env, connectionCheck.connection, appLogger);
await initializeJoplinUtils(config(), ctx.models, ctx.joplin.services.mustache);
appLogger().info('Migrating database...');
await migrateDb(appContext.db);
await migrateDb(ctx.db);
appLogger().info('Starting services...');
await startServices(appContext);
await startServices(ctx);
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 });
await notificationHandler(context, koaNext);
const ctx = await koaAppContext({ sessionId: session.id });
await notificationHandler(ctx, koaNext);
const notifications: Notification[] = await models().notification().all();
expect(notifications.length).toBe(1);
expect(notifications[0].key).toBe('change_admin_password');
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!',
});
const context = await koaAppContext({ sessionId: session.id });
await notificationHandler(context, koaNext);
const ctx = await koaAppContext({ sessionId: session.id });
await notificationHandler(ctx, koaNext);
const notifications: Notification[] = await models().notification().all();
expect(notifications.length).toBe(1);
expect(notifications[0].key).toBe('change_admin_password');
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');
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 notificationModel = ctx.models.notification();
const defaultAdmin = await ctx.joplin.models.user().login(defaultAdminEmail, defaultAdminPassword);
const notificationModel = ctx.joplin.models.notification();
if (defaultAdmin) {
await notificationModel.add(
ctx.owner.id,
ctx.joplin.owner.id,
NotificationKey.ChangeAdminPassword,
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 {
await notificationModel.markAsRead(ctx.owner.id, NotificationKey.ChangeAdminPassword);
await notificationModel.markAsRead(ctx.joplin.owner.id, NotificationKey.ChangeAdminPassword);
}
}
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(
ctx.owner.id,
ctx.joplin.owner.id,
NotificationKey.UsingSqliteInProd
);
}
@ -43,8 +43,8 @@ async function handleSqliteInProdNotification(ctx: AppContext) {
async function makeNotificationViews(ctx: AppContext): Promise<NotificationView[]> {
const markdownIt = new MarkdownIt();
const notificationModel = ctx.models.notification();
const notifications = await notificationModel.allUnreadByUserId(ctx.owner.id);
const notificationModel = ctx.joplin.models.notification();
const notifications = await notificationModel.allUnreadByUserId(ctx.joplin.owner.id);
const views: NotificationView[] = [];
for (const n of notifications) {
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
// on the website. It is inactive for API calls.
export default async function(ctx: AppContext, next: KoaNext): Promise<void> {
ctx.notifications = [];
ctx.joplin.notifications = [];
try {
if (isApiRequest(ctx)) return next();
if (!ctx.owner) return next();
if (!ctx.joplin.owner) return next();
await handleChangeAdminPasswordNotification(ctx);
await handleSqliteInProdNotification(ctx);
ctx.notifications = await makeNotificationViews(ctx);
ctx.joplin.notifications = await makeNotificationViews(ctx);
} catch (error) {
logger.error(error);
}

View File

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

View File

@ -7,17 +7,17 @@ export default async function(ctx: AppContext) {
const requestStartTime = Date.now();
try {
const responseObject = await execRequest(ctx.routes, ctx);
const responseObject = await execRequest(ctx.joplin.routes, ctx);
if (responseObject instanceof Response) {
ctx.response = responseObject.response;
} else if (isView(responseObject)) {
const view = responseObject as View;
ctx.response.status = view?.content?.error ? view?.content?.error?.httpCode || 500 : 200;
ctx.response.body = await ctx.services.mustache.renderView(view, {
notifications: ctx.notifications || [],
hasNotifications: !!ctx.notifications && !!ctx.notifications.length,
owner: ctx.owner,
ctx.response.body = await ctx.joplin.services.mustache.renderView(view, {
notifications: ctx.joplin.notifications || [],
hasNotifications: !!ctx.joplin.notifications && !!ctx.joplin.notifications.length,
owner: ctx.joplin.owner,
});
} else {
ctx.response.status = 200;
@ -25,9 +25,9 @@ export default async function(ctx: AppContext) {
}
} catch (error) {
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 {
ctx.appLogger().error(error);
ctx.joplin.appLogger().error(error);
}
// Uncomment this when getting HTML blobs as errors while running tests.
@ -47,15 +47,15 @@ export default async function(ctx: AppContext) {
content: {
error,
stack: config().showErrorStackTraces ? error.stack : '',
owner: ctx.owner,
owner: ctx.joplin.owner,
},
title: 'Error',
};
ctx.response.body = await ctx.services.mustache.renderView(view);
ctx.response.body = await ctx.joplin.services.mustache.renderView(view);
} else { // JSON
ctx.response.set('Content-Type', 'application/json');
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;
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
// other middlewares but that should give a good approximation
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,
},
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}`,
url: fullUrl,
services: ctx.services,
db: ctx.db,
models: ctx.models,
routes: ctx.routes,
};
return newContext;

View File

@ -20,7 +20,7 @@ router.post('api/debug', async (_path: SubPath, ctx: AppContext) => {
console.info(`Action: ${query.action}`);
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> = {
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;
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 bodyFields = parsedBody.fields;
@ -49,12 +49,12 @@ export async function putItemContents(path: SubPath, ctx: AppContext, isBatch: b
// query parameter.
if (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 = [
{
name: ctx.models.item().pathToName(path.id),
name: ctx.joplin.models.item().pathToName(path.id),
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)) {
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;
}
@ -89,8 +89,8 @@ async function itemFromPath(userId: Uuid, itemModel: ItemModel, path: SubPath, m
}
router.get('api/items/:id', async (path: SubPath, ctx: AppContext) => {
const itemModel = ctx.models.item();
const item = await itemFromPath(ctx.owner.id, itemModel, path);
const itemModel = ctx.joplin.models.item();
const item = await itemFromPath(ctx.joplin.owner.id, itemModel, path);
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:/:') {
// We use this for testing only and for safety reasons it's probably
// best to disable it on production.
if (ctx.env !== 'dev') throw new ErrorMethodNotAllowed('Deleting the root is not allowed');
await ctx.models.item().deleteAll(ctx.owner.id);
if (ctx.joplin.env !== 'dev') throw new ErrorMethodNotAllowed('Deleting the root is not allowed');
await ctx.joplin.models.item().deleteAll(ctx.joplin.owner.id);
} else {
const item = await itemFromPath(ctx.owner.id, ctx.models.item(), path);
await ctx.models.item().checkIfAllowed(ctx.owner, AclAction.Delete, item);
await ctx.models.item().deleteForUser(ctx.owner.id, item);
const item = await itemFromPath(ctx.joplin.owner.id, ctx.joplin.models.item(), path);
await ctx.joplin.models.item().checkIfAllowed(ctx.joplin.owner, AclAction.Delete, item);
await ctx.joplin.models.item().deleteForUser(ctx.joplin.owner.id, item);
}
} catch (error) {
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) => {
const itemModel = ctx.models.item();
const item = await itemFromPath(ctx.owner.id, itemModel, path);
const itemModel = ctx.joplin.models.item();
const item = await itemFromPath(ctx.joplin.owner.id, itemModel, path);
const serializedContent = await itemModel.serializedContent(item.id);
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) => {
const changeModel = ctx.models.change();
return changeModel.delta(ctx.owner.id, requestDeltaPagination(ctx.query));
const changeModel = ctx.joplin.models.change();
return changeModel.delta(ctx.joplin.owner.id, requestDeltaPagination(ctx.query));
});
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 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;
});

View File

@ -12,10 +12,10 @@ router.public = true;
router.post('api/sessions', async (_path: SubPath, ctx: AppContext) => {
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');
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 };
});

View File

@ -9,11 +9,11 @@ import { AclAction } from '../../models/BaseModel';
const router = new Router(RouteType.Api);
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);
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);
@ -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) => {
const shareUser = await ctx.models.shareUser().load(path.id);
const shareUser = await ctx.joplin.models.shareUser().load(path.id);
if (!shareUser) throw new ErrorNotFound();
await ctx.models.shareUser().checkIfAllowed(ctx.owner, AclAction.Delete, shareUser);
await ctx.models.shareUser().delete(shareUser.id);
await ctx.joplin.models.shareUser().checkIfAllowed(ctx.joplin.owner, AclAction.Delete, shareUser);
await ctx.joplin.models.shareUser().delete(shareUser.id);
});
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[] = [];
for (const su of shareUsers) {
const share = await ctx.models.share().load(su.share_id);
const sharer = await ctx.models.user().load(share.owner_id);
const share = await ctx.joplin.models.share().load(su.share_id);
const sharer = await ctx.joplin.models.user().load(share.owner_id);
items.push({
id: su.id,

View File

@ -19,7 +19,7 @@ router.public = true;
router.post('api/shares', async (_path: SubPath, ctx: AppContext) => {
ownerRequired(ctx);
const shareModel = ctx.models.share();
const shareModel = ctx.joplin.models.share();
const fields = await bodyFields<any>(ctx.req);
const shareInput: ShareApiInput = shareModel.fromApiInput(fields) as ShareApiInput;
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.
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) {
return ctx.models.share().shareNote(ctx.owner, shareInput.note_id);
return ctx.joplin.models.share().shareNote(ctx.joplin.owner, shareInput.note_id);
} else {
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 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');
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,
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) => {
ownerRequired(ctx);
const shareId = path.id;
const share = await ctx.models.share().load(shareId);
await ctx.models.share().checkIfAllowed(ctx.owner, AclAction.Read, share);
const share = await ctx.joplin.models.share().load(shareId);
await ctx.joplin.models.share().checkIfAllowed(ctx.joplin.owner, AclAction.Read, share);
const shareUsers = await ctx.models.shareUser().byShareId(shareId, null);
const users = await ctx.models.user().loadByIds(shareUsers.map(su => su.user_id));
const shareUsers = await ctx.joplin.models.shareUser().byShareId(shareId, null);
const users = await ctx.joplin.models.user().loadByIds(shareUsers.map(su => su.user_id));
const items = shareUsers.map(su => {
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) => {
const shareModel = ctx.models.share();
const shareModel = ctx.joplin.models.share();
const share = await shareModel.load(path.id);
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) => {
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.
return {
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) => {
ownerRequired(ctx);
const share = await ctx.models.share().load(path.id);
await ctx.models.share().checkIfAllowed(ctx.owner, AclAction.Delete, share);
await ctx.models.share().delete(share.id);
const share = await ctx.joplin.models.share().load(path.id);
await ctx.joplin.models.share().checkIfAllowed(ctx.joplin.owner, AclAction.Delete, share);
await ctx.joplin.models.share().delete(share.id);
});
export default router;

View File

@ -11,23 +11,23 @@ import uuidgen from '../../utils/uuidgen';
const router = new Router(RouteType.Api);
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}`);
return 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) => {
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;
});
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);
// 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.must_set_password = 1;
user.email_confirmed = 0;
const output = await ctx.models.user().save(user);
return ctx.models.user().toApiOutput(output);
const output = await ctx.joplin.models.user().save(user);
return ctx.joplin.models.user().toApiOutput(output);
});
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 {
items: await ctx.models.user().all(),
items: await ctx.joplin.models.user().all(),
has_more: false,
};
});
router.del('api/users/:id', async (path: SubPath, ctx: AppContext) => {
const user = await fetchUser(path, ctx);
await ctx.models.user().checkIfAllowed(ctx.owner, AclAction.Delete, user);
await ctx.models.user().delete(user.id);
await ctx.joplin.models.user().checkIfAllowed(ctx.joplin.owner, AclAction.Delete, user);
await ctx.joplin.models.user().delete(user.id);
});
router.patch('api/users/:id', async (path: SubPath, ctx: AppContext) => {
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);
await ctx.models.user().save({ id: user.id, ...postedUser });
await ctx.joplin.models.user().save({ id: user.id, ...postedUser });
});
export default router;

View File

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

View File

@ -14,11 +14,11 @@ const router = new Router(RouteType.Web);
router.get('changes', async (_path: SubPath, ctx: AppContext) => {
const pagination = makeTablePagination(ctx.query, 'updated_time', PaginationOrderDir.DESC);
const paginatedChanges = await ctx.models.change().allByUser(ctx.owner.id, pagination);
const items = await ctx.models.item().loadByIds(paginatedChanges.items.map(i => i.item_id), { fields: ['id'] });
const paginatedChanges = await ctx.joplin.models.change().allByUser(ctx.joplin.owner.id, pagination);
const items = await ctx.joplin.models.item().loadByIds(paginatedChanges.items.map(i => i.item_id), { fields: ['id'] });
const table: Table = {
baseUrl: ctx.models.change().changeUrl(),
baseUrl: ctx.joplin.models.change().changeUrl(),
requestQuery: ctx.query,
pageCount: paginatedChanges.page_count,
pagination,
@ -42,7 +42,7 @@ router.get('changes', async (_path: SubPath, ctx: AppContext) => {
{
value: change.item_name,
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),

View File

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

View File

@ -16,12 +16,12 @@ const router = new Router(RouteType.Web);
router.get('items', async (_path: SubPath, ctx: AppContext) => {
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 = {
baseUrl: ctx.models.item().itemUrl(),
baseUrl: ctx.joplin.models.item().itemUrl(),
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,
headers: [
{
@ -72,7 +72,7 @@ router.get('items', 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);
if (!item) throw new ErrorNotFound();
return respondWithItemContent(ctx.response, item, item.content);
@ -83,13 +83,13 @@ router.post('items', async (_path: SubPath, ctx: AppContext) => {
const fields = body.fields;
if (fields.delete_all_button) {
const itemModel = ctx.models.item();
await itemModel.deleteAll(ctx.owner.id);
const itemModel = ctx.joplin.models.item();
await itemModel.deleteAll(ctx.joplin.owner.id);
} else {
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;

View File

@ -28,7 +28,7 @@ router.post('login', async (_path: SubPath, ctx: AppContext) => {
try {
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);
return redirect(ctx, `${config().baseUrl}/home`);
} catch (error) {

View File

@ -10,7 +10,7 @@ const router = new Router(RouteType.Web);
router.post('logout', async (_path: SubPath, ctx: AppContext) => {
const sessionId = contextSessionId(ctx, false);
ctx.cookies.set('sessionId', '');
await ctx.models.session().logout(sessionId);
await ctx.joplin.models.session().logout(sessionId);
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) => {
const fields: Notification = await bodyFields(ctx.req);
const notificationId = path.id;
const model = ctx.models.notification();
const model = ctx.joplin.models.notification();
const existingNotification = await model.load(notificationId);
if (!existingNotification) throw new ErrorNotFound();

View File

@ -24,19 +24,19 @@ const router: Router = new Router(RouteType.Web);
router.public = true;
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);
if (!share) throw new ErrorNotFound();
const itemModel = ctx.models.item();
const itemModel = ctx.joplin.models.item();
const item = await itemModel.loadWithContent(share.item_id);
if (!item) throw new ErrorNotFound();
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.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 password = checkPassword(formUser, true);
const user = await ctx.models.user().save({
const user = await ctx.joplin.models.user().save({
...accountTypeProperties(AccountType.Basic),
email: formUser.email,
full_name: formUser.full_name,
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);
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`);
} catch (error) {

View File

@ -137,7 +137,7 @@ const postHandlers: Record<string, StripeRouteHandler> = {
const stripeUserId = checkoutSession.customer 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,
AccountType.Pro,
stripeUserId,
@ -158,7 +158,7 @@ const postHandlers: Record<string, StripeRouteHandler> = {
// saved in checkout.session.completed.
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 () => {
@ -170,7 +170,7 @@ const postHandlers: Record<string, StripeRouteHandler> = {
const invoice = event.data.object as Stripe.Invoice;
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) => {
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');
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);
router.get('users', async (_path: SubPath, ctx: AppContext) => {
const userModel = ctx.models.user();
await userModel.checkIfAllowed(ctx.owner, AclAction.List);
const userModel = ctx.joplin.models.user();
await userModel.checkIfAllowed(ctx.joplin.owner, AclAction.List);
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) => {
const owner = ctx.owner;
const owner = ctx.joplin.owner;
const isMe = userIsMe(path);
const isNew = userIsNew(path);
const userModel = ctx.models.user();
const userModel = ctx.joplin.models.user();
const userId = userIsMe(path) ? owner.id : path.id;
user = !isNew ? user || await userModel.load(userId) : null;
if (isNew && !user) user = defaultUser();
await userModel.checkIfAllowed(ctx.owner, AclAction.Read, user);
await userModel.checkIfAllowed(ctx.joplin.owner, AclAction.Read, user);
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) => {
const userId = path.id;
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) {
const view: View = {
@ -158,17 +158,17 @@ router.get('users/:id/confirm', async (path: SubPath, ctx: AppContext, error: Er
user,
error,
token,
postUrl: ctx.models.user().confirmUrl(userId, token),
postUrl: ctx.joplin.models.user().confirmUrl(userId, token),
},
navbar: false,
};
return view;
} else {
await ctx.models.token().deleteByValue(userId, token);
await ctx.models.notification().add(userId, NotificationKey.EmailConfirmed);
await ctx.joplin.models.token().deleteByValue(userId, token);
await ctx.joplin.models.notification().add(userId, NotificationKey.EmailConfirmed);
if (ctx.owner) {
if (ctx.joplin.owner) {
return redirect(ctx, `${config().baseUrl}/home`);
} else {
return redirect(ctx, `${config().baseUrl}/login`);
@ -187,17 +187,17 @@ router.post('users/:id/confirm', async (path: SubPath, ctx: AppContext) => {
try {
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);
await ctx.models.user().save({ id: userId, password, must_set_password: 0 });
await ctx.models.token().deleteByValue(userId, fields.token);
await ctx.joplin.models.user().save({ id: userId, password, must_set_password: 0 });
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);
await ctx.models.notification().add(userId, NotificationKey.PasswordSet);
await ctx.joplin.models.notification().add(userId, NotificationKey.PasswordSet);
return redirect(ctx, `${config().baseUrl}/home`);
} catch (error) {
@ -217,7 +217,7 @@ interface FormFields {
router.post('users', async (path: SubPath, ctx: AppContext) => {
let user: User = {};
const userId = userIsMe(path) ? ctx.owner.id : path.id;
const userId = userIsMe(path) ? ctx.joplin.owner.id : path.id;
try {
const body = await formParse(ctx.req);
@ -226,11 +226,11 @@ router.post('users', async (path: SubPath, ctx: AppContext) => {
if (userIsMe(path)) fields.id = userId;
user = makeUser(isNew, fields);
const userModel = ctx.models.user();
const userModel = ctx.joplin.models.user();
if (fields.post_button) {
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) {
await userModel.save(userToSave);
@ -239,7 +239,7 @@ router.post('users', async (path: SubPath, ctx: AppContext) => {
}
} else if (fields.delete_button) {
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);
} else if (fields.send_reset_password_email) {
const user = await userModel.load(path.id);

View File

@ -10,7 +10,7 @@ interface RouteInfo {
export default class Router {
// 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
// should not be publicly available should call ownerRequired(ctx);
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) {
if (!ctx.owner) throw new ErrorForbidden();
if (!ctx.joplin.owner) throw new ErrorForbidden();
}
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
// 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();
if (!match.route.isPublic(match.subPath.schema) && !ctx.joplin.owner) throw new ErrorForbidden();
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> {
appContext.env = env;
appContext.db = dbConnection;
appContext.models = newModelFactory(appContext.db, config());
appContext.services = await setupServices(env, appContext.models, config());
appContext.appLogger = appLogger;
appContext.routes = { ...routes };
const models = newModelFactory(dbConnection, config());
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;
}

View File

@ -1,7 +1,7 @@
import { AppContext } from './types';
export default async function startServices(appContext: AppContext) {
const services = appContext.services;
const services = appContext.joplin.services;
void services.share.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 baseAppContext = await setupAppContext({} as any, Env.Dev, db_, () => appLogger);
// Set type to "any" because the Koa context has many properties and we
// don't need to mock all of them.
const appContext: any = {
...await setupAppContext({} as any, Env.Dev, db_, () => appLogger),
env: Env.Dev,
db: db_,
models: models(),
appLogger: () => appLogger,
baseAppContext,
joplin: {
...baseAppContext.joplin,
env: Env.Dev,
db: db_,
models: models(),
owner: owner,
},
path: req.url,
owner: owner,
cookies: new FakeCookies(),
request: new FakeRequest(req),
response: new FakeResponse(),

View File

@ -18,7 +18,7 @@ export interface NotificationView {
closeUrl: string;
}
export interface AppContext extends Koa.Context {
interface AppContextJoplin {
env: Env;
db: DbConnection;
models: Models;
@ -29,6 +29,22 @@ export interface AppContext extends Koa.Context {
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 {
PostgreSQL = 'pg',
SQLite = 'sqlite3',