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",
@ -155,4 +156,4 @@
"optionalDependencies": {
"electron-installer-debian": "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>
@ -53,79 +54,84 @@ const TPL = `<div class="note-detail-content-widget note-detail-printable">
</div>`;
const CONTENT_WIDGETS = {
_optionsAppearance: [
ThemeOptions,
FontsOptions,
ZoomFactorOptions,
NativeTitleBarOptions,
MaxContentWidthOptions,
RibbonOptions
],
_optionsShortcuts: [ KeyboardShortcutsOptions ],
_optionsTextNotes: [
HeadingStyleOptions,
TableOfContentsOptions,
HighlightsListOptions,
TextAutoReadOnlySizeOptions
],
_optionsCodeNotes: [
VimKeyBindingsOptions,
WrapLinesOptions,
CodeAutoReadOnlySizeOptions,
CodeMimeTypesOptions
],
_optionsImages: [ ImageOptions ],
_optionsSpellcheck: [ SpellcheckOptions ],
_optionsPassword: [ PasswordOptions ],
_optionsEtapi: [ EtapiOptions ],
_optionsBackup: [ BackupOptions ],
_optionsSync: [ SyncOptions ],
_optionsOther: [
SearchEngineOptions,
TrayOptions,
NoteErasureTimeoutOptions,
AttachmentErasureTimeoutOptions,
RevisionsSnapshotIntervalOptions,
NetworkConnectionsOptions
],
_optionsAdvanced: [
DatabaseIntegrityCheckOptions,
ConsistencyChecksOptions,
DatabaseAnonymizationOptions,
AdvancedSyncOptions,
VacuumDatabaseOptions
],
_backendLog: [ BackendLogWidget ]
_optionsAppearance: [
ThemeOptions,
FontsOptions,
ZoomFactorOptions,
NativeTitleBarOptions,
MaxContentWidthOptions,
RibbonOptions,
],
_optionsShortcuts: [KeyboardShortcutsOptions],
_optionsTextNotes: [
HeadingStyleOptions,
TableOfContentsOptions,
HighlightsListOptions,
TextAutoReadOnlySizeOptions,
],
_optionsCodeNotes: [
VimKeyBindingsOptions,
WrapLinesOptions,
CodeAutoReadOnlySizeOptions,
CodeMimeTypesOptions,
],
_optionsMFA: [MultiFactorAuthenticationOptions],
_optionsImages: [ImageOptions],
_optionsSpellcheck: [SpellcheckOptions],
_optionsPassword: [PasswordOptions],
_optionsEtapi: [EtapiOptions],
_optionsBackup: [BackupOptions],
_optionsSync: [SyncOptions],
_optionsOther: [
SearchEngineOptions,
TrayOptions,
NoteErasureTimeoutOptions,
AttachmentErasureTimeoutOptions,
RevisionsSnapshotIntervalOptions,
NetworkConnectionsOptions,
],
_optionsAdvanced: [
DatabaseIntegrityCheckOptions,
ConsistencyChecksOptions,
DatabaseAnonymizationOptions,
AdvancedSyncOptions,
VacuumDatabaseOptions,
],
_backendLog: [BackendLogWidget],
};
export default class ContentWidgetTypeWidget extends TypeWidget {
static getType() { return "contentWidget"; }
static getType() {
return "contentWidget";
}
doRender() {
this.$widget = $(TPL);
this.$content = this.$widget.find(".note-detail-content-widget-content");
doRender() {
this.$widget = $(TPL);
this.$content = this.$widget.find(".note-detail-content-widget-content");
super.doRender();
}
async doRefresh(note) {
this.$content.empty();
this.children = [];
const contentWidgets = CONTENT_WIDGETS[note.noteId];
if (contentWidgets) {
for (const clazz of contentWidgets) {
const widget = new clazz();
await widget.handleEvent('setNoteContext', { noteContext: this.noteContext });
this.child(widget);
this.$content.append(widget.render());
await widget.refresh();
}
} else {
this.$content.append(`Unknown widget for "${note.noteId}"`);
}
super.doRender();
}
async doRefresh(note) {
this.$content.empty();
this.children = [];
const contentWidgets = CONTENT_WIDGETS[note.noteId];
if (contentWidgets) {
for (const clazz of contentWidgets) {
const widget = new clazz();
await widget.handleEvent("setNoteContext", {
noteContext: this.noteContext,
});
this.child(widget);
this.$content.append(widget.render());
await widget.refresh();
}
} else {
this.$content.append(`Unknown widget for "${note.noteId}"`);
}
}
}

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);
return getScryptHash(password, salt);
}
function getPasswordDerivedKey(password: crypto.BinaryLike) {
const salt = optionService.getOption('passwordDerivedKeySalt');
const salt = optionService.getOption("passwordDerivedKeySalt");
return getScryptHash(password, salt);
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;
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
getVerificationHash,
getPasswordDerivedKey,
getTotpSecretVerificationHash,
getTotpSecretDerivedKey,
};

