Merge remote-tracking branch 'origin/stable' into next

# Conflicts:
#	package-lock.json
#	package.json
#	src/public/app/entities/note_short.js
#	src/public/app/services/protected_session.js
#	src/routes/api/login.js
This commit is contained in:
zadam 2021-05-08 10:58:38 +02:00
commit 051b9dff21
14 changed files with 107 additions and 83 deletions

View File

@ -58,7 +58,7 @@
"mime-types": "2.1.30", "mime-types": "2.1.30",
"multer": "1.4.2", "multer": "1.4.2",
"node-abi": "2.26.0", "node-abi": "2.26.0",
"open": "8.0.6", "open": "8.0.8",
"portscanner": "2.2.0", "portscanner": "2.2.0",
"rand-token": "1.0.1", "rand-token": "1.0.1",
"request": "^2.88.2", "request": "^2.88.2",
@ -80,8 +80,8 @@
}, },
"devDependencies": { "devDependencies": {
"cross-env": "7.0.3", "cross-env": "7.0.3",
"electron": "13.0.0-beta.18", "electron": "13.0.0-beta.23",
"electron-builder": "22.10.5", "electron-builder": "22.11.1",
"electron-packager": "15.2.0", "electron-packager": "15.2.0",
"electron-rebuild": "2.3.5", "electron-rebuild": "2.3.5",
"esm": "3.2.25", "esm": "3.2.25",
@ -90,7 +90,7 @@
"lorem-ipsum": "2.0.3", "lorem-ipsum": "2.0.3",
"rcedit": "3.0.0", "rcedit": "3.0.0",
"webpack": "5.36.2", "webpack": "5.36.2",
"webpack-cli": "4.6.0" "webpack-cli": "4.7.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"electron-installer-debian": "3.1.0" "electron-installer-debian": "3.1.0"

View File

@ -3,6 +3,8 @@ import noteAttributeCache from "../services/note_attribute_cache.js";
import ws from "../services/ws.js"; import ws from "../services/ws.js";
import options from "../services/options.js"; import options from "../services/options.js";
import froca from "../services/froca.js"; import froca from "../services/froca.js";
import treeCache from "../services/tree_cache.js";
import bundle from "../services/bundle.js";
const LABEL = 'label'; const LABEL = 'label';
const RELATION = 'relation'; const RELATION = 'relation';
@ -701,6 +703,55 @@ class NoteShort {
const labels = this.getLabels('workspaceTabBackgroundColor'); const labels = this.getLabels('workspaceTabBackgroundColor');
return labels.length > 0 ? labels[0].value : ""; return labels.length > 0 ? labels[0].value : "";
} }
/** @returns {boolean} true if this note is JavaScript (code or attachment) */
isJavaScript() {
return (this.type === "code" || this.type === "file")
&& (this.mime.startsWith("application/javascript")
|| this.mime === "application/x-javascript"
|| this.mime === "text/javascript");
}
/** @returns {boolean} true if this note is HTML */
isHtml() {
return (this.type === "code" || this.type === "file" || this.type === "render") && this.mime === "text/html";
}
/** @returns {string|null} JS script environment - either "frontend" or "backend" */
getScriptEnv() {
if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith('env=frontend'))) {
return "frontend";
}
if (this.type === 'render') {
return "frontend";
}
if (this.isJavaScript() && this.mime.endsWith('env=backend')) {
return "backend";
}
return null;
}
async executeScript() {
if (!this.isJavaScript()) {
throw new Error(`Note ${this.noteId} is of type ${this.type} and mime ${this.mime} and thus cannot be executed`);
}
const env = this.getScriptEnv();
if (env === "frontend") {
const bundleService = (await import("../services/bundle.js")).default;
await bundleService.getAndExecuteBundle(this.noteId);
}
else if (env === "backend") {
await server.post('script/run/' + this.noteId);
}
else {
throw new Error(`Unrecognized env type ${env} for note ${this.noteId}`);
}
}
} }
export default NoteShort; export default NoteShort;

View File

