mirror of
https://github.com/zadam/trilium.git
synced 2025-06-06 18:08:33 +02:00
Adding basic MFA in the form of TOTP
This commit is contained in:
parent
a68b75f069
commit
3fb4d95fd7
30
package-lock.json
generated
30
package-lock.json
generated
@ -73,6 +73,7 @@
|
|||||||
"semver": "7.6.0",
|
"semver": "7.6.0",
|
||||||
"serve-favicon": "2.5.0",
|
"serve-favicon": "2.5.0",
|
||||||
"session-file-store": "1.5.0",
|
"session-file-store": "1.5.0",
|
||||||
|
"speakeasy": "^2.0.0",
|
||||||
"split.js": "1.6.5",
|
"split.js": "1.6.5",
|
||||||
"stream-throttle": "0.1.3",
|
"stream-throttle": "0.1.3",
|
||||||
"striptags": "3.2.0",
|
"striptags": "3.2.0",
|
||||||
@ -2708,6 +2709,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
|
"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": {
|
"node_modules/base64-js": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
@ -11795,6 +11801,17 @@
|
|||||||
"integrity": "sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA==",
|
"integrity": "sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/split.js": {
|
||||||
"version": "1.6.5",
|
"version": "1.6.5",
|
||||||
"resolved": "https://registry.npmjs.org/split.js/-/split.js-1.6.5.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
|
"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": {
|
"base64-js": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
@ -22382,6 +22404,14 @@
|
|||||||
"integrity": "sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA==",
|
"integrity": "sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA==",
|
||||||
"dev": true
|
"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": {
|
"split.js": {
|
||||||
"version": "1.6.5",
|
"version": "1.6.5",
|
||||||
"resolved": "https://registry.npmjs.org/split.js/-/split.js-1.6.5.tgz",
|
"resolved": "https://registry.npmjs.org/split.js/-/split.js-1.6.5.tgz",
|
||||||
|
@ -97,6 +97,7 @@
|
|||||||
"semver": "7.6.0",
|
"semver": "7.6.0",
|
||||||
"serve-favicon": "2.5.0",
|
"serve-favicon": "2.5.0",
|
||||||
"session-file-store": "1.5.0",
|
"session-file-store": "1.5.0",
|
||||||
|
"speakeasy": "^2.0.0",
|
||||||
"split.js": "1.6.5",
|
"split.js": "1.6.5",
|
||||||
"stream-throttle": "0.1.3",
|
"stream-throttle": "0.1.3",
|
||||||
"striptags": "3.2.0",
|
"striptags": "3.2.0",
|
||||||
|
@ -32,6 +32,7 @@ import DatabaseAnonymizationOptions from "./options/advanced/database_anonymizat
|
|||||||
import BackendLogWidget from "./content/backend_log.js";
|
import BackendLogWidget from "./content/backend_log.js";
|
||||||
import AttachmentErasureTimeoutOptions from "./options/other/attachment_erasure_timeout.js";
|
import AttachmentErasureTimeoutOptions from "./options/other/attachment_erasure_timeout.js";
|
||||||
import RibbonOptions from "./options/appearance/ribbon.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">
|
const TPL = `<div class="note-detail-content-widget note-detail-printable">
|
||||||
<style>
|
<style>
|
||||||
@ -59,47 +60,50 @@ const CONTENT_WIDGETS = {
|
|||||||
ZoomFactorOptions,
|
ZoomFactorOptions,
|
||||||
NativeTitleBarOptions,
|
NativeTitleBarOptions,
|
||||||
MaxContentWidthOptions,
|
MaxContentWidthOptions,
|
||||||
RibbonOptions
|
RibbonOptions,
|
||||||
],
|
],
|
||||||
_optionsShortcuts: [ KeyboardShortcutsOptions ],
|
_optionsShortcuts: [KeyboardShortcutsOptions],
|
||||||
_optionsTextNotes: [
|
_optionsTextNotes: [
|
||||||
HeadingStyleOptions,
|
HeadingStyleOptions,
|
||||||
TableOfContentsOptions,
|
TableOfContentsOptions,
|
||||||
HighlightsListOptions,
|
HighlightsListOptions,
|
||||||
TextAutoReadOnlySizeOptions
|
TextAutoReadOnlySizeOptions,
|
||||||
],
|
],
|
||||||
_optionsCodeNotes: [
|
_optionsCodeNotes: [
|
||||||
VimKeyBindingsOptions,
|
VimKeyBindingsOptions,
|
||||||
WrapLinesOptions,
|
WrapLinesOptions,
|
||||||
CodeAutoReadOnlySizeOptions,
|
CodeAutoReadOnlySizeOptions,
|
||||||
CodeMimeTypesOptions
|
CodeMimeTypesOptions,
|
||||||
],
|
],
|
||||||
_optionsImages: [ ImageOptions ],
|
_optionsMFA: [MultiFactorAuthenticationOptions],
|
||||||
_optionsSpellcheck: [ SpellcheckOptions ],
|
_optionsImages: [ImageOptions],
|
||||||
_optionsPassword: [ PasswordOptions ],
|
_optionsSpellcheck: [SpellcheckOptions],
|
||||||
_optionsEtapi: [ EtapiOptions ],
|
_optionsPassword: [PasswordOptions],
|
||||||
_optionsBackup: [ BackupOptions ],
|
_optionsEtapi: [EtapiOptions],
|
||||||
_optionsSync: [ SyncOptions ],
|
_optionsBackup: [BackupOptions],
|
||||||
|
_optionsSync: [SyncOptions],
|
||||||
_optionsOther: [
|
_optionsOther: [
|
||||||
SearchEngineOptions,
|
SearchEngineOptions,
|
||||||
TrayOptions,
|
TrayOptions,
|
||||||
NoteErasureTimeoutOptions,
|
NoteErasureTimeoutOptions,
|
||||||
AttachmentErasureTimeoutOptions,
|
AttachmentErasureTimeoutOptions,
|
||||||
RevisionsSnapshotIntervalOptions,
|
RevisionsSnapshotIntervalOptions,
|
||||||
NetworkConnectionsOptions
|
NetworkConnectionsOptions,
|
||||||
],
|
],
|
||||||
_optionsAdvanced: [
|
_optionsAdvanced: [
|
||||||
DatabaseIntegrityCheckOptions,
|
DatabaseIntegrityCheckOptions,
|
||||||
ConsistencyChecksOptions,
|
ConsistencyChecksOptions,
|
||||||
DatabaseAnonymizationOptions,
|
DatabaseAnonymizationOptions,
|
||||||
AdvancedSyncOptions,
|
AdvancedSyncOptions,
|
||||||
VacuumDatabaseOptions
|
VacuumDatabaseOptions,
|
||||||
],
|
],
|
||||||
_backendLog: [ BackendLogWidget ]
|
_backendLog: [BackendLogWidget],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class ContentWidgetTypeWidget extends TypeWidget {
|
export default class ContentWidgetTypeWidget extends TypeWidget {
|
||||||
static getType() { return "contentWidget"; }
|
static getType() {
|
||||||
|
return "contentWidget";
|
||||||
|
}
|
||||||
|
|
||||||
doRender() {
|
doRender() {
|
||||||
this.$widget = $(TPL);
|
this.$widget = $(TPL);
|
||||||
@ -118,7 +122,9 @@ export default class ContentWidgetTypeWidget extends TypeWidget {
|
|||||||
for (const clazz of contentWidgets) {
|
for (const clazz of contentWidgets) {
|
||||||
const widget = new clazz();
|
const widget = new clazz();
|
||||||
|
|
||||||
await widget.handleEvent('setNoteContext', { noteContext: this.noteContext });
|
await widget.handleEvent("setNoteContext", {
|
||||||
|
noteContext: this.noteContext,
|
||||||
|
});
|
||||||
this.child(widget);
|
this.child(widget);
|
||||||
|
|
||||||
this.$content.append(widget.render());
|
this.$content.append(widget.render());
|
||||||
|
@ -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
24
src/routes/api/totp.ts
Normal 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,
|
||||||
|
};
|
1273
src/routes/routes.ts
1273
src/routes/routes.ts
File diff suppressed because it is too large
Load Diff
@ -1,28 +1,45 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
import optionService = require('../options');
|
import optionService = require("../options");
|
||||||
import crypto = require('crypto');
|
import crypto = require("crypto");
|
||||||
|
|
||||||
function getVerificationHash(password: crypto.BinaryLike) {
|
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) {
|
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) {
|
function getScryptHash(password: crypto.BinaryLike, salt: crypto.BinaryLike) {
|
||||||
const hashed = crypto.scryptSync(password, salt, 32,
|
const hashed = crypto.scryptSync(password, salt, 32, {
|
||||||
{N: 16384, r:8, p:1});
|
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 = {
|
export = {
|
||||||
getVerificationHash,
|
getVerificationHash,
|
||||||
getPasswordDerivedKey
|
getPasswordDerivedKey,
|
||||||
|
getTotpSecretVerificationHash,
|
||||||
|
getTotpSecretDerivedKey,
|
||||||
};
|
};
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
import optionService = require('../options');
|
import optionService = require("../options");
|
||||||
import myScryptService = require('./my_scrypt');
|
import myScryptService = require("./my_scrypt");
|
||||||
import utils = require('../utils');
|
import utils = require("../utils");
|
||||||
import dataEncryptionService = require('./data_encryption');
|
import dataEncryptionService = require("./data_encryption");
|
||||||
|
|
||||||
function verifyPassword(password: string) {
|
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) {
|
if (!dbPasswordHash) {
|
||||||
return false;
|
return false;
|
||||||
@ -18,17 +22,23 @@ function verifyPassword(password: string) {
|
|||||||
function setDataKey(password: string, plainTextDataKey: string | Buffer) {
|
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) {
|
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;
|
||||||
}
|
}
|
||||||
@ -36,5 +46,5 @@ function getDataKey(password: string) {
|
|||||||
export = {
|
export = {
|
||||||
verifyPassword,
|
verifyPassword,
|
||||||
getDataKey,
|
getDataKey,
|
||||||
setDataKey
|
setDataKey,
|
||||||
};
|
};
|
||||||
|
110
src/services/encryption/totp_secret.ts
Normal file
110
src/services/encryption/totp_secret.ts
Normal 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,
|
||||||
|
};
|
52
src/services/encryption/totp_secret_encryption.ts
Normal file
52
src/services/encryption/totp_secret_encryption.ts
Normal 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,
|
||||||
|
};
|
@ -1,10 +1,10 @@
|
|||||||
import BAttribute = require("../becca/entities/battribute");
|
import BAttribute = require("../becca/entities/battribute");
|
||||||
import { AttributeType, NoteType } from "../becca/entities/rows";
|
import { AttributeType, NoteType } from "../becca/entities/rows";
|
||||||
|
|
||||||
import becca = require('../becca/becca');
|
import becca = require("../becca/becca");
|
||||||
import noteService = require('./notes');
|
import noteService = require("./notes");
|
||||||
import log = require('./log');
|
import log = require("./log");
|
||||||
import migrationService = require('./migration');
|
import migrationService = require("./migration");
|
||||||
|
|
||||||
const LBTPL_ROOT = "_lbTplRoot";
|
const LBTPL_ROOT = "_lbTplRoot";
|
||||||
const LBTPL_BASE = "_lbTplBase";
|
const LBTPL_BASE = "_lbTplBase";
|
||||||
@ -19,7 +19,7 @@ interface Attribute {
|
|||||||
type: AttributeType;
|
type: AttributeType;
|
||||||
name: string;
|
name: string;
|
||||||
isInheritable?: boolean;
|
isInheritable?: boolean;
|
||||||
value?: string
|
value?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Item {
|
interface Item {
|
||||||
@ -34,8 +34,20 @@ interface Item {
|
|||||||
baseSize?: string;
|
baseSize?: string;
|
||||||
growthFactor?: string;
|
growthFactor?: string;
|
||||||
targetNoteId?: "_backendLog" | "_globalNoteMap";
|
targetNoteId?: "_backendLog" | "_globalNoteMap";
|
||||||
builtinWidget?: "bookmarks" | "spacer" | "backInHistoryButton" | "forwardInHistoryButton" | "syncStatus" | "protectedSession" | "todayInJournal" | "calendar";
|
builtinWidget?:
|
||||||
command?: "jumpToNote" | "searchNotes" | "createNoteIntoInbox" | "showRecentChanges";
|
| "bookmarks"
|
||||||
|
| "spacer"
|
||||||
|
| "backInHistoryButton"
|
||||||
|
| "forwardInHistoryButton"
|
||||||
|
| "syncStatus"
|
||||||
|
| "protectedSession"
|
||||||
|
| "todayInJournal"
|
||||||
|
| "calendar";
|
||||||
|
command?:
|
||||||
|
| "jumpToNote"
|
||||||
|
| "searchNotes"
|
||||||
|
| "createNoteIntoInbox"
|
||||||
|
| "showRecentChanges";
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -45,218 +57,412 @@ interface Item {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const HIDDEN_SUBTREE_DEFINITION: Item = {
|
const HIDDEN_SUBTREE_DEFINITION: Item = {
|
||||||
id: '_hidden',
|
id: "_hidden",
|
||||||
title: 'Hidden Notes',
|
title: "Hidden Notes",
|
||||||
type: 'doc',
|
type: "doc",
|
||||||
icon: 'bx bx-chip',
|
icon: "bx bx-chip",
|
||||||
// we want to keep the hidden subtree always last, otherwise there will be problems with e.g., keyboard navigation
|
// 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
|
// over tree when it's in the middle
|
||||||
notePosition: 999_999_999,
|
notePosition: 999_999_999,
|
||||||
attributes: [
|
attributes: [
|
||||||
{ type: 'label', name: 'excludeFromNoteMap', isInheritable: true },
|
{ type: "label", name: "excludeFromNoteMap", isInheritable: true },
|
||||||
{ type: 'label', name: 'docName', value: 'hidden' }
|
{ type: "label", name: "docName", value: "hidden" },
|
||||||
],
|
],
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: '_search',
|
id: "_search",
|
||||||
title: 'Search History',
|
title: "Search History",
|
||||||
type: 'doc'
|
type: "doc",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '_globalNoteMap',
|
id: "_globalNoteMap",
|
||||||
title: 'Note Map',
|
title: "Note Map",
|
||||||
type: 'noteMap',
|
type: "noteMap",
|
||||||
attributes: [
|
attributes: [
|
||||||
{ type: 'label', name: 'mapRootNoteId', value: 'hoisted' },
|
{ type: "label", name: "mapRootNoteId", value: "hoisted" },
|
||||||
{ type: 'label', name: 'keepCurrentHoisting' }
|
{ type: "label", name: "keepCurrentHoisting" },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '_sqlConsole',
|
id: "_sqlConsole",
|
||||||
title: 'SQL Console History',
|
title: "SQL Console History",
|
||||||
type: 'doc',
|
type: "doc",
|
||||||
icon: 'bx-data'
|
icon: "bx-data",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '_share',
|
id: "_share",
|
||||||
title: 'Shared Notes',
|
title: "Shared Notes",
|
||||||
type: 'doc',
|
type: "doc",
|
||||||
attributes: [ { type: 'label', name: 'docName', value: 'share' } ]
|
attributes: [{ type: "label", name: "docName", value: "share" }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '_bulkAction',
|
id: "_bulkAction",
|
||||||
title: 'Bulk Action',
|
title: "Bulk Action",
|
||||||
type: 'doc',
|
type: "doc",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '_backendLog',
|
id: "_backendLog",
|
||||||
title: 'Backend Log',
|
title: "Backend Log",
|
||||||
type: 'contentWidget',
|
type: "contentWidget",
|
||||||
icon: 'bx-terminal',
|
icon: "bx-terminal",
|
||||||
attributes: [
|
attributes: [{ type: "label", name: "keepCurrentHoisting" }],
|
||||||
{ type: 'label', name: 'keepCurrentHoisting' }
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// place for user scripts hidden stuff (scripts should not create notes directly under hidden root)
|
// place for user scripts hidden stuff (scripts should not create notes directly under hidden root)
|
||||||
id: '_userHidden',
|
id: "_userHidden",
|
||||||
title: 'User Hidden',
|
title: "User Hidden",
|
||||||
type: 'doc',
|
type: "doc",
|
||||||
attributes: [ { type: 'label', name: 'docName', value: 'user_hidden' } ]
|
attributes: [{ type: "label", name: "docName", value: "user_hidden" }],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: LBTPL_ROOT,
|
id: LBTPL_ROOT,
|
||||||
title: 'Launch Bar Templates',
|
title: "Launch Bar Templates",
|
||||||
type: 'doc',
|
type: "doc",
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: LBTPL_BASE,
|
id: LBTPL_BASE,
|
||||||
title: 'Base Abstract Launcher',
|
title: "Base Abstract Launcher",
|
||||||
type: 'doc'
|
type: "doc",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: LBTPL_COMMAND,
|
id: LBTPL_COMMAND,
|
||||||
title: 'Command Launcher',
|
title: "Command Launcher",
|
||||||
type: 'doc',
|
type: "doc",
|
||||||
attributes: [
|
attributes: [
|
||||||
{ type: 'relation', name: 'template', value: LBTPL_BASE },
|
{ type: "relation", name: "template", value: LBTPL_BASE },
|
||||||
{ type: 'label', name: 'launcherType', value: 'command' },
|
{ type: "label", name: "launcherType", value: "command" },
|
||||||
{ type: 'label', name: 'docName', value: 'launchbar_command_launcher' }
|
{
|
||||||
]
|
type: "label",
|
||||||
|
name: "docName",
|
||||||
|
value: "launchbar_command_launcher",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: LBTPL_NOTE_LAUNCHER,
|
id: LBTPL_NOTE_LAUNCHER,
|
||||||
title: 'Note Launcher',
|
title: "Note Launcher",
|
||||||
type: 'doc',
|
type: "doc",
|
||||||
attributes: [
|
attributes: [
|
||||||
{ type: 'relation', name: 'template', value: LBTPL_BASE },
|
{ type: "relation", name: "template", value: LBTPL_BASE },
|
||||||
{ type: 'label', name: 'launcherType', value: 'note' },
|
{ type: "label", name: "launcherType", value: "note" },
|
||||||
{ type: 'label', name: 'relation:target', value: 'promoted' },
|
{ type: "label", name: "relation:target", value: "promoted" },
|
||||||
{ type: 'label', name: 'relation:hoistedNote', 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: "label",
|
||||||
]
|
name: "label:keyboardShortcut",
|
||||||
|
value: "promoted,text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "label",
|
||||||
|
name: "docName",
|
||||||
|
value: "launchbar_note_launcher",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: LBTPL_SCRIPT,
|
id: LBTPL_SCRIPT,
|
||||||
title: 'Script Launcher',
|
title: "Script Launcher",
|
||||||
type: 'doc',
|
type: "doc",
|
||||||
attributes: [
|
attributes: [
|
||||||
{ type: 'relation', name: 'template', value: LBTPL_BASE },
|
{ type: "relation", name: "template", value: LBTPL_BASE },
|
||||||
{ type: 'label', name: 'launcherType', value: 'script' },
|
{ type: "label", name: "launcherType", value: "script" },
|
||||||
{ type: 'label', name: 'relation:script', value: 'promoted' },
|
{ type: "label", name: "relation:script", value: "promoted" },
|
||||||
{ type: 'label', name: 'label:keyboardShortcut', value: 'promoted,text' },
|
{
|
||||||
{ type: 'label', name: 'docName', value: 'launchbar_script_launcher' }
|
type: "label",
|
||||||
]
|
name: "label:keyboardShortcut",
|
||||||
|
value: "promoted,text",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "label",
|
||||||
|
name: "docName",
|
||||||
|
value: "launchbar_script_launcher",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: LBTPL_BUILTIN_WIDGET,
|
id: LBTPL_BUILTIN_WIDGET,
|
||||||
title: 'Built-in Widget',
|
title: "Built-in Widget",
|
||||||
type: 'doc',
|
type: "doc",
|
||||||
attributes: [
|
attributes: [
|
||||||
{ type: 'relation', name: 'template', value: LBTPL_BASE },
|
{ type: "relation", name: "template", value: LBTPL_BASE },
|
||||||
{ type: 'label', name: 'launcherType', value: 'builtinWidget' }
|
{ type: "label", name: "launcherType", value: "builtinWidget" },
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: LBTPL_SPACER,
|
id: LBTPL_SPACER,
|
||||||
title: 'Spacer',
|
title: "Spacer",
|
||||||
type: 'doc',
|
type: "doc",
|
||||||
icon: 'bx-move-vertical',
|
icon: "bx-move-vertical",
|
||||||
attributes: [
|
attributes: [
|
||||||
{ type: 'relation', name: 'template', value: LBTPL_BUILTIN_WIDGET },
|
{ type: "relation", name: "template", value: LBTPL_BUILTIN_WIDGET },
|
||||||
{ type: 'label', name: 'builtinWidget', value: 'spacer' },
|
{ type: "label", name: "builtinWidget", value: "spacer" },
|
||||||
{ type: 'label', name: 'label:baseSize', value: 'promoted,number' },
|
{ type: "label", name: "label:baseSize", value: "promoted,number" },
|
||||||
{ type: 'label', name: 'label:growthFactor', value: 'promoted,number' },
|
{
|
||||||
{ type: 'label', name: 'docName', value: 'launchbar_spacer' }
|
type: "label",
|
||||||
]
|
name: "label:growthFactor",
|
||||||
|
value: "promoted,number",
|
||||||
|
},
|
||||||
|
{ type: "label", name: "docName", value: "launchbar_spacer" },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: LBTPL_CUSTOM_WIDGET,
|
id: LBTPL_CUSTOM_WIDGET,
|
||||||
title: 'Custom Widget',
|
title: "Custom Widget",
|
||||||
type: 'doc',
|
type: "doc",
|
||||||
attributes: [
|
attributes: [
|
||||||
{ type: 'relation', name: 'template', value: LBTPL_BASE },
|
{ type: "relation", name: "template", value: LBTPL_BASE },
|
||||||
{ type: 'label', name: 'launcherType', value: 'customWidget' },
|
{ type: "label", name: "launcherType", value: "customWidget" },
|
||||||
{ type: 'label', name: 'relation:widget', value: 'promoted' },
|
{ type: "label", name: "relation:widget", value: "promoted" },
|
||||||
{ type: 'label', name: 'docName', value: 'launchbar_widget_launcher' }
|
{
|
||||||
]
|
type: "label",
|
||||||
|
name: "docName",
|
||||||
|
value: "launchbar_widget_launcher",
|
||||||
},
|
},
|
||||||
]
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '_lbRoot',
|
id: "_lbRoot",
|
||||||
title: 'Launch Bar',
|
title: "Launch Bar",
|
||||||
type: 'doc',
|
type: "doc",
|
||||||
icon: 'bx-sidebar',
|
icon: "bx-sidebar",
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
attributes: [ { type: 'label', name: 'docName', value: 'launchbar_intro' } ],
|
attributes: [
|
||||||
|
{ type: "label", name: "docName", value: "launchbar_intro" },
|
||||||
|
],
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: '_lbAvailableLaunchers',
|
id: "_lbAvailableLaunchers",
|
||||||
title: 'Available Launchers',
|
title: "Available Launchers",
|
||||||
type: 'doc',
|
type: "doc",
|
||||||
icon: 'bx-hide',
|
icon: "bx-hide",
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
attributes: [ { type: 'label', name: 'docName', value: 'launchbar_intro' } ],
|
attributes: [
|
||||||
|
{ type: "label", name: "docName", value: "launchbar_intro" },
|
||||||
|
],
|
||||||
children: [
|
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: "_lbBackInHistory",
|
||||||
{ id: '_lbForwardInHistory', title: 'Go to Next Note', type: 'launcher', builtinWidget: 'forwardInHistoryButton', icon: 'bx bxs-right-arrow-square',
|
title: "Go to Previous Note",
|
||||||
attributes: [ { type: 'label', name: 'docName', value: 'launchbar_history_navigation' } ]},
|
type: "launcher",
|
||||||
{ id: '_lbBackendLog', title: 'Backend Log', type: 'launcher', targetNoteId: '_backendLog', icon: 'bx bx-terminal' },
|
builtinWidget: "backInHistoryButton",
|
||||||
]
|
icon: "bx bxs-left-arrow-square",
|
||||||
|
attributes: [
|
||||||
|
{
|
||||||
|
type: "label",
|
||||||
|
name: "docName",
|
||||||
|
value: "launchbar_history_navigation",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '_lbVisibleLaunchers',
|
id: "_lbForwardInHistory",
|
||||||
title: 'Visible Launchers',
|
title: "Go to Next Note",
|
||||||
type: 'doc',
|
type: "launcher",
|
||||||
icon: 'bx-show',
|
builtinWidget: "forwardInHistoryButton",
|
||||||
isExpanded: true,
|
icon: "bx bxs-right-arrow-square",
|
||||||
attributes: [ { type: 'label', name: 'docName', value: 'launchbar_intro' } ],
|
attributes: [
|
||||||
children: [
|
{
|
||||||
{ id: '_lbNewNote', title: 'New Note', type: 'launcher', command: 'createNoteIntoInbox', icon: 'bx bx-file-blank' },
|
type: "label",
|
||||||
{ id: '_lbSearch', title: 'Search Notes', type: 'launcher', command: 'searchNotes', icon: 'bx bx-search', attributes: [
|
name: "docName",
|
||||||
{ type: 'label', name: 'desktopOnly' }
|
value: "launchbar_history_navigation",
|
||||||
] },
|
},
|
||||||
{ 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',
|
id: "_lbBackendLog",
|
||||||
title: 'Options',
|
title: "Backend Log",
|
||||||
type: 'book',
|
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: [
|
children: [
|
||||||
{ id: '_optionsAppearance', title: 'Appearance', type: 'contentWidget', icon: 'bx-layout' },
|
{
|
||||||
{ id: '_optionsShortcuts', title: 'Shortcuts', type: 'contentWidget', icon: 'bxs-keyboard' },
|
id: "_lbNewNote",
|
||||||
{ id: '_optionsTextNotes', title: 'Text Notes', type: 'contentWidget', icon: 'bx-text' },
|
title: "New Note",
|
||||||
{ id: '_optionsCodeNotes', title: 'Code Notes', type: 'contentWidget', icon: 'bx-code' },
|
type: "launcher",
|
||||||
{ id: '_optionsImages', title: 'Images', type: 'contentWidget', icon: 'bx-image' },
|
command: "createNoteIntoInbox",
|
||||||
{ id: '_optionsSpellcheck', title: 'Spellcheck', type: 'contentWidget', icon: 'bx-check-double' },
|
icon: "bx bx-file-blank",
|
||||||
{ 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: "_lbSearch",
|
||||||
{ id: '_optionsSync', title: 'Sync', type: 'contentWidget', icon: 'bx-wifi' },
|
title: "Search Notes",
|
||||||
{ id: '_optionsOther', title: 'Other', type: 'contentWidget', icon: 'bx-dots-horizontal' },
|
type: "launcher",
|
||||||
{ id: '_optionsAdvanced', title: 'Advanced', type: 'contentWidget' }
|
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) {
|
function checkHiddenSubtree(force = false) {
|
||||||
@ -266,15 +472,17 @@ function checkHiddenSubtree(force = false) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
checkHiddenSubtreeRecursively('root', HIDDEN_SUBTREE_DEFINITION);
|
checkHiddenSubtreeRecursively("root", HIDDEN_SUBTREE_DEFINITION);
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkHiddenSubtreeRecursively(parentNoteId: string, item: Item) {
|
function checkHiddenSubtreeRecursively(parentNoteId: string, item: Item) {
|
||||||
if (!item.id || !item.type || !item.title) {
|
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}'`);
|
throw new Error(`ID has to start with underscore, given '${item.id}'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -282,41 +490,63 @@ function checkHiddenSubtreeRecursively(parentNoteId: string, item: Item) {
|
|||||||
let branch;
|
let branch;
|
||||||
|
|
||||||
if (!note) {
|
if (!note) {
|
||||||
({note, branch} = noteService.createNewNote({
|
({ note, branch } = noteService.createNewNote({
|
||||||
noteId: item.id,
|
noteId: item.id,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
type: item.type,
|
type: item.type,
|
||||||
parentNoteId: parentNoteId,
|
parentNoteId: parentNoteId,
|
||||||
content: '',
|
content: "",
|
||||||
ignoreForbiddenParents: true
|
ignoreForbiddenParents: true,
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
branch = note.getParentBranches().find(branch => branch.parentNoteId === parentNoteId);
|
branch = note
|
||||||
|
.getParentBranches()
|
||||||
|
.find((branch) => branch.parentNoteId === parentNoteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const attrs = [...(item.attributes || [])];
|
const attrs = [...(item.attributes || [])];
|
||||||
|
|
||||||
if (item.icon) {
|
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) {
|
if (item.command) {
|
||||||
attrs.push({ type: 'relation', name: 'template', value: LBTPL_COMMAND });
|
attrs.push({ type: "relation", name: "template", value: LBTPL_COMMAND });
|
||||||
attrs.push({ type: 'label', name: 'command', value: item.command });
|
attrs.push({ type: "label", name: "command", value: item.command });
|
||||||
} else if (item.builtinWidget) {
|
} else if (item.builtinWidget) {
|
||||||
if (item.builtinWidget === 'spacer') {
|
if (item.builtinWidget === "spacer") {
|
||||||
attrs.push({ type: 'relation', name: 'template', value: LBTPL_SPACER });
|
attrs.push({ type: "relation", name: "template", value: LBTPL_SPACER });
|
||||||
attrs.push({ type: 'label', name: 'baseSize', value: item.baseSize });
|
attrs.push({ type: "label", name: "baseSize", value: item.baseSize });
|
||||||
attrs.push({ type: 'label', name: 'growthFactor', value: item.growthFactor });
|
attrs.push({
|
||||||
|
type: "label",
|
||||||
|
name: "growthFactor",
|
||||||
|
value: item.growthFactor,
|
||||||
|
});
|
||||||
} else {
|
} 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) {
|
} else if (item.targetNoteId) {
|
||||||
attrs.push({ type: 'relation', name: 'template', value: LBTPL_NOTE_LAUNCHER });
|
attrs.push({
|
||||||
attrs.push({ type: 'relation', name: 'target', value: item.targetNoteId });
|
type: "relation",
|
||||||
|
name: "template",
|
||||||
|
value: LBTPL_NOTE_LAUNCHER,
|
||||||
|
});
|
||||||
|
attrs.push({
|
||||||
|
type: "relation",
|
||||||
|
name: "target",
|
||||||
|
value: item.targetNoteId,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`No action defined for launcher ${JSON.stringify(item)}`);
|
throw new Error(`No action defined for launcher ${JSON.stringify(item)}`);
|
||||||
}
|
}
|
||||||
@ -331,12 +561,18 @@ function checkHiddenSubtreeRecursively(parentNoteId: string, item: Item) {
|
|||||||
if (branch) {
|
if (branch) {
|
||||||
// in case of launchers the branch ID is not preserved and should not be relied upon - launchers which move between
|
// 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
|
// 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.notePosition = item.notePosition;
|
||||||
branch.save();
|
branch.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.isExpanded !== undefined && branch.isExpanded !== item.isExpanded) {
|
if (
|
||||||
|
item.isExpanded !== undefined &&
|
||||||
|
branch.isExpanded !== item.isExpanded
|
||||||
|
) {
|
||||||
branch.isExpanded = item.isExpanded;
|
branch.isExpanded = item.isExpanded;
|
||||||
branch.save();
|
branch.save();
|
||||||
}
|
}
|
||||||
@ -345,14 +581,14 @@ function checkHiddenSubtreeRecursively(parentNoteId: string, item: Item) {
|
|||||||
for (const attr of attrs) {
|
for (const attr of attrs) {
|
||||||
const attrId = note.noteId + "_" + attr.type.charAt(0) + attr.name;
|
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({
|
new BAttribute({
|
||||||
attributeId: attrId,
|
attributeId: attrId,
|
||||||
noteId: note.noteId,
|
noteId: note.noteId,
|
||||||
type: attr.type,
|
type: attr.type,
|
||||||
name: attr.name,
|
name: attr.name,
|
||||||
value: attr.value,
|
value: attr.value,
|
||||||
isInheritable: false
|
isInheritable: false,
|
||||||
}).save();
|
}).save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -371,5 +607,5 @@ export = {
|
|||||||
LBTPL_SCRIPT,
|
LBTPL_SCRIPT,
|
||||||
LBTPL_BUILTIN_WIDGET,
|
LBTPL_BUILTIN_WIDGET,
|
||||||
LBTPL_SPACER,
|
LBTPL_SPACER,
|
||||||
LBTPL_CUSTOM_WIDGET
|
LBTPL_CUSTOM_WIDGET,
|
||||||
};
|
};
|
||||||
|
@ -1,15 +1,25 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1, maximum-scale=1"
|
||||||
|
/>
|
||||||
<title>Login</title>
|
<title>Login</title>
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="<%= assetPath %>/images/app-icons/ios/apple-touch-icon.png">
|
<link
|
||||||
<link rel="shortcut icon" href="favicon.ico">
|
rel="apple-touch-icon"
|
||||||
</head>
|
sizes="180x180"
|
||||||
<body>
|
href="<%= assetPath %>/images/app-icons/ios/apple-touch-icon.png"
|
||||||
<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="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>
|
<h1>Trilium login</h1>
|
||||||
|
|
||||||
<% if (failedAuth) { %>
|
<% if (failedAuth) { %>
|
||||||
@ -22,13 +32,25 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="password">Password</label>
|
<label for="password">Password</label>
|
||||||
<div class="controls">
|
<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>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="checkbox">
|
<div class="checkbox">
|
||||||
<label>
|
<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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -37,21 +59,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Required for correct loading of scripts in Electron
|
// 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;
|
let device;
|
||||||
|
|
||||||
if (window.location.search === '?desktop') {
|
if (window.location.search === "?desktop") {
|
||||||
device = "desktop";
|
device = "desktop";
|
||||||
}
|
} else if (window.location.search === "?mobile") {
|
||||||
else if (window.location.search === '?mobile') {
|
|
||||||
device = "mobile";
|
device = "mobile";
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
device = isMobile() ? "mobile" : "desktop";
|
device = isMobile() ? "mobile" : "desktop";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,16 +91,21 @@
|
|||||||
|
|
||||||
// https://stackoverflow.com/a/73731646/944162
|
// https://stackoverflow.com/a/73731646/944162
|
||||||
function isMobile() {
|
function isMobile() {
|
||||||
const mQ = matchMedia?.('(pointer:coarse)');
|
const mQ = matchMedia?.("(pointer:coarse)");
|
||||||
if (mQ?.media === '(pointer:coarse)') return !!mQ.matches;
|
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) ||
|
return (
|
||||||
/\b(Android|Windows Phone|iPad|iPod)\b/i.test(navigator.userAgent);
|
/\b(BlackBerry|webOS|iPhone|IEMobile)\b/i.test(navigator.userAgent) ||
|
||||||
|
/\b(Android|Windows Phone|iPad|iPod)\b/i.test(navigator.userAgent)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<link href="<%= assetPath %>/libraries/bootstrap/css/bootstrap.min.css" rel="stylesheet">
|
<link
|
||||||
</body>
|
href="<%= assetPath %>/libraries/bootstrap/css/bootstrap.min.css"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user