View File

@ -1,40 +1,50 @@
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;
}
if (!dbPasswordHash) {
return false;
}
return givenPasswordHash === dbPasswordHash;
return givenPasswordHash === dbPasswordHash;
}
function setDataKey(password: string, plainTextDataKey: string | Buffer) {
const passwordDerivedKey = myScryptService.getPasswordDerivedKey(password);
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 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;
return decryptedDataKey;
}
export = {
verifyPassword,
getDataKey,
setDataKey
verifyPassword,
getDataKey,
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";
@ -16,26 +16,38 @@ const LBTPL_SPACER = "_lbTplSpacer";
const LBTPL_CUSTOM_WIDGET = "_lbTplCustomWidget";
interface Attribute {
type: AttributeType;
name: string;
isInheritable?: boolean;
value?: string
type: AttributeType;
name: string;
isInheritable?: boolean;
value?: string;
}
interface Item {
notePosition?: number;
id: string;
title: string;
type: NoteType;
icon?: string;
attributes?: Attribute[];
children?: Item[];
isExpanded?: boolean;
baseSize?: string;
growthFactor?: string;
targetNoteId?: "_backendLog" | "_globalNoteMap";
builtinWidget?: "bookmarks" | "spacer" | "backInHistoryButton" | "forwardInHistoryButton" | "syncStatus" | "protectedSession" | "todayInJournal" | "calendar";
command?: "jumpToNote" | "searchNotes" | "createNoteIntoInbox" | "showRecentChanges";
notePosition?: number;
id: string;
title: string;
type: NoteType;
icon?: string;
attributes?: Attribute[];
children?: Item[];
isExpanded?: boolean;
baseSize?: string;
growthFactor?: string;
targetNoteId?: "_backendLog" | "_globalNoteMap";
builtinWidget?:
| "bookmarks"
| "spacer"
| "backInHistoryButton"
| "forwardInHistoryButton"
| "syncStatus"
| "protectedSession"
| "todayInJournal"
| "calendar";
command?:
| "jumpToNote"
| "searchNotes"
| "createNoteIntoInbox"
| "showRecentChanges";
}
/*
@ -45,331 +57,555 @@ interface Item {
*/
const HIDDEN_SUBTREE_DEFINITION: Item = {
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' }
],
children: [
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" },
],
children: [
{
id: "_search",
title: "Search History",
type: "doc",
},
{
id: "_globalNoteMap",
title: "Note Map",
type: "noteMap",
attributes: [
{ type: "label", name: "mapRootNoteId", value: "hoisted" },
{ type: "label", name: "keepCurrentHoisting" },
],
},
{
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: "_bulkAction",
title: "Bulk Action",
type: "doc",
},
{
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: LBTPL_ROOT,
title: "Launch Bar Templates",
type: "doc",
children: [
{
id: '_search',
title: 'Search History',
type: 'doc'
id: LBTPL_BASE,
title: "Base Abstract Launcher",
type: "doc",
},
{
id: '_globalNoteMap',
title: 'Note Map',
type: 'noteMap',
attributes: [
{ type: 'label', name: 'mapRootNoteId', value: 'hoisted' },
{ type: 'label', name: 'keepCurrentHoisting' }
]
id: LBTPL_COMMAND,
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",
},
],
},
{
id: '_sqlConsole',
title: 'SQL Console History',
type: 'doc',
icon: 'bx-data'
id: LBTPL_NOTE_LAUNCHER,
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",
},
],
},
{
id: '_share',
title: 'Shared Notes',
type: 'doc',
attributes: [ { type: 'label', name: 'docName', value: 'share' } ]
id: LBTPL_SCRIPT,
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",
},
],
},
{
id: '_bulkAction',
title: 'Bulk Action',
type: 'doc',
id: LBTPL_BUILTIN_WIDGET,
title: "Built-in Widget",
type: "doc",
attributes: [
{ type: "relation", name: "template", value: LBTPL_BASE },
{ type: "label", name: "launcherType", value: "builtinWidget" },
],
},
{
id: '_backendLog',
title: 'Backend Log',
type: 'contentWidget',
icon: 'bx-terminal',
attributes: [
{ type: 'label', name: 'keepCurrentHoisting' }
]
id: LBTPL_SPACER,
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" },
],
},
{
// 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: LBTPL_CUSTOM_WIDGET,
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",
},
],
},
],
},
{
id: "_lbRoot",
title: "Launch Bar",
type: "doc",
icon: "bx-sidebar",
isExpanded: true,
attributes: [
{ type: "label", name: "docName", value: "launchbar_intro" },
],
children: [
{
id: LBTPL_ROOT,
title: 'Launch Bar Templates',
type: 'doc',
children: [
id: "_lbAvailableLaunchers",
title: "Available Launchers",
type: "doc",
icon: "bx-hide",
isExpanded: true,
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: [
{
id: LBTPL_BASE,
title: 'Base Abstract Launcher',
type: 'doc'
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: [
{
id: LBTPL_COMMAND,
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: "label",
name: "docName",
value: "launchbar_history_navigation",
},
{
id: LBTPL_NOTE_LAUNCHER,
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' }
]
},
{
id: LBTPL_SCRIPT,
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' }
]
},
{
id: LBTPL_BUILTIN_WIDGET,
title: 'Built-in Widget',
type: 'doc',
attributes: [
{ type: 'relation', name: 'template', value: LBTPL_BASE },
{ type: 'label', name: 'launcherType', value: 'builtinWidget' }
]
},
{
id: LBTPL_SPACER,
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' }
]
},
{
id: LBTPL_CUSTOM_WIDGET,
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' }
]
},
]
],
},
{
id: "_lbBackendLog",
title: "Backend Log",
type: "launcher",
targetNoteId: "_backendLog",
icon: "bx bx-terminal",
},
],
},
{
id: '_lbRoot',
title: 'Launch Bar',
type: 'doc',
icon: 'bx-sidebar',
isExpanded: true,
attributes: [ { type: 'label', name: 'docName', value: 'launchbar_intro' } ],
children: [
{
id: '_lbAvailableLaunchers',
title: 'Available Launchers',
type: 'doc',
icon: 'bx-hide',
isExpanded: true,
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: '_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: "_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: "_options",
title: "Options",
type: "book",
children: [
{
id: "_optionsAppearance",
title: "Appearance",
type: "contentWidget",
icon: "bx-layout",
},
{
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: '_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: "_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) {
if (!force && !migrationService.isDbUpToDate()) {
// on-delete hook might get triggered during some future migration and cause havoc
log.info("Will not check hidden subtree until migration is finished.");
return;
}
if (!force && !migrationService.isDbUpToDate()) {
// on-delete hook might get triggered during some future migration and cause havoc
log.info("Will not check hidden subtree until migration is finished.");
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)}`);
}
if (!item.id || !item.type || !item.title) {
throw new Error(
`Item does not contain mandatory properties: ${JSON.stringify(item)}`
);
}
if (item.id.charAt(0) !== '_') {
throw new Error(`ID has to start with underscore, given '${item.id}'`);
}
if (item.id.charAt(0) !== "_") {
throw new Error(`ID has to start with underscore, given '${item.id}'`);
}
let note = becca.notes[item.id];
let branch;
let note = becca.notes[item.id];
let branch;
if (!note) {
({note, branch} = noteService.createNewNote({
noteId: item.id,
title: item.title,
type: item.type,
parentNoteId: parentNoteId,
content: '',
ignoreForbiddenParents: true
}));
if (!note) {
({ note, branch } = noteService.createNewNote({
noteId: item.id,
title: item.title,
type: item.type,
parentNoteId: parentNoteId,
content: "",
ignoreForbiddenParents: true,
}));
} else {
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}` });
}
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 });
} 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,
});
} else {
attrs.push({
type: "relation",
name: "template",
value: LBTPL_BUILTIN_WIDGET,
});
}
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,
});
} else {
branch = note.getParentBranches().find(branch => branch.parentNoteId === parentNoteId);
throw new Error(`No action defined for launcher ${JSON.stringify(item)}`);
}
}
if (note.type !== item.type) {
// enforce a correct note type
note.type = item.type;
note.save();
}
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
) {
branch.notePosition = item.notePosition;
branch.save();
}
const attrs = [...(item.attributes || [])];
if (item.icon) {
attrs.push({ type: 'label', name: 'iconClass', value: `bx ${item.icon}` });
if (
item.isExpanded !== undefined &&
branch.isExpanded !== item.isExpanded
) {
branch.isExpanded = item.isExpanded;
branch.save();
}
}
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 });
} 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 });
} else {
attrs.push({ type: 'relation', name: 'template', value: LBTPL_BUILTIN_WIDGET });
}
for (const attr of attrs) {
const attrId = note.noteId + "_" + attr.type.charAt(0) + attr.name;
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 });
} else {
throw new Error(`No action defined for launcher ${JSON.stringify(item)}`);
}
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,
}).save();
}
}
if (note.type !== item.type) {
// enforce a correct note type
note.type = item.type;
note.save();
}
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) {
branch.notePosition = item.notePosition;
branch.save();
}
if (item.isExpanded !== undefined && branch.isExpanded !== item.isExpanded) {
branch.isExpanded = item.isExpanded;
branch.save();
}
}
for (const attr of attrs) {
const attrId = note.noteId + "_" + attr.type.charAt(0) + attr.name;
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
}).save();
}
}
for (const child of item.children || []) {
checkHiddenSubtreeRecursively(item.id, child);
}
for (const child of item.children || []) {
checkHiddenSubtreeRecursively(item.id, child);
}
}
export = {
checkHiddenSubtree,
LBTPL_ROOT,
LBTPL_BASE,
LBTPL_COMMAND,
LBTPL_NOTE_LAUNCHER,
LBTPL_SCRIPT,
LBTPL_BUILTIN_WIDGET,
LBTPL_SPACER,
LBTPL_CUSTOM_WIDGET
checkHiddenSubtree,
LBTPL_ROOT,
LBTPL_BASE,
LBTPL_COMMAND,
LBTPL_NOTE_LAUNCHER,
LBTPL_SCRIPT,
LBTPL_BUILTIN_WIDGET,
LBTPL_SPACER,
LBTPL_CUSTOM_WIDGET,
};

