mirror of
https://github.com/laurent22/joplin.git
synced 2024-11-27 08:21:03 +02:00
Server: Support for notifications and clean up
This commit is contained in:
parent
4128c53fcf
commit
d2771029a3
@ -1460,6 +1460,9 @@ packages/server/src/controllers/index/HomeController.js.map
|
||||
packages/server/src/controllers/index/LoginController.d.ts
|
||||
packages/server/src/controllers/index/LoginController.js
|
||||
packages/server/src/controllers/index/LoginController.js.map
|
||||
packages/server/src/controllers/index/NotificationController.d.ts
|
||||
packages/server/src/controllers/index/NotificationController.js
|
||||
packages/server/src/controllers/index/NotificationController.js.map
|
||||
packages/server/src/controllers/index/ProfileController.d.ts
|
||||
packages/server/src/controllers/index/ProfileController.js
|
||||
packages/server/src/controllers/index/ProfileController.js.map
|
||||
@ -1469,9 +1472,18 @@ packages/server/src/controllers/index/UserController.js.map
|
||||
packages/server/src/db.d.ts
|
||||
packages/server/src/db.js
|
||||
packages/server/src/db.js.map
|
||||
packages/server/src/middleware/notificationHandler.d.ts
|
||||
packages/server/src/middleware/notificationHandler.js
|
||||
packages/server/src/middleware/notificationHandler.js.map
|
||||
packages/server/src/middleware/requestProcessor.d.ts
|
||||
packages/server/src/middleware/requestProcessor.js
|
||||
packages/server/src/middleware/requestProcessor.js.map
|
||||
packages/server/src/migrations/20190913171451_create.d.ts
|
||||
packages/server/src/migrations/20190913171451_create.js
|
||||
packages/server/src/migrations/20190913171451_create.js.map
|
||||
packages/server/src/migrations/20203012152842_notifications.d.ts
|
||||
packages/server/src/migrations/20203012152842_notifications.js
|
||||
packages/server/src/migrations/20203012152842_notifications.js.map
|
||||
packages/server/src/models/ApiClientModel.d.ts
|
||||
packages/server/src/models/ApiClientModel.js
|
||||
packages/server/src/models/ApiClientModel.js.map
|
||||
@ -1490,6 +1502,12 @@ packages/server/src/models/FileModel.js.map
|
||||
packages/server/src/models/FileModel.test.d.ts
|
||||
packages/server/src/models/FileModel.test.js
|
||||
packages/server/src/models/FileModel.test.js.map
|
||||
packages/server/src/models/NotificationModel.d.ts
|
||||
packages/server/src/models/NotificationModel.js
|
||||
packages/server/src/models/NotificationModel.js.map
|
||||
packages/server/src/models/NotificationModel.test.d.ts
|
||||
packages/server/src/models/NotificationModel.test.js
|
||||
packages/server/src/models/NotificationModel.test.js.map
|
||||
packages/server/src/models/PermissionModel.d.ts
|
||||
packages/server/src/models/PermissionModel.js
|
||||
packages/server/src/models/PermissionModel.js.map
|
||||
@ -1535,6 +1553,9 @@ packages/server/src/routes/index/login.js.map
|
||||
packages/server/src/routes/index/logout.d.ts
|
||||
packages/server/src/routes/index/logout.js
|
||||
packages/server/src/routes/index/logout.js.map
|
||||
packages/server/src/routes/index/notifications.d.ts
|
||||
packages/server/src/routes/index/notifications.js
|
||||
packages/server/src/routes/index/notifications.js.map
|
||||
packages/server/src/routes/index/profile.d.ts
|
||||
packages/server/src/routes/index/profile.js
|
||||
packages/server/src/routes/index/profile.js.map
|
||||
|
21
.gitignore
vendored
21
.gitignore
vendored
@ -1449,6 +1449,9 @@ packages/server/src/controllers/index/HomeController.js.map
|
||||
packages/server/src/controllers/index/LoginController.d.ts
|
||||
packages/server/src/controllers/index/LoginController.js
|
||||
packages/server/src/controllers/index/LoginController.js.map
|
||||
packages/server/src/controllers/index/NotificationController.d.ts
|
||||
packages/server/src/controllers/index/NotificationController.js
|
||||
packages/server/src/controllers/index/NotificationController.js.map
|
||||
packages/server/src/controllers/index/ProfileController.d.ts
|
||||
packages/server/src/controllers/index/ProfileController.js
|
||||
packages/server/src/controllers/index/ProfileController.js.map
|
||||
@ -1458,9 +1461,18 @@ packages/server/src/controllers/index/UserController.js.map
|
||||
packages/server/src/db.d.ts
|
||||
packages/server/src/db.js
|
||||
packages/server/src/db.js.map
|
||||
packages/server/src/middleware/notificationHandler.d.ts
|
||||
packages/server/src/middleware/notificationHandler.js
|
||||
packages/server/src/middleware/notificationHandler.js.map
|
||||
packages/server/src/middleware/requestProcessor.d.ts
|
||||
packages/server/src/middleware/requestProcessor.js
|
||||
packages/server/src/middleware/requestProcessor.js.map
|
||||
packages/server/src/migrations/20190913171451_create.d.ts
|
||||
packages/server/src/migrations/20190913171451_create.js
|
||||
packages/server/src/migrations/20190913171451_create.js.map
|
||||
packages/server/src/migrations/20203012152842_notifications.d.ts
|
||||
packages/server/src/migrations/20203012152842_notifications.js
|
||||
packages/server/src/migrations/20203012152842_notifications.js.map
|
||||
packages/server/src/models/ApiClientModel.d.ts
|
||||
packages/server/src/models/ApiClientModel.js
|
||||
packages/server/src/models/ApiClientModel.js.map
|
||||
@ -1479,6 +1491,12 @@ packages/server/src/models/FileModel.js.map
|
||||
packages/server/src/models/FileModel.test.d.ts
|
||||
packages/server/src/models/FileModel.test.js
|
||||
packages/server/src/models/FileModel.test.js.map
|
||||
packages/server/src/models/NotificationModel.d.ts
|
||||
packages/server/src/models/NotificationModel.js
|
||||
packages/server/src/models/NotificationModel.js.map
|
||||
packages/server/src/models/NotificationModel.test.d.ts
|
||||
packages/server/src/models/NotificationModel.test.js
|
||||
packages/server/src/models/NotificationModel.test.js.map
|
||||
packages/server/src/models/PermissionModel.d.ts
|
||||
packages/server/src/models/PermissionModel.js
|
||||
packages/server/src/models/PermissionModel.js.map
|
||||
@ -1524,6 +1542,9 @@ packages/server/src/routes/index/login.js.map
|
||||
packages/server/src/routes/index/logout.d.ts
|
||||
packages/server/src/routes/index/logout.js
|
||||
packages/server/src/routes/index/logout.js.map
|
||||
packages/server/src/routes/index/notifications.d.ts
|
||||
packages/server/src/routes/index/notifications.js
|
||||
packages/server/src/routes/index/notifications.js.map
|
||||
packages/server/src/routes/index/profile.d.ts
|
||||
packages/server/src/routes/index/profile.js
|
||||
packages/server/src/routes/index/profile.js.map
|
||||
|
@ -313,6 +313,17 @@ class Resource extends BaseItem {
|
||||
return r ? r.total : 0;
|
||||
}
|
||||
|
||||
static async createdLocallyCount() {
|
||||
const r = await this.db().selectOne(`
|
||||
SELECT count(*) as total
|
||||
FROM resources
|
||||
WHERE id NOT IN
|
||||
(SELECT resource_id FROM resource_local_states)
|
||||
`);
|
||||
|
||||
return r ? r.total : 0;
|
||||
}
|
||||
|
||||
static fetchStatusToLabel(status) {
|
||||
if (status === Resource.FETCH_STATUS_IDLE) return _('Not downloaded');
|
||||
if (status === Resource.FETCH_STATUS_STARTED) return _('Downloading');
|
||||
|
@ -187,8 +187,10 @@ class ReportService {
|
||||
if (status === Resource.FETCH_STATUS_DONE) {
|
||||
const downloadedButEncryptedBlobCount = await Resource.downloadedButEncryptedBlobCount();
|
||||
const downloadedCount = await Resource.downloadStatusCounts(Resource.FETCH_STATUS_DONE);
|
||||
const createdLocallyCount = await Resource.createdLocallyCount();
|
||||
section.body.push(_('%s: %d', _('Downloaded and decrypted'), downloadedCount - downloadedButEncryptedBlobCount));
|
||||
section.body.push(_('%s: %d', _('Downloaded and encrypted'), downloadedButEncryptedBlobCount));
|
||||
section.body.push(_('%s: %d', _('Created locally'), createdLocallyCount));
|
||||
} else {
|
||||
const count = await Resource.downloadStatusCounts(status);
|
||||
section.body.push(_('%s: %d', Resource.fetchStatusToLabel(status), count));
|
||||
|
76
packages/server/package-lock.json
generated
76
packages/server/package-lock.json
generated
@ -1262,6 +1262,12 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/highlight.js": {
|
||||
"version": "9.12.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/highlight.js/-/highlight.js-9.12.4.tgz",
|
||||
"integrity": "sha512-t2szdkwmg2JJyuCM20e8kR2X59WCE5Zkl4bzm1u1Oukjm79zpbiAv+QjnwLnuuV0WHEcX2NgUItu0pAMKuOPww==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/http-assert": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.1.tgz",
|
||||
@ -1331,6 +1337,29 @@
|
||||
"@types/koa": "*"
|
||||
}
|
||||
},
|
||||
"@types/linkify-it": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.0.tgz",
|
||||
"integrity": "sha512-x9OaQQTb1N2hPZ/LWJsqushexDvz7NgzuZxiRmZio44WPuolTZNHDBCrOxCzRVOMwamJRO2dWax5NbygOf1OTQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/markdown-it": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.0.0.tgz",
|
||||
"integrity": "sha512-+RJNprPSIcEUBzj3nx8WYwRsDdAKF6/dG932OleYKbTqBSJ7VvZK0JbPKeEpIYxoniUhgvgyZjO4vlCd4mFTdw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/highlight.js": "^9.7.0",
|
||||
"@types/linkify-it": "*",
|
||||
"@types/mdurl": "*"
|
||||
}
|
||||
},
|
||||
"@types/mdurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz",
|
||||
"integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/mime": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz",
|
||||
@ -2652,6 +2681,11 @@
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"entities": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
|
||||
"integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w=="
|
||||
},
|
||||
"error-ex": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
|
||||
@ -5562,6 +5596,14 @@
|
||||
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=",
|
||||
"dev": true
|
||||
},
|
||||
"linkify-it": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.2.tgz",
|
||||
"integrity": "sha512-gDBO4aHNZS6coiZCKVhSNh43F9ioIL4JwRjLZPkoLIY4yZFwg264Y5lu2x6rb1Js42Gh6Yqm2f6L2AJcnkzinQ==",
|
||||
"requires": {
|
||||
"uc.micro": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
@ -5632,6 +5674,30 @@
|
||||
"object-visit": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"markdown-it": {
|
||||
"version": "12.0.4",
|
||||
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.0.4.tgz",
|
||||
"integrity": "sha512-34RwOXZT8kyuOJy25oJNJoulO8L0bTHYWXcdZBYZqFnjIy3NgjeoM3FmPXIOFQ26/lSHYMr8oc62B6adxXcb3Q==",
|
||||
"requires": {
|
||||
"argparse": "^2.0.1",
|
||||
"entities": "~2.1.0",
|
||||
"linkify-it": "^3.0.1",
|
||||
"mdurl": "^1.0.1",
|
||||
"uc.micro": "^1.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"mdurl": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
|
||||
"integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4="
|
||||
},
|
||||
"media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
@ -7177,11 +7243,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sprintf-js": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
|
||||
"integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug=="
|
||||
},
|
||||
"sqlite3": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-4.1.0.tgz",
|
||||
@ -7583,6 +7644,11 @@
|
||||
"integrity": "sha512-thGloWsGH3SOxv1SoY7QojKi0tc+8FnOmiarEGMbd/lar7QOEd3hvlx3Fp5y6FlDUGl9L+pd4n2e+oToGMmhRQ==",
|
||||
"dev": true
|
||||
},
|
||||
"uc.micro": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
|
||||
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "3.12.2",
|
||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.12.2.tgz",
|
||||
|
@ -23,6 +23,7 @@
|
||||
"html-entities": "^1.3.1",
|
||||
"knex": "^0.19.4",
|
||||
"koa": "^2.8.1",
|
||||
"markdown-it": "^12.0.4",
|
||||
"mustache": "^3.1.0",
|
||||
"nanoid": "^2.1.1",
|
||||
"nodemon": "^2.0.6",
|
||||
@ -37,6 +38,7 @@
|
||||
"@types/fs-extra": "^8.0.0",
|
||||
"@types/jest": "^26.0.15",
|
||||
"@types/koa": "^2.0.49",
|
||||
"@types/markdown-it": "^12.0.0",
|
||||
"@types/mustache": "^0.8.32",
|
||||
"@types/yargs": "^13.0.2",
|
||||
"jest": "^26.6.3",
|
||||
|
@ -1,10 +1,9 @@
|
||||
// Allows displaying error stack traces with TypeScript file paths
|
||||
require('source-map-support').install();
|
||||
|
||||
import * as Koa from 'koa';
|
||||
import routes from './routes/routes';
|
||||
import { ErrorNotFound } from './utils/errors';
|
||||
import * as fs from 'fs-extra';
|
||||
import { argv } from 'yargs';
|
||||
import { routeResponseFormat, findMatchingRoute, Response, RouteResponseFormat, MatchedRoute } from './utils/routeUtils';
|
||||
import Logger, { LoggerWrapper, TargetType } from '@joplin/lib/Logger';
|
||||
import config, { initConfig, baseUrl } from './config';
|
||||
import configDev from './config-dev';
|
||||
@ -14,9 +13,15 @@ import { createDb, dropDb } from './tools/dbTools';
|
||||
import { dropTables, connectDb, disconnectDb, migrateDb, waitForConnection } from './db';
|
||||
import modelFactory from './models/factory';
|
||||
import controllerFactory from './controllers/factory';
|
||||
import { AppContext, Config } from './utils/types';
|
||||
import { AppContext, Config, Env } from './utils/types';
|
||||
import FsDriverNode from '@joplin/lib/fs-driver-node';
|
||||
import mustacheService, { isView, View } from './services/MustacheService';
|
||||
import requestProcessor from './middleware/requestProcessor';
|
||||
import notificationHandler from './middleware/notificationHandler';
|
||||
|
||||
const { shimInit } = require('@joplin/lib/shim-init-node.js');
|
||||
shimInit();
|
||||
|
||||
const env: Env = argv.env as Env || Env.Prod;
|
||||
|
||||
interface Configs {
|
||||
[name: string]: Config;
|
||||
@ -28,13 +33,6 @@ const configs: Configs = {
|
||||
buildTypes: configBuildTypes,
|
||||
};
|
||||
|
||||
require('source-map-support').install();
|
||||
|
||||
const env: string = argv.env as string || 'prod';
|
||||
|
||||
const { shimInit } = require('@joplin/lib/shim-init-node.js');
|
||||
shimInit();
|
||||
|
||||
let appLogger_: LoggerWrapper = null;
|
||||
|
||||
function appLogger(): LoggerWrapper {
|
||||
@ -46,60 +44,8 @@ function appLogger(): LoggerWrapper {
|
||||
|
||||
const app = new Koa();
|
||||
|
||||
app.use(async (ctx: Koa.Context) => {
|
||||
appLogger().info(`${ctx.request.method} ${ctx.path}`);
|
||||
|
||||
const match: MatchedRoute = null;
|
||||
|
||||
try {
|
||||
const match = findMatchingRoute(ctx.path, routes);
|
||||
|
||||
if (match) {
|
||||
const responseObject = await match.route.exec(match.subPath, ctx);
|
||||
|
||||
if (responseObject instanceof Response) {
|
||||
ctx.response = responseObject.response;
|
||||
} else if (isView(responseObject)) {
|
||||
ctx.response.status = 200;
|
||||
ctx.response.body = await mustacheService.renderView(responseObject);
|
||||
} else {
|
||||
ctx.response.status = 200;
|
||||
ctx.response.body = responseObject;
|
||||
}
|
||||
} else {
|
||||
throw new ErrorNotFound();
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.httpCode >= 400 && error.httpCode < 500) {
|
||||
appLogger().error(`${error.httpCode}: ` + `${ctx.request.method} ${ctx.path}` + ` : ${error.message}`);
|
||||
} else {
|
||||
appLogger().error(error);
|
||||
}
|
||||
|
||||
ctx.response.status = error.httpCode ? error.httpCode : 500;
|
||||
|
||||
const responseFormat = routeResponseFormat(match, ctx.path);
|
||||
|
||||
if (responseFormat === RouteResponseFormat.Html) {
|
||||
ctx.response.set('Content-Type', 'text/html');
|
||||
const view: View = {
|
||||
name: 'error',
|
||||
path: 'index/error',
|
||||
content: {
|
||||
error,
|
||||
stack: env === 'dev' ? error.stack : '',
|
||||
},
|
||||
};
|
||||
ctx.response.body = await mustacheService.renderView(view);
|
||||
} else { // JSON
|
||||
ctx.response.set('Content-Type', 'application/json');
|
||||
const r: any = { error: error.message };
|
||||
if (env === 'dev' && error.stack) r.stack = error.stack;
|
||||
if (error.code) r.code = error.code;
|
||||
ctx.response.body = r;
|
||||
}
|
||||
}
|
||||
});
|
||||
app.use(notificationHandler);
|
||||
app.use(requestProcessor);
|
||||
|
||||
async function main() {
|
||||
const configObject: Config = configs[env];
|
||||
@ -151,9 +97,11 @@ async function main() {
|
||||
delete connectionCheckLogInfo.connection;
|
||||
|
||||
appLogger().info('Connection check:', connectionCheckLogInfo);
|
||||
appContext.env = env;
|
||||
appContext.db = connectionCheck.connection;
|
||||
appContext.models = modelFactory(appContext.db, baseUrl());
|
||||
appContext.controllers = controllerFactory(appContext.models);
|
||||
appContext.appLogger = appLogger;
|
||||
|
||||
appLogger().info('Migrating database...');
|
||||
await migrateDb(appContext.db);
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Session, User } from '../../db';
|
||||
import { checkPassword } from '../../utils/auth';
|
||||
import { Session } from '../../db';
|
||||
import { ErrorForbidden } from '../../utils/errors';
|
||||
import uuidgen from '../../utils/uuidgen';
|
||||
import BaseController from '../BaseController';
|
||||
@ -8,12 +7,10 @@ export default class SessionController extends BaseController {
|
||||
|
||||
public async authenticate(email: string, password: string): Promise<Session> {
|
||||
const userModel = this.models.user();
|
||||
const user: User = await userModel.loadByEmail(email);
|
||||
const user = await userModel.login(email, password);
|
||||
if (!user) throw new ErrorForbidden('Invalid username or password');
|
||||
if (!checkPassword(password, user.password)) throw new ErrorForbidden('Invalid username or password');
|
||||
const session: Session = { id: uuidgen(), user_id: user.id };
|
||||
const sessionModel = this.models.session();
|
||||
return sessionModel.save(session, { isNew: true });
|
||||
return this.models.session().save(session, { isNew: true });
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import IndexHomeController from './index/HomeController';
|
||||
import IndexProfileController from './index/ProfileController';
|
||||
import IndexUserController from './index/UserController';
|
||||
import IndexFileController from './index/FileController';
|
||||
import IndexNotificationController from './index/NotificationController';
|
||||
|
||||
export class Controllers {
|
||||
|
||||
@ -53,6 +54,10 @@ export class Controllers {
|
||||
return new IndexFileController(this.models_);
|
||||
}
|
||||
|
||||
public indexNotifications() {
|
||||
return new IndexNotificationController(this.models_);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default function(models: Models) {
|
||||
|
@ -0,0 +1,23 @@
|
||||
import BaseController from '../BaseController';
|
||||
import { Notification } from '../../db';
|
||||
import { ErrorNotFound } from '../../utils/errors';
|
||||
|
||||
export default class NotificationController extends BaseController {
|
||||
|
||||
public async patchOne(sessionId: string, notificationId: string, notification: Notification): Promise<void> {
|
||||
const owner = await this.initSession(sessionId);
|
||||
const model = this.models.notification({ userId: owner.id });
|
||||
const existingNotification = await model.load(notificationId);
|
||||
if (!existingNotification) throw new ErrorNotFound();
|
||||
|
||||
|
||||
console.info('aaaaaaa', notification);
|
||||
const toSave: Notification = {};
|
||||
if ('read' in notification) toSave.read = notification.read;
|
||||
if (!Object.keys(toSave).length) return;
|
||||
|
||||
toSave.id = notificationId;
|
||||
await model.save(toSave);
|
||||
}
|
||||
|
||||
}
|
@ -19,6 +19,9 @@ const logger = Logger.create('db');
|
||||
const migrationDir = `${__dirname}/migrations`;
|
||||
const sqliteDbDir = pathUtils.dirname(__dirname);
|
||||
|
||||
export const defaultAdminEmail = 'admin@localhost';
|
||||
export const defaultAdminPassword = 'admin';
|
||||
|
||||
export type DbConnection = Knex;
|
||||
|
||||
export interface DbConfigConnection {
|
||||
@ -128,6 +131,17 @@ export async function dropTables(db: DbConnection): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function truncateTables(db: DbConnection): Promise<void> {
|
||||
for (const tableName of allTableNames()) {
|
||||
try {
|
||||
await db(tableName).truncate();
|
||||
} catch (error) {
|
||||
if (isNoSuchTableError(error)) continue;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isNoSuchTableError(error: any): boolean {
|
||||
if (error) {
|
||||
// Postgres error: 42P01: undefined_table
|
||||
@ -181,6 +195,11 @@ export enum ItemAddressingType {
|
||||
Path,
|
||||
}
|
||||
|
||||
export enum NotificationLevel {
|
||||
Important = 10,
|
||||
Normal = 20,
|
||||
}
|
||||
|
||||
export enum ItemType {
|
||||
File = 1,
|
||||
User,
|
||||
@ -261,6 +280,15 @@ export interface ApiClient extends WithDates, WithUuid {
|
||||
secret?: string;
|
||||
}
|
||||
|
||||
export interface Notification extends WithDates, WithUuid {
|
||||
owner_id?: Uuid;
|
||||
level?: NotificationLevel;
|
||||
key?: string;
|
||||
message?: string;
|
||||
read?: number;
|
||||
canBeDismissed?: number;
|
||||
}
|
||||
|
||||
export const databaseSchema: DatabaseTables = {
|
||||
users: {
|
||||
id: { type: 'string' },
|
||||
@ -320,5 +348,16 @@ export const databaseSchema: DatabaseTables = {
|
||||
updated_time: { type: 'string' },
|
||||
created_time: { type: 'string' },
|
||||
},
|
||||
notifications: {
|
||||
id: { type: 'string' },
|
||||
owner_id: { type: 'string' },
|
||||
level: { type: 'number' },
|
||||
key: { type: 'string' },
|
||||
message: { type: 'string' },
|
||||
read: { type: 'number' },
|
||||
canBeDismissed: { type: 'number' },
|
||||
updated_time: { type: 'string' },
|
||||
created_time: { type: 'string' },
|
||||
},
|
||||
};
|
||||
// AUTO-GENERATED-TYPES
|
||||
|
52
packages/server/src/middleware/notificationHandler.ts
Normal file
52
packages/server/src/middleware/notificationHandler.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { AppContext, KoaNext, NotificationView } from '../utils/types';
|
||||
import { isApiRequest, contextSessionId } from '../utils/requestUtils';
|
||||
import { defaultAdminEmail, defaultAdminPassword, NotificationLevel, User } from '../db';
|
||||
import { _ } from '@joplin/lib/locale';
|
||||
import Logger from '@joplin/lib/Logger';
|
||||
import * as MarkdownIt from 'markdown-it';
|
||||
|
||||
const logger = Logger.create('notificationHandler');
|
||||
|
||||
export default async function(ctx: AppContext, next: KoaNext): Promise<void> {
|
||||
ctx.notifications = [];
|
||||
|
||||
try {
|
||||
if (isApiRequest(ctx)) return next();
|
||||
|
||||
const sessionId = contextSessionId(ctx);
|
||||
const user: User = await ctx.models.session().sessionUser(sessionId);
|
||||
if (!user) return next();
|
||||
|
||||
const notificationModel = ctx.models.notification({ userId: user.id });
|
||||
|
||||
if (user.is_admin) {
|
||||
const defaultAdmin = await ctx.models.user().login(defaultAdminEmail, defaultAdminPassword);
|
||||
|
||||
if (defaultAdmin) {
|
||||
await notificationModel.add(
|
||||
'change_admin_password',
|
||||
NotificationLevel.Important,
|
||||
_('The default admin password is insecure and has not been changed! [Change it now](%s)', await ctx.models.user().profileUrl())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const markdownIt = new MarkdownIt();
|
||||
const notifications = await notificationModel.allUnreadByUserId(user.id);
|
||||
const views: NotificationView[] = [];
|
||||
for (const n of notifications) {
|
||||
views.push({
|
||||
id: n.id,
|
||||
messageHtml: markdownIt.render(n.message),
|
||||
level: n.level === NotificationLevel.Important ? 'warning' : 'info',
|
||||
closeUrl: notificationModel.closeUrl(n.id),
|
||||
});
|
||||
}
|
||||
|
||||
ctx.notifications = views;
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
62
packages/server/src/middleware/requestProcessor.ts
Normal file
62
packages/server/src/middleware/requestProcessor.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import routes from '../routes/routes';
|
||||
import { ErrorNotFound } from '../utils/errors';
|
||||
import { routeResponseFormat, findMatchingRoute, Response, RouteResponseFormat, MatchedRoute } from '../utils/routeUtils';
|
||||
import { AppContext, Env } from '../utils/types';
|
||||
import mustacheService, { isView, View } from '../services/MustacheService';
|
||||
|
||||
export default async function(ctx: AppContext) {
|
||||
ctx.appLogger().info(`${ctx.request.method} ${ctx.path}`);
|
||||
|
||||
const match: MatchedRoute = null;
|
||||
|
||||
try {
|
||||
const match = findMatchingRoute(ctx.path, routes);
|
||||
|
||||
if (match) {
|
||||
const responseObject = await match.route.exec(match.subPath, ctx);
|
||||
|
||||
if (responseObject instanceof Response) {
|
||||
ctx.response = responseObject.response;
|
||||
} else if (isView(responseObject)) {
|
||||
ctx.response.status = 200;
|
||||
ctx.response.body = await mustacheService.renderView(responseObject, {
|
||||
notifications: ctx.notifications || [],
|
||||
});
|
||||
} else {
|
||||
ctx.response.status = 200;
|
||||
ctx.response.body = responseObject;
|
||||
}
|
||||
} else {
|
||||
throw new ErrorNotFound();
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.httpCode >= 400 && error.httpCode < 500) {
|
||||
ctx.appLogger().error(`${error.httpCode}: ` + `${ctx.request.method} ${ctx.path}` + ` : ${error.message}`);
|
||||
} else {
|
||||
ctx.appLogger().error(error);
|
||||
}
|
||||
|
||||
ctx.response.status = error.httpCode ? error.httpCode : 500;
|
||||
|
||||
const responseFormat = routeResponseFormat(match, ctx.path);
|
||||
|
||||
if (responseFormat === RouteResponseFormat.Html) {
|
||||
ctx.response.set('Content-Type', 'text/html');
|
||||
const view: View = {
|
||||
name: 'error',
|
||||
path: 'index/error',
|
||||
content: {
|
||||
error,
|
||||
stack: ctx.env === Env.Dev ? error.stack : '',
|
||||
},
|
||||
};
|
||||
ctx.response.body = await mustacheService.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 (error.code) r.code = error.code;
|
||||
ctx.response.body = r;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import * as Knex from 'knex';
|
||||
import { DbConnection } from '../db';
|
||||
import { DbConnection, defaultAdminEmail, defaultAdminPassword } from '../db';
|
||||
import { hashPassword } from '../utils/auth';
|
||||
import uuidgen from '../utils/uuidgen';
|
||||
|
||||
@ -97,8 +97,8 @@ export async function up(db: DbConnection): Promise<any> {
|
||||
|
||||
await db('users').insert({
|
||||
id: adminId,
|
||||
email: 'admin@localhost',
|
||||
password: hashPassword('admin'),
|
||||
email: defaultAdminEmail,
|
||||
password: hashPassword(defaultAdminPassword),
|
||||
full_name: 'Admin',
|
||||
is_admin: 1,
|
||||
updated_time: now,
|
||||
|
@ -0,0 +1,24 @@
|
||||
import * as Knex from 'knex';
|
||||
import { DbConnection } from '../db';
|
||||
|
||||
export async function up(db: DbConnection): Promise<any> {
|
||||
await db.schema.createTable('notifications', function(table: Knex.CreateTableBuilder) {
|
||||
table.string('id', 32).unique().primary().notNullable();
|
||||
table.string('owner_id', 32).notNullable();
|
||||
table.integer('level').notNullable();
|
||||
table.text('key', 'string').notNullable();
|
||||
table.text('message', 'mediumtext').notNullable();
|
||||
table.integer('read').defaultTo(0).notNullable();
|
||||
table.integer('canBeDismissed').defaultTo(1).notNullable();
|
||||
table.bigInteger('updated_time').notNullable();
|
||||
table.bigInteger('created_time').notNullable();
|
||||
});
|
||||
|
||||
await db.schema.alterTable('notifications', function(table: Knex.CreateTableBuilder) {
|
||||
table.unique(['owner_id', 'key']);
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(db: DbConnection): Promise<any> {
|
||||
await db.schema.dropTable('notifications');
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
import { WithDates, WithUuid, File, User, Session, Permission, databaseSchema, ApiClient, DbConnection, Change, ItemType, ChangeType } from '../db';
|
||||
import { WithDates, WithUuid, File, User, Session, Permission, databaseSchema, ApiClient, DbConnection, Change, ItemType, ChangeType, Notification } from '../db';
|
||||
import TransactionHandler from '../utils/TransactionHandler';
|
||||
import uuidgen from '../utils/uuidgen';
|
||||
import { ErrorUnprocessableEntity, ErrorBadRequest } from '../utils/errors';
|
||||
import { Models } from './factory';
|
||||
|
||||
export type AnyItemType = File | User | Session | Permission | ApiClient | Change;
|
||||
export type AnyItemTypes = File[] | User[] | Session[] | Permission[] | ApiClient[] | Change[];
|
||||
export type AnyItemType = File | User | Session | Permission | ApiClient | Change | Notification;
|
||||
export type AnyItemTypes = File[] | User[] | Session[] | Permission[] | ApiClient[] | Change[] | Notification[];
|
||||
|
||||
export interface ModelOptions {
|
||||
userId?: string;
|
||||
|
49
packages/server/src/models/NotificationModel.test.ts
Normal file
49
packages/server/src/models/NotificationModel.test.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { createUserAndSession, beforeAllDb, afterAllDb, beforeEachDb, models, expectThrow } from '../utils/testUtils';
|
||||
import { Notification, NotificationLevel } from '../db';
|
||||
|
||||
describe('NotificationModel', function() {
|
||||
|
||||
beforeAll(async () => {
|
||||
await beforeAllDb('NotificationModel');
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await afterAllDb();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await beforeEachDb();
|
||||
});
|
||||
|
||||
test('should require a user to create the notification', async function() {
|
||||
await expectThrow(async () => models().notification().add('test', NotificationLevel.Normal, 'test'));
|
||||
});
|
||||
|
||||
test('should create a notification', async function() {
|
||||
const { user } = await createUserAndSession(1, true);
|
||||
const model = models().notification({ userId: user.id });
|
||||
await model.add('test', NotificationLevel.Important, 'testing');
|
||||
const n: Notification = await model.loadByKey('test');
|
||||
expect(n.key).toBe('test');
|
||||
expect(n.message).toBe('testing');
|
||||
expect(n.level).toBe(NotificationLevel.Important);
|
||||
});
|
||||
|
||||
test('should create only one notification per key', async function() {
|
||||
const { user } = await createUserAndSession(1, true);
|
||||
const model = models().notification({ userId: user.id });
|
||||
await model.add('test', NotificationLevel.Important, 'testing');
|
||||
await model.add('test', NotificationLevel.Important, 'testing');
|
||||
expect((await model.all()).length).toBe(1);
|
||||
});
|
||||
|
||||
test('should mark a notification as read', async function() {
|
||||
const { user } = await createUserAndSession(1, true);
|
||||
const model = models().notification({ userId: user.id });
|
||||
await model.add('test', NotificationLevel.Important, 'testing');
|
||||
expect((await model.loadByKey('test')).read).toBe(0);
|
||||
await model.markAsRead('test');
|
||||
expect((await model.loadByKey('test')).read).toBe(1);
|
||||
});
|
||||
|
||||
});
|
51
packages/server/src/models/NotificationModel.ts
Normal file
51
packages/server/src/models/NotificationModel.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { Notification, NotificationLevel, Uuid } from '../db';
|
||||
import BaseModel from './BaseModel';
|
||||
|
||||
export default class NotificationModel extends BaseModel {
|
||||
|
||||
protected get tableName(): string {
|
||||
return 'notifications';
|
||||
}
|
||||
|
||||
public async add(key: string, level: NotificationLevel, message: string): Promise<Notification> {
|
||||
const n: Notification = await this.loadByKey(key);
|
||||
if (n) return n;
|
||||
return this.save({ key, message, level, owner_id: this.userId });
|
||||
}
|
||||
|
||||
public async markAsRead(key: string): Promise<void> {
|
||||
await this.db(this.tableName)
|
||||
.update({ read: 1 })
|
||||
.where('key', '=', key)
|
||||
.andWhere('owner_id', '=', this.userId);
|
||||
}
|
||||
|
||||
public loadByKey(key: string): Promise<Notification> {
|
||||
return this.db(this.tableName)
|
||||
.select(this.defaultFields)
|
||||
.where('key', '=', key)
|
||||
.andWhere('owner_id', '=', this.userId)
|
||||
.first();
|
||||
}
|
||||
|
||||
public allUnreadByUserId(userId: Uuid): Promise<Notification[]> {
|
||||
return this.db(this.tableName)
|
||||
.select(this.defaultFields)
|
||||
.where('owner_id', '=', userId)
|
||||
.andWhere('read', '=', 0)
|
||||
.orderBy('updated_time', 'asc');
|
||||
}
|
||||
|
||||
public closeUrl(id: Uuid): string {
|
||||
return `${this.baseUrl}/notifications/${id}`;
|
||||
}
|
||||
|
||||
public load(id: Uuid): Promise<Notification> {
|
||||
return this.db(this.tableName)
|
||||
.select(this.defaultFields)
|
||||
.where({ id: id })
|
||||
.andWhere('owner_id', '=', this.userId)
|
||||
.first();
|
||||
}
|
||||
|
||||
}
|
@ -14,6 +14,13 @@ export default class UserModel extends BaseModel {
|
||||
return this.db<User>(this.tableName).where(user).first();
|
||||
}
|
||||
|
||||
public async login(email: string, password: string): Promise<User> {
|
||||
const user = await this.loadByEmail(email);
|
||||
if (!user) return null;
|
||||
if (!auth.checkPassword(password, user.password)) return null;
|
||||
return user;
|
||||
}
|
||||
|
||||
public fromApiInput(object: User): User {
|
||||
const user: User = {};
|
||||
|
||||
@ -64,6 +71,10 @@ export default class UserModel extends BaseModel {
|
||||
return !!s[0].length && !!s[1].length;
|
||||
}
|
||||
|
||||
public async profileUrl(): Promise<string> {
|
||||
return `${this.baseUrl}/profile`;
|
||||
}
|
||||
|
||||
private async checkIsOwnerOrAdmin(userId: string): Promise<void> {
|
||||
if (!this.userId) throw new ErrorForbidden('no user is active');
|
||||
|
||||
|
@ -62,6 +62,7 @@ import UserModel from './UserModel';
|
||||
import PermissionModel from './PermissionModel';
|
||||
import SessionModel from './SessionModel';
|
||||
import ChangeModel from './ChangeModel';
|
||||
import NotificationModel from './NotificationModel';
|
||||
|
||||
export class Models {
|
||||
|
||||
@ -96,6 +97,11 @@ export class Models {
|
||||
public change(options: ModelOptions = null) {
|
||||
return new ChangeModel(this.db_, newModelFactory, this.baseUrl_, options);
|
||||
}
|
||||
|
||||
public notification(options: ModelOptions = null) {
|
||||
return new NotificationModel(this.db_, newModelFactory, this.baseUrl_, options);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default function newModelFactory(db: DbConnection, baseUrl: string): Models {
|
||||
|
20
packages/server/src/routes/index/notifications.ts
Normal file
20
packages/server/src/routes/index/notifications.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { SubPath, Route } from '../../utils/routeUtils';
|
||||
import { AppContext } from '../../utils/types';
|
||||
import { bodyFields, contextSessionId } from '../../utils/requestUtils';
|
||||
import { ErrorMethodNotAllowed } from '../../utils/errors';
|
||||
|
||||
const route: Route = {
|
||||
|
||||
exec: async function(path: SubPath, ctx: AppContext) {
|
||||
const sessionId = contextSessionId(ctx);
|
||||
|
||||
if (path.id && ctx.method === 'PATCH') {
|
||||
return ctx.controllers.indexNotifications().patchOne(sessionId, path.id, await bodyFields(ctx.req));
|
||||
}
|
||||
|
||||
throw new ErrorMethodNotAllowed();
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
export default route;
|
@ -10,6 +10,7 @@ import indexProfileRoute from './index/profile';
|
||||
import indexUsersRoute from './index/users';
|
||||
import indexUserRoute from './index/user';
|
||||
import indexFilesRoute from './index/files';
|
||||
import indexNotificationsRoute from './index/notifications';
|
||||
import defaultRoute from './default';
|
||||
|
||||
const routes: Routes = {
|
||||
@ -24,6 +25,7 @@ const routes: Routes = {
|
||||
'users': indexUsersRoute,
|
||||
'user': indexUserRoute,
|
||||
'files': indexFilesRoute,
|
||||
'notifications': indexNotificationsRoute,
|
||||
|
||||
'': defaultRoute,
|
||||
};
|
||||
|
@ -46,7 +46,7 @@ class MustacheService {
|
||||
return output;
|
||||
}
|
||||
|
||||
public async renderView(view: View): Promise<string> {
|
||||
public async renderView(view: View, globalParams: any = null): Promise<string> {
|
||||
const partials = view.partials || [];
|
||||
const cssFiles = this.resolvesFilePaths('css', view.cssFiles || []);
|
||||
const jsFiles = this.resolvesFilePaths('js', view.jsFiles || []);
|
||||
@ -59,17 +59,22 @@ class MustacheService {
|
||||
|
||||
const filePath = `${config().viewDir}/${view.path}.mustache`;
|
||||
|
||||
globalParams = {
|
||||
...this.defaultLayoutOptions,
|
||||
...globalParams,
|
||||
};
|
||||
|
||||
const contentHtml = Mustache.render(
|
||||
await this.loadTemplateContent(filePath),
|
||||
{
|
||||
...view.content,
|
||||
global: this.defaultLayoutOptions,
|
||||
global: globalParams,
|
||||
},
|
||||
partialContents
|
||||
);
|
||||
|
||||
const layoutView: any = Object.assign({}, {
|
||||
global: this.defaultLayoutOptions,
|
||||
global: globalParams,
|
||||
pageName: view.name,
|
||||
contentHtml: contentHtml,
|
||||
cssFiles: cssFiles,
|
||||
@ -80,23 +85,6 @@ class MustacheService {
|
||||
return Mustache.render(await this.loadTemplateContent(this.defaultLayoutPath), layoutView, partialContents);
|
||||
}
|
||||
|
||||
// public async render(path: string, view: any, options: RenderOptions = null): Promise<string> {
|
||||
// const partials = options && options.partials ? options.partials : {};
|
||||
// const cssFiles = this.resolvesFilePaths('css', options && options.cssFiles ? options.cssFiles : []);
|
||||
// const jsFiles = this.resolvesFilePaths('js', options && options.jsFiles ? options.jsFiles : []);
|
||||
|
||||
// const filePath = `${config().viewDir}/${path}.mustache`;
|
||||
// const contentHtml = Mustache.render(await this.loadTemplateContent(filePath), { ...view, global: this.defaultLayoutOptions }, partials);
|
||||
|
||||
// const layoutView: any = Object.assign({}, this.defaultLayoutOptions, {
|
||||
// contentHtml: contentHtml,
|
||||
// cssFiles: cssFiles,
|
||||
// jsFiles: jsFiles,
|
||||
// });
|
||||
|
||||
// return Mustache.render(await this.loadTemplateContent(this.defaultLayoutPath), layoutView);
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
const mustacheService = new MustacheService();
|
||||
|
@ -28,6 +28,7 @@ const config = {
|
||||
'main.files': 'WithDates, WithUuid',
|
||||
'main.api_clients': 'WithDates, WithUuid',
|
||||
'main.changes': 'WithDates, WithUuid',
|
||||
'main.notifications': 'WithDates, WithUuid',
|
||||
},
|
||||
};
|
||||
|
||||
@ -66,6 +67,7 @@ function createTypeString(table: any) {
|
||||
if (name === 'item_type') type = 'ItemType';
|
||||
if (table.name === 'files' && name === 'content') type = 'Buffer';
|
||||
if (table.name === 'changes' && name === 'type') type = 'ChangeType';
|
||||
if (table.name === 'notifications' && name === 'level') type = 'NotificationLevel';
|
||||
if ((name === 'id' || name.endsWith('_id') || name === 'uuid') && type === 'string') type = 'Uuid';
|
||||
|
||||
colStrings.push(`\t${name}?: ${type};`);
|
||||
|
@ -9,6 +9,9 @@ export default function(name: string, owner: User = null): View {
|
||||
content: {
|
||||
owner,
|
||||
},
|
||||
partials: ['navbar'],
|
||||
partials: [
|
||||
'navbar',
|
||||
'notifications',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
@ -43,3 +43,7 @@ export function contextSessionId(ctx: AppContext): string {
|
||||
if (!id) throw new ErrorForbidden('Invalid or missing session');
|
||||
return id;
|
||||
}
|
||||
|
||||
export function isApiRequest(ctx: AppContext): boolean {
|
||||
return ctx.path.indexOf('/api/') === 0;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { User, Session, DbConnection, connectDb, disconnectDb, File } from '../db';
|
||||
import { User, Session, DbConnection, connectDb, disconnectDb, File, truncateTables } from '../db';
|
||||
import { createDb } from '../tools/dbTools';
|
||||
import modelFactory from '../models/factory';
|
||||
import controllerFactory from '../controllers/factory';
|
||||
@ -35,17 +35,9 @@ export async function afterAllDb() {
|
||||
}
|
||||
|
||||
export async function beforeEachDb() {
|
||||
await clearDatabase();
|
||||
await truncateTables(db_);
|
||||
}
|
||||
|
||||
export const clearDatabase = async function(): Promise<void> {
|
||||
await db_('sessions').truncate();
|
||||
await db_('users').truncate();
|
||||
await db_('permissions').truncate();
|
||||
await db_('files').truncate();
|
||||
await db_('changes').truncate();
|
||||
};
|
||||
|
||||
export const testAssetDir = `${packageRootDir}/assets/tests`;
|
||||
|
||||
interface UserAndSession {
|
||||
|
@ -1,12 +1,29 @@
|
||||
import { LoggerWrapper } from '@joplin/lib/Logger';
|
||||
import * as Koa from 'koa';
|
||||
import { Controllers } from '../controllers/factory';
|
||||
import { DbConnection } from '../db';
|
||||
import { DbConnection, Uuid } from '../db';
|
||||
import { Models } from '../models/factory';
|
||||
|
||||
export enum Env {
|
||||
Dev = 'dev',
|
||||
Prod = 'prod',
|
||||
BuildTypes = 'buildTypes',
|
||||
}
|
||||
|
||||
export interface NotificationView {
|
||||
id: Uuid;
|
||||
messageHtml: string;
|
||||
level: string;
|
||||
closeUrl: string;
|
||||
}
|
||||
|
||||
export interface AppContext extends Koa.Context {
|
||||
env: Env;
|
||||
db: DbConnection;
|
||||
models: Models;
|
||||
controllers: Controllers;
|
||||
appLogger(): LoggerWrapper;
|
||||
notifications: NotificationView[];
|
||||
}
|
||||
|
||||
export interface DatabaseConfig {
|
||||
@ -27,3 +44,5 @@ export interface Config {
|
||||
logDir: string;
|
||||
database: DatabaseConfig;
|
||||
}
|
||||
|
||||
export type KoaNext = ()=> Promise<void>;
|
||||
|
@ -13,6 +13,7 @@
|
||||
</head>
|
||||
<body class="page-{{{pageName}}}">
|
||||
{{> navbar}}
|
||||
{{> notifications}}
|
||||
<main class="main">
|
||||
{{{contentHtml}}}
|
||||
</main>
|
||||
|
28
packages/server/src/views/partials/notifications.mustache
Normal file
28
packages/server/src/views/partials/notifications.mustache
Normal file
@ -0,0 +1,28 @@
|
||||
{{#global.notifications}}
|
||||
<div class="notification is-{{level}}" id="notification-{{id}}">
|
||||
<button data-close-url="{{closeUrl}}" data-id="{{id}}" class="delete close-notification-button"></button>
|
||||
{{{messageHtml}}}
|
||||
</div>
|
||||
{{/global.notifications}}
|
||||
|
||||
<script>
|
||||
onDocumentReady(function() {
|
||||
const buttons = document.getElementsByClassName('close-notification-button');
|
||||
|
||||
for (const button of buttons) {
|
||||
button.addEventListener('click', function(event) {
|
||||
const closeUrl = button.dataset.closeUrl;
|
||||
const notificationId = button.dataset.id;
|
||||
fetch(closeUrl, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
read: 1,
|
||||
}),
|
||||
});
|
||||
|
||||
document.getElementById('notification-' + notificationId).style.display = 'none';
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
Loading…
Reference in New Issue
Block a user