@ -119,8 +119,6 @@ const appContext = new AppContext(window.glob.isMainWindow);
// we should save all outstanding changes before the page/app is closed // we should save all outstanding changes before the page/app is closed
$(window).on('beforeunload', () => { $(window).on('beforeunload', () => {
protectedSessionHolder.resetSessionCookie();
let allSaved = true; let allSaved = true;
appContext.beforeUnloadListeners = appContext.beforeUnloadListeners.filter(wr => !!wr.deref()); appContext.beforeUnloadListeners = appContext.beforeUnloadListeners.filter(wr => !!wr.deref());

View File

@ -216,6 +216,7 @@ export default class Entrypoints extends Component {
return; return;
} }
// TODO: use note.executeScript()
if (note.mime.endsWith("env=frontend")) { if (note.mime.endsWith("env=frontend")) {
await bundleService.getAndExecuteBundle(note.noteId); await bundleService.getAndExecuteBundle(note.noteId);
} else if (note.mime.endsWith("env=backend")) { } else if (note.mime.endsWith("env=backend")) {

View File

@ -68,8 +68,6 @@ function setupGlobs() {
return false; return false;
}; };
protectedSessionHolder.setProtectedSessionId(null);
for (const appCssNoteId of glob.appCssNoteIds || []) { for (const appCssNoteId of glob.appCssNoteIds || []) {
libraryLoader.requireCss(`api/notes/download/${appCssNoteId}`); libraryLoader.requireCss(`api/notes/download/${appCssNoteId}`);
} }

View File

@ -1,16 +1,16 @@
import utils from './utils.js';
import server from './server.js'; import server from './server.js';
import protectedSessionHolder from './protected_session_holder.js'; import protectedSessionHolder from './protected_session_holder.js';
import toastService from "./toast.js"; import toastService from "./toast.js";
import ws from "./ws.js"; import ws from "./ws.js";
import appContext from "./app_context.js"; import appContext from "./app_context.js";
import froca from "./froca.js"; import froca from "./froca.js";
import utils from "./utils.js";
let protectedSessionDeferred = null; let protectedSessionDeferred = null;
async function leaveProtectedSession() { async function leaveProtectedSession() {
if (protectedSessionHolder.isProtectedSessionAvailable()) { if (protectedSessionHolder.isProtectedSessionAvailable()) {
protectedSessionHolder.resetProtectedSession(); await protectedSessionHolder.resetProtectedSession();
} }
} }
@ -41,37 +41,37 @@ async function reloadData() {
} }
async function setupProtectedSession(password) { async function setupProtectedSession(password) {
const response = await enterProtectedSessionOnServer(password); const response = await server.post('login/protected', { password: password });
if (!response.success) { if (!response.success) {
toastService.showError("Wrong password.", 3000); toastService.showError("Wrong password.", 3000);
return; return;
} }
protectedSessionHolder.setProtectedSessionId(response.protectedSessionId); protectedSessionHolder.enableProtectedSession();
protectedSessionHolder.touchProtectedSession(); }
await reloadData(); ws.subscribeToMessages(async message => {
if (message.type === 'protectedSessionLogin') {
await reloadData();
await appContext.triggerEvent('frocaReloaded'); await appContext.triggerEvent('frocaReloaded');
appContext.triggerEvent('protectedSessionStarted'); appContext.triggerEvent('protectedSessionStarted');
if (protectedSessionDeferred !== null) { if (protectedSessionDeferred !== null) {
import("../dialogs/protected_session.js").then(dialog => dialog.close()); import("../dialogs/protected_session.js").then(dialog => dialog.close());
protectedSessionDeferred.resolve(true); protectedSessionDeferred.resolve(true);
protectedSessionDeferred = null; protectedSessionDeferred = null;
}
toastService.showMessage("Protected session has been started.");
} }
else if (message.type === 'protectedSessionLogout') {
toastService.showMessage("Protected session has been started."); utils.reloadApp();
} }
});
async function enterProtectedSessionOnServer(password) {
return await server.post('login/protected', {
password: password
});
}
async function protectNote(noteId, protect, includingSubtree) { async function protectNote(noteId, protect, includingSubtree) {
await enterProtectedSession(); await enterProtectedSession();

View File

@ -1,9 +1,6 @@
import utils from "./utils.js";
import options from './options.js'; import options from './options.js';
import server from "./server.js"; import server from "./server.js";
const PROTECTED_SESSION_ID_KEY = 'protectedSessionId';
let lastProtectedSessionOperationDate = 0; let lastProtectedSessionOperationDate = 0;
setInterval(() => { setInterval(() => {
@ -15,32 +12,23 @@ setInterval(() => {
} }
}, 10000); }, 10000);
function setProtectedSessionId(id) { function enableProtectedSession() {
// using session cookie so that it disappears after browser/tab is closed glob.isProtectedSessionAvailable = true;
utils.setSessionCookie(PROTECTED_SESSION_ID_KEY, id);
}
function resetSessionCookie() { touchProtectedSession();
utils.setSessionCookie(PROTECTED_SESSION_ID_KEY, null);
} }
async function resetProtectedSession() { async function resetProtectedSession() {
resetSessionCookie();
await server.post("logout/protected"); await server.post("logout/protected");
utils.reloadApp();
} }
function isProtectedSessionAvailable() { function isProtectedSessionAvailable() {
return !!utils.getCookie(PROTECTED_SESSION_ID_KEY); return glob.isProtectedSessionAvailable;
} }
function touchProtectedSession() { function touchProtectedSession() {
if (isProtectedSessionAvailable()) { if (isProtectedSessionAvailable()) {
lastProtectedSessionOperationDate = Date.now(); lastProtectedSessionOperationDate = Date.now();
setProtectedSessionId(utils.getCookie(PROTECTED_SESSION_ID_KEY));
} }
} }
@ -51,8 +39,7 @@ function touchProtectedSessionIfNecessary(note) {
} }
export default { export default {
setProtectedSessionId, enableProtectedSession,
resetSessionCookie,
resetProtectedSession, resetProtectedSession,
isProtectedSessionAvailable, isProtectedSessionAvailable,
touchProtectedSession, touchProtectedSession,

View File

@ -1,5 +1,6 @@
import BasicWidget from "./basic_widget.js"; import BasicWidget from "./basic_widget.js";
import HistoryNavigationWidget from "./history_navigation.js"; import HistoryNavigationWidget from "./history_navigation.js";
import protectedSessionHolder from "../services/protected_session_holder.js";
import protectedSessionService from "../services/protected_session.js"; import protectedSessionService from "../services/protected_session.js";
import QuickSearchWidget from "./quick_search.js"; import QuickSearchWidget from "./quick_search.js";
@ -68,8 +69,7 @@ const TPL = `
</button> </button>
<button class="btn btn-sm leave-protected-session-button noborder" <button class="btn btn-sm leave-protected-session-button noborder"
title="Leave protected session so that protected notes are not accessible any more." title="Leave protected session so that protected notes are not accessible any more.">
style="display: none;">
<span class="bx bx-log-out"></span> <span class="bx bx-log-out"></span>
Leave protected session Leave protected session
@ -96,9 +96,11 @@ export default class StandardTopWidget extends BasicWidget {
this.$enterProtectedSessionButton = this.$widget.find(".enter-protected-session-button"); this.$enterProtectedSessionButton = this.$widget.find(".enter-protected-session-button");
this.$enterProtectedSessionButton.on('click', protectedSessionService.enterProtectedSession); this.$enterProtectedSessionButton.on('click', protectedSessionService.enterProtectedSession);
this.$enterProtectedSessionButton.toggle(!protectedSessionHolder.isProtectedSessionAvailable());
this.$leaveProtectedSessionButton = this.$widget.find(".leave-protected-session-button"); this.$leaveProtectedSessionButton = this.$widget.find(".leave-protected-session-button");
this.$leaveProtectedSessionButton.on('click', protectedSessionService.leaveProtectedSession); this.$leaveProtectedSessionButton.on('click', protectedSessionService.leaveProtectedSession);
this.$leaveProtectedSessionButton.toggle(protectedSessionHolder.isProtectedSessionAvailable());
return this.$widget; return this.$widget;
} }

View File

@ -14,6 +14,10 @@ button.btn, button.btn-sm {
font-size: inherit; font-size: inherit;
} }
.btn-micro {
padding: 0 10px 0 10px;
}
.input-group-text { .input-group-text {
background-color: var(--accented-background-color) !important; background-color: var(--accented-background-color) !important;
color: var(--muted-text-color) !important; color: var(--muted-text-color) !important;

View File

@ -8,11 +8,12 @@ const passwordEncryptionService = require('../../services/password_encryption');
const protectedSessionService = require('../../services/protected_session'); const protectedSessionService = require('../../services/protected_session');
const appInfo = require('../../services/app_info'); const appInfo = require('../../services/app_info');
const eventService = require('../../services/events'); const eventService = require('../../services/events');
const cls = require('../../services/cls');
const sqlInit = require('../../services/sql_init'); const sqlInit = require('../../services/sql_init');
const sql = require('../../services/sql'); const sql = require('../../services/sql');
const optionService = require('../../services/options'); const optionService = require('../../services/options');
const ApiToken = require('../../services/becca/entities/api_token.js'); const ApiToken = require('../../services/becca/entities/api_token.js');
const ApiToken = require('../../entities/api_token');
const ws = require("../../services/ws.js");
function loginSync(req) { function loginSync(req) {
if (!sqlInit.schemaExists()) { if (!sqlInit.schemaExists()) {
@ -65,16 +66,14 @@ function loginToProtectedSession(req) {
const decryptedDataKey = passwordEncryptionService.getDataKey(password); const decryptedDataKey = passwordEncryptionService.getDataKey(password);
const protectedSessionId = protectedSessionService.setDataKey(decryptedDataKey); protectedSessionService.setDataKey(decryptedDataKey);
// this is set here so that event handlers have access to the protected session
cls.set('protectedSessionId', protectedSessionId);
eventService.emit(eventService.ENTER_PROTECTED_SESSION); eventService.emit(eventService.ENTER_PROTECTED_SESSION);
ws.sendMessageToAllClients({ type: 'protectedSessionLogin' });
return { return {
success: true, success: true
protectedSessionId: protectedSessionId
}; };
} }
@ -82,6 +81,8 @@ function logoutFromProtectedSession() {
protectedSessionService.resetDataKey(); protectedSessionService.resetDataKey();
eventService.emit(eventService.LEAVE_PROTECTED_SESSION); eventService.emit(eventService.LEAVE_PROTECTED_SESSION);
ws.sendMessageToAllClients({ type: 'protectedSessionLogout' });
} }
function token(req) { function token(req) {

View File

@ -7,6 +7,7 @@ const config = require('../services/config');
const optionService = require('../services/options'); const optionService = require('../services/options');
const log = require('../services/log'); const log = require('../services/log');
const env = require('../services/env'); const env = require('../services/env');
const protectedSessionService = require("../services/protected_session.js");
function index(req, res) { function index(req, res) {
const options = optionService.getOptionsMap(); const options = optionService.getOptionsMap();
@ -30,7 +31,8 @@ function index(req, res) {
appCssNoteIds: getAppCssNoteIds(), appCssNoteIds: getAppCssNoteIds(),
isDev: env.isDev(), isDev: env.isDev(),
isMainWindow: !req.query.extra, isMainWindow: !req.query.extra,
extraHoistedNoteId: req.query.extraHoistedNoteId extraHoistedNoteId: req.query.extraHoistedNoteId,
isProtectedSessionAvailable: protectedSessionService.isProtectedSessionAvailable()
}); });
} }

View File

@ -93,7 +93,6 @@ function route(method, path, middleware, routeHandler, resultHandler, transactio
cls.set('sourceId', req.headers['trilium-source-id']); cls.set('sourceId', req.headers['trilium-source-id']);
cls.set('localNowDateTime', req.headers['trilium-local-now-datetime']); cls.set('localNowDateTime', req.headers['trilium-local-now-datetime']);
cls.set('hoistedNoteId', req.headers['trilium-hoisted-note-id'] || 'root'); cls.set('hoistedNoteId', req.headers['trilium-hoisted-note-id'] || 'root');
protectedSessionService.setProtectedSessionId(req);
const cb = () => routeHandler(req, res, next); const cb = () => routeHandler(req, res, next);

View File

@ -1,42 +1,24 @@
"use strict"; "use strict";
const utils = require('./utils');
const log = require('./log'); const log = require('./log');
const dataEncryptionService = require('./data_encryption'); const dataEncryptionService = require('./data_encryption');
const cls = require('./cls');
let dataKeyMap = {}; let dataKey = null;
function setDataKey(decryptedDataKey) { function setDataKey(decryptedDataKey) {
const protectedSessionId = utils.randomSecureToken(32); dataKey = Array.from(decryptedDataKey);
dataKeyMap[protectedSessionId] = Array.from(decryptedDataKey); // can't store buffer in session
return protectedSessionId;
}
function setProtectedSessionId(req) {
cls.set('protectedSessionId', req.cookies.protectedSessionId);
}
function getProtectedSessionId() {
return cls.get('protectedSessionId');
} }
function getDataKey() { function getDataKey() {
const protectedSessionId = getProtectedSessionId(); return dataKey;
return dataKeyMap[protectedSessionId];
} }
function resetDataKey() { function resetDataKey() {
dataKeyMap = {}; dataKey = null;
} }
function isProtectedSessionAvailable() { function isProtectedSessionAvailable() {
const protectedSessionId = getProtectedSessionId(); return !!dataKey;
return !!dataKeyMap[protectedSessionId];
} }
function decryptNotes(notes) { function decryptNotes(notes) {
@ -74,12 +56,10 @@ function decryptString(cipherText) {
module.exports = { module.exports = {
setDataKey, setDataKey,
getDataKey,
resetDataKey, resetDataKey,
isProtectedSessionAvailable, isProtectedSessionAvailable,
encrypt, encrypt,
decrypt, decrypt,
decryptString, decryptString,
decryptNotes, decryptNotes
setProtectedSessionId
}; };

View File

@ -56,6 +56,7 @@
appCssNoteIds: <%- JSON.stringify(appCssNoteIds) %>, appCssNoteIds: <%- JSON.stringify(appCssNoteIds) %>,
isMainWindow: <%= isMainWindow %>, isMainWindow: <%= isMainWindow %>,
extraHoistedNoteId: '<%= extraHoistedNoteId %>', extraHoistedNoteId: '<%= extraHoistedNoteId %>',
isProtectedSessionAvailable: <%= isProtectedSessionAvailable %>,
}; };
</script> </script>