View File

@ -1,83 +1,111 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<head>
<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">
</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;">
<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"
>
<h1>Trilium login</h1>
<% if (failedAuth) { %>
<div class="alert alert-warning">
Password is incorrect. Please try again.
</div>
<div class="alert alert-warning">
Password is incorrect. Please try again.
</div>
<% } %>
<form action="login" method="POST">
<div class="form-group">
<label for="password">Password</label>
<div class="controls">
<input id="password" name="password" placeholder="" class="form-control" type="password">
</div>
<div class="form-group">
<label for="password">Password</label>
<div class="controls">
<input
id="password"
name="password"
placeholder=""
class="form-control"
type="password"
/>
</div>
<div class="form-group">
<div class="checkbox">
<label>
<input id="remember-me" name="rememberMe" value="1" type="checkbox"> Remember me
</label>
</div>
</div>
<div class="form-group">
<button class="btn btn-success">Login</button>
</div>
<div class="form-group">
<div class="checkbox">
<label>
<input
id="remember-me"
name="rememberMe"
value="1"
type="checkbox"
/>
Remember me
</label>
</div>
</div>
<div class="form-group">
<button class="btn btn-success">Login</button>
</div>
</form>
</div>
</div>
</div>
<script>
// Required for correct loading of scripts in Electron
if (typeof module === 'object') {window.module = module; module = undefined;}
<script>
// Required for correct loading of scripts in Electron
if (typeof module === "object") {
window.module = module;
module = undefined;
}
let device;
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";
}
}
console.log("Setting device cookie to:", device);
console.log("Setting device cookie to:", device);
setCookie("trilium-device", device);
setCookie("trilium-device", device);
function setCookie(name, value) {
function setCookie(name, value) {
const date = new Date(Date.now() + 10 * 365 * 24 * 60 * 60 * 1000);
const expires = "; expires=" + date.toUTCString();
document.cookie = name + "=" + (value || "") + expires + "; path=/";
}
document.cookie = name + "=" + (value || "") + expires + "; path=/";
}
// https://stackoverflow.com/a/73731646/944162
function isMobile() {
const mQ = matchMedia?.('(pointer:coarse)');
if (mQ?.media === '(pointer:coarse)') return !!mQ.matches;
// https://stackoverflow.com/a/73731646/944162
function isMobile() {
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);
}
</script>
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">
</body>
<link
href="<%= assetPath %>/libraries/bootstrap/css/bootstrap.min.css"
rel="stylesheet"
/>
</body>
</html>