Adding basic MFA in the form of TOTP

This commit is contained in:
Brandon 2024-05-02 10:16:45 -07:00
parent a68b75f069
commit 3fb4d95fd7
12 changed files with 2154 additions and 874 deletions

30
package-lock.json generated
View File

@ -73,6 +73,7 @@
"semver": "7.6.0",
"serve-favicon": "2.5.0",
"session-file-store": "1.5.0",
"speakeasy": "^2.0.0",
"split.js": "1.6.5",
"stream-throttle": "0.1.3",
"striptags": "3.2.0",
@ -2708,6 +2709,11 @@
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
},
"node_modules/base32.js": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.0.1.tgz",
"integrity": "sha512-EGHIRiegFa62/SsA1J+Xs2tIzludPdzM064N9wjbiEgHnGnJ1V0WEpA4pEwCYT5nDvZk3ubf0shqaCS7k6xeUQ=="
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@ -11795,6 +11801,17 @@
"integrity": "sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA==",
"dev": true
},
"node_modules/speakeasy": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/speakeasy/-/speakeasy-2.0.0.tgz",
"integrity": "sha512-lW2A2s5LKi8rwu77ewisuUOtlCydF/hmQSOJjpTqTj1gZLkNgTaYnyvfxy2WBr4T/h+9c4g8HIITfj83OkFQFw==",
"dependencies": {
"base32.js": "0.0.1"
},
"engines": {
"node": ">= 0.10.0"
}
},
"node_modules/split.js": {
"version": "1.6.5",
"resolved": "https://registry.npmjs.org/split.js/-/split.js-1.6.5.tgz",
@ -15542,6 +15559,11 @@
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
},
"base32.js": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.0.1.tgz",
"integrity": "sha512-EGHIRiegFa62/SsA1J+Xs2tIzludPdzM064N9wjbiEgHnGnJ1V0WEpA4pEwCYT5nDvZk3ubf0shqaCS7k6xeUQ=="
},
"base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@ -22382,6 +22404,14 @@
"integrity": "sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA==",
"dev": true
},
"speakeasy": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/speakeasy/-/speakeasy-2.0.0.tgz",
"integrity": "sha512-lW2A2s5LKi8rwu77ewisuUOtlCydF/hmQSOJjpTqTj1gZLkNgTaYnyvfxy2WBr4T/h+9c4g8HIITfj83OkFQFw==",
"requires": {
"base32.js": "0.0.1"
}
},
"split.js": {
"version": "1.6.5",
"resolved": "https://registry.npmjs.org/split.js/-/split.js-1.6.5.tgz",

View File

@ -97,6 +97,7 @@
"semver": "7.6.0",
"serve-favicon": "2.5.0",
"session-file-store": "1.5.0",
"speakeasy": "^2.0.0",
"split.js": "1.6.5",
"stream-throttle": "0.1.3",
"striptags": "3.2.0",

View File

@ -32,6 +32,7 @@ import DatabaseAnonymizationOptions from "./options/advanced/database_anonymizat
import BackendLogWidget from "./content/backend_log.js";
import AttachmentErasureTimeoutOptions from "./options/other/attachment_erasure_timeout.js";
import RibbonOptions from "./options/appearance/ribbon.js";
import MultiFactorAuthenticationOptions from "./options/multi_factor_authentication.js";
const TPL = `<div class="note-detail-content-widget note-detail-printable">
<style>
@ -59,21 +60,22 @@ const CONTENT_WIDGETS = {
ZoomFactorOptions,
NativeTitleBarOptions,
MaxContentWidthOptions,
RibbonOptions
RibbonOptions,
],
_optionsShortcuts: [KeyboardShortcutsOptions],
_optionsTextNotes: [
HeadingStyleOptions,
TableOfContentsOptions,
HighlightsListOptions,
TextAutoReadOnlySizeOptions
TextAutoReadOnlySizeOptions,
],
_optionsCodeNotes: [
VimKeyBindingsOptions,
WrapLinesOptions,
CodeAutoReadOnlySizeOptions,
CodeMimeTypesOptions
CodeMimeTypesOptions,
],
_optionsMFA: [MultiFactorAuthenticationOptions],
_optionsImages: [ImageOptions],
_optionsSpellcheck: [SpellcheckOptions],
_optionsPassword: [PasswordOptions],
@ -86,20 +88,22 @@ const CONTENT_WIDGETS = {
NoteErasureTimeoutOptions,
AttachmentErasureTimeoutOptions,
RevisionsSnapshotIntervalOptions,
NetworkConnectionsOptions
NetworkConnectionsOptions,
],
_optionsAdvanced: [
DatabaseIntegrityCheckOptions,
ConsistencyChecksOptions,
DatabaseAnonymizationOptions,
AdvancedSyncOptions,
VacuumDatabaseOptions
VacuumDatabaseOptions,
],
_backendLog: [ BackendLogWidget ]
_backendLog: [BackendLogWidget],
};
export default class ContentWidgetTypeWidget extends TypeWidget {
static getType() { return "contentWidget"; }
static getType() {
return "contentWidget";
}
doRender() {
this.$widget = $(TPL);
@ -118,7 +122,9 @@ export default class ContentWidgetTypeWidget extends TypeWidget {
for (const clazz of contentWidgets) {
const widget = new clazz();
await widget.handleEvent('setNoteContext', { noteContext: this.noteContext });
await widget.handleEvent("setNoteContext", {
noteContext: this.noteContext,
});
this.child(widget);
this.$content.append(widget.render());

View File

@ -0,0 +1,109 @@
import server from "../../../services/server.js";
import protectedSessionHolder from "../../../services/protected_session_holder.js";
import toastService from "../../../services/toast.js";
import OptionsWidget from "./options_widget.js";
// import { randomBytes } from "crypto";
// import { generateSecret } from "../../../services/totp.js";
// const speakeasy = require("speakeasy");
// ${speakeasy.generateSecret().base32}
const TPL = `
<div class="options-section">
<h4 class="mfa-heading"></h4>
<div class="alert alert-warning" role="alert" style="font-weight: bold; color: red !important;">
Use TOTP (Time-based One-Time Password) to safeguard your data in this application because it adds an additional layer of security by generating unique passcodes that expire quickly, making it harder for unauthorized access. TOTP also reduces the risk of account compromise through common threats like phishing attacks or password breaches.
</div>
<span class="totp-secret" > </span>
<br>
<button class="regenerate-totp"> Regenerate TOTP Secret </button>
</div>`;
export default class MultiFactorAuthenticationOptions extends OptionsWidget {
doRender() {
this.$widget = $(TPL);
this.$mfaHeadding = this.$widget.find(".mfa-heading");
this.$regenerateTotpButton = this.$widget.find(".regenerate-totp");
this.$totpSecret = this.$widget.find(".totp-secret");
this.$mfaHeadding.text("Multi-Factor Authentication");
this.generateKey();
// var gen = require("speakeasy");
// toastService.showMessage("***REMOVED***");
this.$regenerateTotpButton.on("click", async () => {
this.generateKey();
});
this.$protectedSessionTimeout = this.$widget.find(
".protected-session-timeout-in-seconds"
);
this.$protectedSessionTimeout.on("change", () =>
this.updateOption(
"protectedSessionTimeout",
this.$protectedSessionTimeout.val()
)
);
}
async generateKey() {
server.get("totp/generate").then((result) => {
if (result.success) {
// password changed so current protected session is invalid and needs to be cleared
this.$totpSecret.text(result.message);
} else {
toastService.showError(result.message);
}
});
}
optionsLoaded(options) {
const isPasswordSet = options.isPasswordSet === "true";
this.$widget.find(".old-password-form-group").toggle(isPasswordSet);
this.$savePasswordButton.text(
isPasswordSet ? "Change fff Password" : "Set Password"
);
this.$protectedSessionTimeout.val(options.protectedSessionTimeout);
}
save() {
const oldPassword = this.$oldPassword.val();
const newPassword1 = this.$newPassword1.val();
const newPassword2 = this.$newPassword2.val();
this.$oldPassword.val("");
this.$newPassword1.val("");
this.$newPassword2.val("");
if (newPassword1 !== newPassword2) {
toastService.showError("New passwords are not the same.");
return false;
}
server
.post("password/change", {
current_password: oldPassword,
new_password: newPassword1,
})
.then((result) => {
if (result.success) {
toastService.showError(
"Password has been changed. Trilium will be reloaded after you press OK."
);
// password changed so current protected session is invalid and needs to be cleared
protectedSessionHolder.resetProtectedSession();
} else {
toastService.showError(result.message);
}
});
return false;
}
}

24
src/routes/api/totp.ts Normal file
View File

@ -0,0 +1,24 @@
const speakeasy = require("speakeasy");
function verifyOTPToken(guessedToken: any) {
console.log("[" + guessedToken + "]");
console.log(typeof guessedToken);
const tokenValidates = speakeasy.totp.verify({
secret: process.env.MFA_SECRET,
encoding: "base32",
token: guessedToken,
window: 1,
});
return tokenValidates;
}
function generateSecret() {
return { success: "true", message: speakeasy.generateSecret().base32 };
}
export = {
verifyOTPToken,
generateSecret,
};

File diff suppressed because it is too large Load Diff

View File

@ -1,28 +1,45 @@
"use strict";
import optionService = require('../options');
import crypto = require('crypto');
import optionService = require("../options");
import crypto = require("crypto");
function getVerificationHash(password: crypto.BinaryLike) {
const salt = optionService.getOption('passwordVerificationSalt');
const salt = optionService.getOption("passwordVerificationSalt");
return getScryptHash(password, salt);
}
function getPasswordDerivedKey(password: crypto.BinaryLike) {
const salt = optionService.getOption('passwordDerivedKeySalt');
const salt = optionService.getOption("passwordDerivedKeySalt");
return getScryptHash(password, salt);
}
function getScryptHash(password: crypto.BinaryLike, salt: crypto.BinaryLike) {
const hashed = crypto.scryptSync(password, salt, 32,
{N: 16384, r:8, p:1});
const hashed = crypto.scryptSync(password, salt, 32, {
N: 16384,
r: 8,
p: 1,
});
return hashed;
}
function getTotpSecretVerificationHash(secret: crypto.BinaryLike) {
const salt = optionService.getOption("totpSecretVerificationSalt");
return getScryptHash(secret, salt);
}
function getTotpSecretDerivedKey(secret: crypto.BinaryLike) {
const salt = optionService.getOption("totpSecretDerivedKeySalt");
return getScryptHash(secret, salt);
}
export = {
getVerificationHash,
getPasswordDerivedKey
getPasswordDerivedKey,
getTotpSecretVerificationHash,
getTotpSecretDerivedKey,
};

View File

@ -1,12 +1,16 @@
import optionService = require('../options');
import myScryptService = require('./my_scrypt');
import utils = require('../utils');
import dataEncryptionService = require('./data_encryption');
import optionService = require("../options");
import myScryptService = require("./my_scrypt");
import utils = require("../utils");
import dataEncryptionService = require("./data_encryption");
function verifyPassword(password: string) {
const givenPasswordHash = utils.toBase64(myScryptService.getVerificationHash(password));
const givenPasswordHash = utils.toBase64(
myScryptService.getVerificationHash(password)
);
const dbPasswordHash = optionService.getOptionOrNull('passwordVerificationHash');
const dbPasswordHash = optionService.getOptionOrNull(
"passwordVerificationHash"
);
if (!dbPasswordHash) {
return false;
@ -18,17 +22,23 @@ function verifyPassword(password: string) {
function setDataKey(password: string, plainTextDataKey: string | Buffer) {
const passwordDerivedKey = myScryptService.getPasswordDerivedKey(password);
const newEncryptedDataKey = dataEncryptionService.encrypt(passwordDerivedKey, plainTextDataKey);
const newEncryptedDataKey = dataEncryptionService.encrypt(
passwordDerivedKey,
plainTextDataKey
);
optionService.setOption('encryptedDataKey', newEncryptedDataKey);
optionService.setOption("encryptedDataKey", newEncryptedDataKey);
}
function getDataKey(password: string) {
const passwordDerivedKey = myScryptService.getPasswordDerivedKey(password);
const encryptedDataKey = optionService.getOption('encryptedDataKey');
const encryptedDataKey = optionService.getOption("encryptedDataKey");
const decryptedDataKey = dataEncryptionService.decrypt(passwordDerivedKey, encryptedDataKey);
const decryptedDataKey = dataEncryptionService.decrypt(
passwordDerivedKey,
encryptedDataKey
);
return decryptedDataKey;
}
@ -36,5 +46,5 @@ function getDataKey(password: string) {
export = {
verifyPassword,
getDataKey,
setDataKey
setDataKey,
};

View File

@ -0,0 +1,110 @@
"use strict";
import sql = require("../sql");
import optionService = require("../options");
import myScryptService = require("./my_scrypt");
import utils = require("../utils");
import totpEncryptionService = require("./totp_secret_encryption");
function isTotpSecretSet() {
return !!sql.getValue(
"SELECT value FROM options WHERE name = 'passwordVerificationHash'"
);
}
function changePassword(currentSecret: string, newSecret: string) {
if (!isTotpSecretSet()) {
throw new Error(
"TOTP Secret has not been set yet, so it cannot be changed. Use 'setTotpSecret' instead."
);
}
sql.transactional(() => {
const decryptedDataKey = totpEncryptionService.getDataKey(currentSecret);
optionService.setOption(
"totpSecretVerificationSalt",
utils.randomSecureToken(32)
);
optionService.setOption(
"totpSecretDerivedKeySalt",
utils.randomSecureToken(32)
);
const newTotpSecretVerificationKey = utils.toBase64(
myScryptService.getTotpSecretVerificationHash(newSecret)
);
if (decryptedDataKey) {
// TODO: what should happen if the decrypted data key is null?
totpEncryptionService.setDataKey(newSecret, decryptedDataKey);
}
optionService.setOption(
"totpSecretVerificationHash",
newTotpSecretVerificationKey
);
});
return {
success: true,
};
}
function setTotpSecret(secret: string) {
if (isTotpSecretSet()) {
throw new Error(
"TOTP Secret is set already. Either change it or perform 'reset TOTP' first."
);
}
optionService.createOption(
"totpSecretVerificationSalt",
utils.randomSecureToken(32),
true
);
optionService.createOption(
"totpSecretDerivedKeySalt",
utils.randomSecureToken(32),
true
);
const totpSecretVerificationKey = utils.toBase64(
myScryptService.getTotpSecretVerificationHash(secret)
);
optionService.createOption(
"totpSecretVerificationHash",
totpSecretVerificationKey,
true
);
// totpEncryptionService expects these options to already exist
optionService.createOption("encryptedTotpSecretDataKey", "", true);
totpEncryptionService.setDataKey(secret, utils.randomSecureToken(16));
return {
success: true,
};
}
function resetPassword() {
// user forgot the password,
sql.transactional(() => {
optionService.setOption("passwordVerificationSalt", "");
optionService.setOption("passwordDerivedKeySalt", "");
optionService.setOption("encryptedDataKey", "");
optionService.setOption("passwordVerificationHash", "");
});
return {
success: true,
};
}
export = {
isTotpSecretSet,
changePassword,
setTotpSecret,
resetPassword,
};

View File

@ -0,0 +1,52 @@
import optionService = require("../options");
import myScryptService = require("./my_scrypt");
import utils = require("../utils");
import dataEncryptionService = require("./data_encryption");
// function verifyPassword(password: string) {
// const givenPasswordHash = utils.toBase64(
// myScryptService.getVerificationHash(password)
// );
// const dbPasswordHash = optionService.getOptionOrNull(
// "passwordVerificationHash"
// );
// if (!dbPasswordHash) {
// return false;
// }
// return givenPasswordHash === dbPasswordHash;
// }
function setDataKey(secret: string, plainTextDataKey: string | Buffer) {
const totpSecretDerivedKey = myScryptService.getTotpSecretDerivedKey(secret);
const newEncryptedDataKey = dataEncryptionService.encrypt(
totpSecretDerivedKey,
plainTextDataKey
);
optionService.setOption("encryptedTotpSecretDataKey", newEncryptedDataKey);
}
function getDataKey(secret: string) {
const totpSecretDerivedKey = myScryptService.getTotpSecretDerivedKey(secret);
const encryptedDataKey = optionService.getOption(
"encryptedTotpSecretDataKey"
);
const decryptedDataKey = dataEncryptionService.decrypt(
totpSecretDerivedKey,
encryptedDataKey
);
return decryptedDataKey;
}
export = {
// verifyPassword,
getDataKey,
setDataKey,
};

View File

@ -1,10 +1,10 @@
import BAttribute = require("../becca/entities/battribute");
import { AttributeType, NoteType } from "../becca/entities/rows";
import becca = require('../becca/becca');
import noteService = require('./notes');
import log = require('./log');
import migrationService = require('./migration');
import becca = require("../becca/becca");
import noteService = require("./notes");
import log = require("./log");
import migrationService = require("./migration");
const LBTPL_ROOT = "_lbTplRoot";
const LBTPL_BASE = "_lbTplBase";
@ -19,7 +19,7 @@ interface Attribute {
type: AttributeType;
name: string;
isInheritable?: boolean;
value?: string
value?: string;
}
interface Item {
@ -34,8 +34,20 @@ interface Item {
baseSize?: string;
growthFactor?: string;
targetNoteId?: "_backendLog" | "_globalNoteMap";
builtinWidget?: "bookmarks" | "spacer" | "backInHistoryButton" | "forwardInHistoryButton" | "syncStatus" | "protectedSession" | "todayInJournal" | "calendar";
command?: "jumpToNote" | "searchNotes" | "createNoteIntoInbox" | "showRecentChanges";
builtinWidget?:
| "bookmarks"
| "spacer"
| "backInHistoryButton"
| "forwardInHistoryButton"
| "syncStatus"
| "protectedSession"
| "todayInJournal"
| "calendar";
command?:
| "jumpToNote"
| "searchNotes"
| "createNoteIntoInbox"
| "showRecentChanges";
}
/*
@ -45,218 +57,412 @@ interface Item {
*/
const HIDDEN_SUBTREE_DEFINITION: Item = {
id: '_hidden',
title: 'Hidden Notes',
type: 'doc',
icon: 'bx bx-chip',
id: "_hidden",
title: "Hidden Notes",
type: "doc",
icon: "bx bx-chip",
// we want to keep the hidden subtree always last, otherwise there will be problems with e.g., keyboard navigation
// over tree when it's in the middle
notePosition: 999_999_999,
attributes: [
{ type: 'label', name: 'excludeFromNoteMap', isInheritable: true },
{ type: 'label', name: 'docName', value: 'hidden' }
{ type: "label", name: "excludeFromNoteMap", isInheritable: true },
{ type: "label", name: "docName", value: "hidden" },
],
children: [
{
id: '_search',
title: 'Search History',
type: 'doc'
id: "_search",
title: "Search History",
type: "doc",
},
{
id: '_globalNoteMap',
title: 'Note Map',
type: 'noteMap',
id: "_globalNoteMap",
title: "Note Map",
type: "noteMap",
attributes: [
{ type: 'label', name: 'mapRootNoteId', value: 'hoisted' },
{ type: 'label', name: 'keepCurrentHoisting' }
]
{ type: "label", name: "mapRootNoteId", value: "hoisted" },
{ type: "label", name: "keepCurrentHoisting" },
],
},
{
id: '_sqlConsole',
title: 'SQL Console History',
type: 'doc',
icon: 'bx-data'
id: "_sqlConsole",
title: "SQL Console History",
type: "doc",
icon: "bx-data",
},
{
id: '_share',
title: 'Shared Notes',
type: 'doc',
attributes: [ { type: 'label', name: 'docName', value: 'share' } ]
id: "_share",
title: "Shared Notes",
type: "doc",
attributes: [{ type: "label", name: "docName", value: "share" }],
},
{
id: '_bulkAction',
title: 'Bulk Action',
type: 'doc',
id: "_bulkAction",
title: "Bulk Action",
type: "doc",
},
{
id: '_backendLog',
title: 'Backend Log',
type: 'contentWidget',
icon: 'bx-terminal',
attributes: [
{ type: 'label', name: 'keepCurrentHoisting' }
]
id: "_backendLog",
title: "Backend Log",
type: "contentWidget",
icon: "bx-terminal",
attributes: [{ type: "label", name: "keepCurrentHoisting" }],
},
{
// place for user scripts hidden stuff (scripts should not create notes directly under hidden root)
id: '_userHidden',
title: 'User Hidden',
type: 'doc',
attributes: [ { type: 'label', name: 'docName', value: 'user_hidden' } ]
id: "_userHidden",
title: "User Hidden",
type: "doc",
attributes: [{ type: "label", name: "docName", value: "user_hidden" }],
},
{
id: LBTPL_ROOT,
title: 'Launch Bar Templates',
type: 'doc',
title: "Launch Bar Templates",
type: "doc",
children: [
{
id: LBTPL_BASE,
title: 'Base Abstract Launcher',
type: 'doc'
title: "Base Abstract Launcher",
type: "doc",
},
{
id: LBTPL_COMMAND,
title: 'Command Launcher',
type: 'doc',
title: "Command Launcher",
type: "doc",
attributes: [
{ type: 'relation', name: 'template', value: LBTPL_BASE },
{ type: 'label', name: 'launcherType', value: 'command' },
{ type: 'label', name: 'docName', value: 'launchbar_command_launcher' }
]
{ type: "relation", name: "template", value: LBTPL_BASE },
{ type: "label", name: "launcherType", value: "command" },
{
type: "label",
name: "docName",
value: "launchbar_command_launcher",
},
],
},
{
id: LBTPL_NOTE_LAUNCHER,
title: 'Note Launcher',
type: 'doc',
title: "Note Launcher",
type: "doc",
attributes: [
{ type: 'relation', name: 'template', value: LBTPL_BASE },
{ type: 'label', name: 'launcherType', value: 'note' },
{ type: 'label', name: 'relation:target', value: 'promoted' },
{ type: 'label', name: 'relation:hoistedNote', value: 'promoted' },
{ type: 'label', name: 'label:keyboardShortcut', value: 'promoted,text' },
{ type: 'label', name: 'docName', value: 'launchbar_note_launcher' }
]
{ type: "relation", name: "template", value: LBTPL_BASE },
{ type: "label", name: "launcherType", value: "note" },
{ type: "label", name: "relation:target", value: "promoted" },
{ type: "label", name: "relation:hoistedNote", value: "promoted" },
{
type: "label",
name: "label:keyboardShortcut",
value: "promoted,text",
},
{
type: "label",
name: "docName",
value: "launchbar_note_launcher",
},
],
},
{
id: LBTPL_SCRIPT,
title: 'Script Launcher',
type: 'doc',
title: "Script Launcher",
type: "doc",
attributes: [
{ type: 'relation', name: 'template', value: LBTPL_BASE },
{ type: 'label', name: 'launcherType', value: 'script' },
{ type: 'label', name: 'relation:script', value: 'promoted' },
{ type: 'label', name: 'label:keyboardShortcut', value: 'promoted,text' },
{ type: 'label', name: 'docName', value: 'launchbar_script_launcher' }
]
{ type: "relation", name: "template", value: LBTPL_BASE },
{ type: "label", name: "launcherType", value: "script" },
{ type: "label", name: "relation:script", value: "promoted" },
{
type: "label",
name: "label:keyboardShortcut",
value: "promoted,text",
},
{
type: "label",
name: "docName",
value: "launchbar_script_launcher",
},
],
},
{
id: LBTPL_BUILTIN_WIDGET,
title: 'Built-in Widget',
type: 'doc',
title: "Built-in Widget",
type: "doc",
attributes: [
{ type: 'relation', name: 'template', value: LBTPL_BASE },
{ type: 'label', name: 'launcherType', value: 'builtinWidget' }
]
{ type: "relation", name: "template", value: LBTPL_BASE },
{ type: "label", name: "launcherType", value: "builtinWidget" },
],
},
{
id: LBTPL_SPACER,
title: 'Spacer',
type: 'doc',
icon: 'bx-move-vertical',
title: "Spacer",
type: "doc",
icon: "bx-move-vertical",
attributes: [
{ type: 'relation', name: 'template', value: LBTPL_BUILTIN_WIDGET },
{ type: 'label', name: 'builtinWidget', value: 'spacer' },
{ type: 'label', name: 'label:baseSize', value: 'promoted,number' },
{ type: 'label', name: 'label:growthFactor', value: 'promoted,number' },
{ type: 'label', name: 'docName', value: 'launchbar_spacer' }
]
{ type: "relation", name: "template", value: LBTPL_BUILTIN_WIDGET },
{ type: "label", name: "builtinWidget", value: "spacer" },
{ type: "label", name: "label:baseSize", value: "promoted,number" },
{
type: "label",
name: "label:growthFactor",
value: "promoted,number",
},
{ type: "label", name: "docName", value: "launchbar_spacer" },
],
},
{
id: LBTPL_CUSTOM_WIDGET,
title: 'Custom Widget',
type: 'doc',
title: "Custom Widget",
type: "doc",
attributes: [
{ type: 'relation', name: 'template', value: LBTPL_BASE },
{ type: 'label', name: 'launcherType', value: 'customWidget' },
{ type: 'label', name: 'relation:widget', value: 'promoted' },
{ type: 'label', name: 'docName', value: 'launchbar_widget_launcher' }
]
{ type: "relation", name: "template", value: LBTPL_BASE },
{ type: "label", name: "launcherType", value: "customWidget" },
{ type: "label", name: "relation:widget", value: "promoted" },
{
type: "label",
name: "docName",
value: "launchbar_widget_launcher",
},
]
],
},
],
},
{
id: '_lbRoot',
title: 'Launch Bar',
type: 'doc',
icon: 'bx-sidebar',
id: "_lbRoot",
title: "Launch Bar",
type: "doc",
icon: "bx-sidebar",
isExpanded: true,
attributes: [ { type: 'label', name: 'docName', value: 'launchbar_intro' } ],
attributes: [
{ type: "label", name: "docName", value: "launchbar_intro" },
],
children: [
{
id: '_lbAvailableLaunchers',
title: 'Available Launchers',
type: 'doc',
icon: 'bx-hide',
id: "_lbAvailableLaunchers",
title: "Available Launchers",
type: "doc",
icon: "bx-hide",
isExpanded: true,
attributes: [ { type: 'label', name: 'docName', value: 'launchbar_intro' } ],
attributes: [
{ type: "label", name: "docName", value: "launchbar_intro" },
],
children: [
{ id: '_lbBackInHistory', title: 'Go to Previous Note', type: 'launcher', builtinWidget: 'backInHistoryButton', icon: 'bx bxs-left-arrow-square',
attributes: [ { type: 'label', name: 'docName', value: 'launchbar_history_navigation' } ]},
{ id: '_lbForwardInHistory', title: 'Go to Next Note', type: 'launcher', builtinWidget: 'forwardInHistoryButton', icon: 'bx bxs-right-arrow-square',
attributes: [ { type: 'label', name: 'docName', value: 'launchbar_history_navigation' } ]},
{ id: '_lbBackendLog', title: 'Backend Log', type: 'launcher', targetNoteId: '_backendLog', icon: 'bx bx-terminal' },
]
{
id: "_lbBackInHistory",
title: "Go to Previous Note",
type: "launcher",
builtinWidget: "backInHistoryButton",
icon: "bx bxs-left-arrow-square",
attributes: [
{
type: "label",
name: "docName",
value: "launchbar_history_navigation",
},
],
},
{
id: '_lbVisibleLaunchers',
title: 'Visible Launchers',
type: 'doc',
icon: 'bx-show',
isExpanded: true,
attributes: [ { type: 'label', name: 'docName', value: 'launchbar_intro' } ],
children: [
{ id: '_lbNewNote', title: 'New Note', type: 'launcher', command: 'createNoteIntoInbox', icon: 'bx bx-file-blank' },
{ id: '_lbSearch', title: 'Search Notes', type: 'launcher', command: 'searchNotes', icon: 'bx bx-search', attributes: [
{ type: 'label', name: 'desktopOnly' }
] },
{ id: '_lbJumpTo', title: 'Jump to Note', type: 'launcher', command: 'jumpToNote', icon: 'bx bx-send', attributes: [
{ type: 'label', name: 'desktopOnly' }
] },
{ id: '_lbNoteMap', title: 'Note Map', type: 'launcher', targetNoteId: '_globalNoteMap', icon: 'bx bx-map-alt' },
{ id: '_lbCalendar', title: 'Calendar', type: 'launcher', builtinWidget: 'calendar', icon: 'bx bx-calendar' },
{ id: '_lbRecentChanges', title: 'Recent Changes', type: 'launcher', command: 'showRecentChanges', icon: 'bx bx-history', attributes: [
{ type: 'label', name: 'desktopOnly' }
] },
{ id: '_lbSpacer1', title: 'Spacer', type: 'launcher', builtinWidget: 'spacer', baseSize: "50", growthFactor: "0" },
{ id: '_lbBookmarks', title: 'Bookmarks', type: 'launcher', builtinWidget: 'bookmarks', icon: 'bx bx-bookmark' },
{ id: '_lbToday', title: "Open Today's Journal Note", type: 'launcher', builtinWidget: 'todayInJournal', icon: 'bx bx-calendar-star' },
{ id: '_lbSpacer2', title: 'Spacer', type: 'launcher', builtinWidget: 'spacer', baseSize: "0", growthFactor: "1" },
{ id: '_lbProtectedSession', title: 'Protected Session', type: 'launcher', builtinWidget: 'protectedSession', icon: 'bx bx bx-shield-quarter' },
{ id: '_lbSyncStatus', title: 'Sync Status', type: 'launcher', builtinWidget: 'syncStatus', icon: 'bx bx-wifi' }
]
}
]
id: "_lbForwardInHistory",
title: "Go to Next Note",
type: "launcher",
builtinWidget: "forwardInHistoryButton",
icon: "bx bxs-right-arrow-square",
attributes: [
{
type: "label",
name: "docName",
value: "launchbar_history_navigation",
},
],
},
{
id: '_options',
title: 'Options',
type: 'book',
id: "_lbBackendLog",
title: "Backend Log",
type: "launcher",
targetNoteId: "_backendLog",
icon: "bx bx-terminal",
},
],
},
{
id: "_lbVisibleLaunchers",
title: "Visible Launchers",
type: "doc",
icon: "bx-show",
isExpanded: true,
attributes: [
{ type: "label", name: "docName", value: "launchbar_intro" },
],
children: [
{ id: '_optionsAppearance', title: 'Appearance', type: 'contentWidget', icon: 'bx-layout' },
{ id: '_optionsShortcuts', title: 'Shortcuts', type: 'contentWidget', icon: 'bxs-keyboard' },
{ id: '_optionsTextNotes', title: 'Text Notes', type: 'contentWidget', icon: 'bx-text' },
{ id: '_optionsCodeNotes', title: 'Code Notes', type: 'contentWidget', icon: 'bx-code' },
{ id: '_optionsImages', title: 'Images', type: 'contentWidget', icon: 'bx-image' },
{ id: '_optionsSpellcheck', title: 'Spellcheck', type: 'contentWidget', icon: 'bx-check-double' },
{ id: '_optionsPassword', title: 'Password', type: 'contentWidget', icon: 'bx-lock' },
{ id: '_optionsEtapi', title: 'ETAPI', type: 'contentWidget', icon: 'bx-extension' },
{ id: '_optionsBackup', title: 'Backup', type: 'contentWidget', icon: 'bx-data' },
{ id: '_optionsSync', title: 'Sync', type: 'contentWidget', icon: 'bx-wifi' },
{ id: '_optionsOther', title: 'Other', type: 'contentWidget', icon: 'bx-dots-horizontal' },
{ id: '_optionsAdvanced', title: 'Advanced', type: 'contentWidget' }
]
}
]
{
id: "_lbNewNote",
title: "New Note",
type: "launcher",
command: "createNoteIntoInbox",
icon: "bx bx-file-blank",
},
{
id: "_lbSearch",
title: "Search Notes",
type: "launcher",
command: "searchNotes",
icon: "bx bx-search",
attributes: [{ type: "label", name: "desktopOnly" }],
},
{
id: "_lbJumpTo",
title: "Jump to Note",
type: "launcher",
command: "jumpToNote",
icon: "bx bx-send",
attributes: [{ type: "label", name: "desktopOnly" }],
},
{
id: "_lbNoteMap",
title: "Note Map",
type: "launcher",
targetNoteId: "_globalNoteMap",
icon: "bx bx-map-alt",
},
{
id: "_lbCalendar",
title: "Calendar",
type: "launcher",
builtinWidget: "calendar",
icon: "bx bx-calendar",
},
{
id: "_lbRecentChanges",
title: "Recent Changes",
type: "launcher",
command: "showRecentChanges",
icon: "bx bx-history",
attributes: [{ type: "label", name: "desktopOnly" }],
},
{
id: "_lbSpacer1",
title: "Spacer",
type: "launcher",
builtinWidget: "spacer",
baseSize: "50",
growthFactor: "0",
},
{
id: "_lbBookmarks",
title: "Bookmarks",
type: "launcher",
builtinWidget: "bookmarks",
icon: "bx bx-bookmark",
},
{
id: "_lbToday",
title: "Open Today's Journal Note",
type: "launcher",
builtinWidget: "todayInJournal",
icon: "bx bx-calendar-star",
},
{
id: "_lbSpacer2",
title: "Spacer",
type: "launcher",
builtinWidget: "spacer",
baseSize: "0",
growthFactor: "1",
},
{
id: "_lbProtectedSession",
title: "Protected Session",
type: "launcher",
builtinWidget: "protectedSession",
icon: "bx bx bx-shield-quarter",
},
{
id: "_lbSyncStatus",
title: "Sync Status",
type: "launcher",
builtinWidget: "syncStatus",
icon: "bx bx-wifi",
},
],
},
],
},
{
id: "_options",
title: "Options",
type: "book",
children: [
{
id: "_optionsAppearance",
title: "Appearance",
type: "contentWidget",
icon: "bx-layout",
},
{
id: "_optionsShortcuts",
title: "Shortcuts",
type: "contentWidget",
icon: "bxs-keyboard",
},
{
id: "_optionsTextNotes",
title: "Text Notes",
type: "contentWidget",
icon: "bx-text",
},
{
id: "_optionsMFA",
title: "MFA",
type: "contentWidget",
icon: "bx-lock",
},
{
id: "_optionsCodeNotes",
title: "Code Notes",
type: "contentWidget",
icon: "bx-code",
},
{
id: "_optionsImages",
title: "Images",
type: "contentWidget",
icon: "bx-image",
},
{
id: "_optionsSpellcheck",
title: "Spellcheck",
type: "contentWidget",
icon: "bx-check-double",
},
{
id: "_optionsPassword",
title: "Password",
type: "contentWidget",
icon: "bx-lock",
},
{
id: "_optionsEtapi",
title: "ETAPI",
type: "contentWidget",
icon: "bx-extension",
},
{
id: "_optionsBackup",
title: "Backup",
type: "contentWidget",
icon: "bx-data",
},
{
id: "_optionsSync",
title: "Sync",
type: "contentWidget",
icon: "bx-wifi",
},
{
id: "_optionsOther",
title: "Other",
type: "contentWidget",
icon: "bx-dots-horizontal",
},
{ id: "_optionsAdvanced", title: "Advanced", type: "contentWidget" },
],
},
],
};
function checkHiddenSubtree(force = false) {
@ -266,15 +472,17 @@ function checkHiddenSubtree(force = false) {
return;
}
checkHiddenSubtreeRecursively('root', HIDDEN_SUBTREE_DEFINITION);
checkHiddenSubtreeRecursively("root", HIDDEN_SUBTREE_DEFINITION);
}
function checkHiddenSubtreeRecursively(parentNoteId: string, item: Item) {
if (!item.id || !item.type || !item.title) {
throw new Error(`Item does not contain mandatory properties: ${JSON.stringify(item)}`);
throw new Error(
`Item does not contain mandatory properties: ${JSON.stringify(item)}`
);
}
if (item.id.charAt(0) !== '_') {
if (item.id.charAt(0) !== "_") {
throw new Error(`ID has to start with underscore, given '${item.id}'`);
}
@ -287,36 +495,58 @@ function checkHiddenSubtreeRecursively(parentNoteId: string, item: Item) {
title: item.title,
type: item.type,
parentNoteId: parentNoteId,
content: '',
ignoreForbiddenParents: true
content: "",
ignoreForbiddenParents: true,
}));
} else {
branch = note.getParentBranches().find(branch => branch.parentNoteId === parentNoteId);
branch = note
.getParentBranches()
.find((branch) => branch.parentNoteId === parentNoteId);
}
const attrs = [...(item.attributes || [])];
if (item.icon) {
attrs.push({ type: 'label', name: 'iconClass', value: `bx ${item.icon}` });
attrs.push({ type: "label", name: "iconClass", value: `bx ${item.icon}` });
}
if (item.type === 'launcher') {
if (item.type === "launcher") {
if (item.command) {
attrs.push({ type: 'relation', name: 'template', value: LBTPL_COMMAND });
attrs.push({ type: 'label', name: 'command', value: item.command });
attrs.push({ type: "relation", name: "template", value: LBTPL_COMMAND });
attrs.push({ type: "label", name: "command", value: item.command });
} else if (item.builtinWidget) {
if (item.builtinWidget === 'spacer') {
attrs.push({ type: 'relation', name: 'template', value: LBTPL_SPACER });
attrs.push({ type: 'label', name: 'baseSize', value: item.baseSize });
attrs.push({ type: 'label', name: 'growthFactor', value: item.growthFactor });
if (item.builtinWidget === "spacer") {
attrs.push({ type: "relation", name: "template", value: LBTPL_SPACER });
attrs.push({ type: "label", name: "baseSize", value: item.baseSize });
attrs.push({
type: "label",
name: "growthFactor",
value: item.growthFactor,
});
} else {
attrs.push({ type: 'relation', name: 'template', value: LBTPL_BUILTIN_WIDGET });
attrs.push({
type: "relation",
name: "template",
value: LBTPL_BUILTIN_WIDGET,
});
}
attrs.push({ type: 'label', name: 'builtinWidget', value: item.builtinWidget });
attrs.push({
type: "label",
name: "builtinWidget",
value: item.builtinWidget,
});
} else if (item.targetNoteId) {
attrs.push({ type: 'relation', name: 'template', value: LBTPL_NOTE_LAUNCHER });
attrs.push({ type: 'relation', name: 'target', value: item.targetNoteId });
attrs.push({
type: "relation",
name: "template",
value: LBTPL_NOTE_LAUNCHER,
});
attrs.push({
type: "relation",
name: "target",
value: item.targetNoteId,
});
} else {
throw new Error(`No action defined for launcher ${JSON.stringify(item)}`);
}
@ -331,12 +561,18 @@ function checkHiddenSubtreeRecursively(parentNoteId: string, item: Item) {
if (branch) {
// in case of launchers the branch ID is not preserved and should not be relied upon - launchers which move between
// visible and available will change branch since the branch's parent-child relationship is immutable
if (item.notePosition !== undefined && branch.notePosition !== item.notePosition) {
if (
item.notePosition !== undefined &&
branch.notePosition !== item.notePosition
) {
branch.notePosition = item.notePosition;
branch.save();
}
if (item.isExpanded !== undefined && branch.isExpanded !== item.isExpanded) {
if (
item.isExpanded !== undefined &&
branch.isExpanded !== item.isExpanded
) {
branch.isExpanded = item.isExpanded;
branch.save();
}
@ -345,14 +581,14 @@ function checkHiddenSubtreeRecursively(parentNoteId: string, item: Item) {
for (const attr of attrs) {
const attrId = note.noteId + "_" + attr.type.charAt(0) + attr.name;
if (!note.getAttributes().find(attr => attr.attributeId === attrId)) {
if (!note.getAttributes().find((attr) => attr.attributeId === attrId)) {
new BAttribute({
attributeId: attrId,
noteId: note.noteId,
type: attr.type,
name: attr.name,
value: attr.value,
isInheritable: false
isInheritable: false,
}).save();
}
}
@ -371,5 +607,5 @@ export = {
LBTPL_SCRIPT,
LBTPL_BUILTIN_WIDGET,
LBTPL_SPACER,
LBTPL_CUSTOM_WIDGET
LBTPL_CUSTOM_WIDGET,
};

View File

@ -1,15 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1"
/>
<title>Login</title>
<link rel="apple-touch-icon" sizes="180x180" href="<%= assetPath %>/images/app-icons/ios/apple-touch-icon.png">
<link rel="shortcut icon" href="favicon.ico">
<link
rel="apple-touch-icon"
sizes="180x180"
href="<%= assetPath %>/images/app-icons/ios/apple-touch-icon.png"
/>
<link rel="shortcut icon" href="favicon.ico" />
</head>
<body>
<div class="container">
<div class="col-xs-12 col-sm-10 col-md-6 col-lg-4 col-xl-4 mx-auto" style="padding-top: 25px;">
<div
class="col-xs-12 col-sm-10 col-md-6 col-lg-4 col-xl-4 mx-auto"
style="padding-top: 25px"
>
<h1>Trilium login</h1>
<% if (failedAuth) { %>
@ -22,13 +32,25 @@
<div class="form-group">
<label for="password">Password</label>
<div class="controls">
<input id="password" name="password" placeholder="" class="form-control" type="password">
<input
id="password"
name="password"
placeholder=""
class="form-control"
type="password"
/>
</div>
</div>
<div class="form-group">
<div class="checkbox">
<label>
<input id="remember-me" name="rememberMe" value="1" type="checkbox"> Remember me
<input
id="remember-me"
name="rememberMe"
value="1"
type="checkbox"
/>
Remember me
</label>
</div>
</div>
@ -41,17 +63,18 @@
<script>
// Required for correct loading of scripts in Electron
if (typeof module === 'object') {window.module = module; module = undefined;}
if (typeof module === "object") {
window.module = module;
module = undefined;
}
let device;
if (window.location.search === '?desktop') {
if (window.location.search === "?desktop") {
device = "desktop";
}
else if (window.location.search === '?mobile') {
} else if (window.location.search === "?mobile") {
device = "mobile";
}
else {
} else {
device = isMobile() ? "mobile" : "desktop";
}
@ -68,16 +91,21 @@
// https://stackoverflow.com/a/73731646/944162
function isMobile() {
const mQ = matchMedia?.('(pointer:coarse)');
if (mQ?.media === '(pointer:coarse)') return !!mQ.matches;
const mQ = matchMedia?.("(pointer:coarse)");
if (mQ?.media === "(pointer:coarse)") return !!mQ.matches;
if ('orientation' in window) return true;
if ("orientation" in window) return true;
return /\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(navigator.userAgent) ||
/\b(Android|Windows Phone|iPad|iPod)\b/i.test(navigator.userAgent);
return (
/\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(navigator.userAgent) ||
/\b(Android|Windows Phone|iPad|iPod)\b/i.test(navigator.userAgent)
);
}
</script>
<link href="<%= assetPath %>/libraries/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link
href="<%= assetPath %>/libraries/bootstrap/css/bootstrap.min.css"
rel="stylesheet"
/>
</body>
</html>