diff --git a/src/public/app/widgets/type_widgets/options/multi_factor_authentication.js b/src/public/app/widgets/type_widgets/options/multi_factor_authentication.js index 4a8f93c0a..3aee5b6d1 100644 --- a/src/public/app/widgets/type_widgets/options/multi_factor_authentication.js +++ b/src/public/app/widgets/type_widgets/options/multi_factor_authentication.js @@ -17,6 +17,26 @@ const TPL = ` 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. +
+

TOTP Settings

+
+ + +
+ +
+
+ + + +
+ +
+

Generate TOTP Secret


@@ -28,7 +48,10 @@ export default class MultiFactorAuthenticationOptions extends OptionsWidget { this.$mfaHeadding = this.$widget.find(".mfa-heading"); this.$regenerateTotpButton = this.$widget.find(".regenerate-totp"); + this.$totpEnabled = this.$widget.find(".totp-enabled"); this.$totpSecret = this.$widget.find(".totp-secret"); + this.$totpSecretInput = this.$widget.find(".totp-secret-input"); + this.$saveTotpButton = this.$widget.find(".save-totp"); this.$mfaHeadding.text("Multi-Factor Authentication"); this.generateKey(); @@ -36,6 +59,10 @@ export default class MultiFactorAuthenticationOptions extends OptionsWidget { // var gen = require("speakeasy"); // toastService.showMessage("***REMOVED***"); + this.$totpEnabled.on("change", async () => { + this.updateCheckboxOption("totpEnabled", this.$totpEnabled); + }); + this.$regenerateTotpButton.on("click", async () => { this.generateKey(); }); @@ -54,7 +81,6 @@ export default class MultiFactorAuthenticationOptions extends OptionsWidget { 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); @@ -62,13 +88,40 @@ export default class MultiFactorAuthenticationOptions extends OptionsWidget { }); } - optionsLoaded(options) { - const isPasswordSet = options.isPasswordSet === "true"; + // async toggleTOTP() { + // if (this.$totpEnabled) + // server.post("totp/enable").then((result) => { + // if (!result.success) toastService.showError(result.message); + // }); + // else + // server.post("totp/disable").then((result) => { + // if (!result.success) toastService.showError(result.message); + // }); + // } + + optionsLoaded(options) { + // I need to make sure that this is actually pinging the server and that this information is being pulled + // because it is telling me "totpEnabled is not allowed to be changed" in a toast every time I check the box + server.get("totp/enabled").then((result) => { + if (result.success) { + console.log("Result message: " + typeof result.message); + console.log("Result message: " + result.message); + this.setCheckboxState(this.$totpEnabled, result.message); + + console.log("TOTP Status: " + typeof result.message); + + if (result.message) { + this.$totpSecretInput.prop("disabled", false); + this.$saveTotpButton.prop("disabled", false); + } else { + this.$totpSecretInput.prop("disabled", true); + this.$saveTotpButton.prop("disabled", true); + } + } else { + toastService.showError(result.message); + } + }); - this.$widget.find(".old-password-form-group").toggle(isPasswordSet); - this.$savePasswordButton.text( - isPasswordSet ? "Change fff Password" : "Set Password" - ); this.$protectedSessionTimeout.val(options.protectedSessionTimeout); } diff --git a/src/public/app/widgets/type_widgets/options/options_widget.js b/src/public/app/widgets/type_widgets/options/options_widget.js index 3f1630870..8798cd20b 100644 --- a/src/public/app/widgets/type_widgets/options/options_widget.js +++ b/src/public/app/widgets/type_widgets/options/options_widget.js @@ -3,55 +3,55 @@ import toastService from "../../../services/toast.js"; import NoteContextAwareWidget from "../../note_context_aware_widget.js"; export default class OptionsWidget extends NoteContextAwareWidget { - constructor() { - super(); + constructor() { + super(); - this.contentSized(); - } - - async updateOption(name, value) { - const opts = { [name]: value }; - - await this.updateMultipleOptions(opts); - } - - async updateMultipleOptions(opts) { - await server.put('options', opts); - - this.showUpdateNotification(); - } - - showUpdateNotification() { - toastService.showPersistent({ - id: "options-change-saved", - title: "Options status", - message: "Options change have been saved.", - icon: "slider", - closeAfter: 2000 - }); - } - - async updateCheckboxOption(name, $checkbox) { - const isChecked = $checkbox.prop("checked"); - - return await this.updateOption(name, isChecked ? 'true' : 'false'); - } - - setCheckboxState($checkbox, optionValue) { - $checkbox.prop('checked', optionValue === 'true'); - } - - optionsLoaded(options) {} - - async refreshWithNote(note) { - const options = await server.get('options'); - - this.optionsLoaded(options); - } - - async entitiesReloadedEvent({loadResults}) { - if (loadResults.getOptionNames().length > 0) { - this.refresh(); - } + this.contentSized(); + } + + async updateOption(name, value) { + const opts = { [name]: value }; + + await this.updateMultipleOptions(opts); + } + + async updateMultipleOptions(opts) { + await server.put("options", opts); + + this.showUpdateNotification(); + } + + showUpdateNotification() { + toastService.showPersistent({ + id: "options-change-saved", + title: "Options status", + message: "Options change have been saved.", + icon: "slider", + closeAfter: 2000, + }); + } + + async updateCheckboxOption(name, $checkbox) { + const isChecked = $checkbox.prop("checked"); + + return await this.updateOption(name, isChecked ? "true" : "false"); + } + + setCheckboxState($checkbox, optionValue) { + $checkbox.prop("checked", optionValue === "true"); + } + + optionsLoaded(options) {} + + async refreshWithNote(note) { + const options = await server.get("options"); + + this.optionsLoaded(options); + } + + async entitiesReloadedEvent({ loadResults }) { + if (loadResults.getOptionNames().length > 0) { + this.refresh(); } + } } diff --git a/src/routes/api/options.ts b/src/routes/api/options.ts index 54630e825..24d7b35ad 100644 --- a/src/routes/api/options.ts +++ b/src/routes/api/options.ts @@ -86,6 +86,7 @@ function updateOption(req: Request) { function updateOptions(req: Request) { for (const optionName in req.body) { + console.log( optionName ) if (!update(optionName, req.body[optionName])) { // this should be improved // it should return 400 instead of current 500, but at least it now rollbacks transaction diff --git a/src/routes/api/totp.ts b/src/routes/api/totp.ts index d4c48bfb3..a4297c130 100644 --- a/src/routes/api/totp.ts +++ b/src/routes/api/totp.ts @@ -1,3 +1,4 @@ +import options = require("../../services/options"); const speakeasy = require("speakeasy"); function verifyOTPToken(guessedToken: any) { @@ -18,7 +19,25 @@ function generateSecret() { return { success: "true", message: speakeasy.generateSecret().base32 }; } +function checkForTOTP() { + const totpEnabled = options.getOptionBool("totpEnabled") + return { success: "true", message: totpEnabled }; +} + +function enableTOTP() { + options.setOption("totpEnabled", true) + return { success: "true" }; +} + +function disableTOTP() { + options.setOption("totpEnabled", false) + return { success: "true" }; +} + export = { verifyOTPToken, generateSecret, + checkForTOTP, + enableTOTP, + disableTOTP }; diff --git a/src/routes/login.ts b/src/routes/login.ts index cce214fbe..14f12fdc0 100644 --- a/src/routes/login.ts +++ b/src/routes/login.ts @@ -14,6 +14,8 @@ import { AppRequest } from './route-interface'; function loginPage(req: Request, res: Response) { res.render('login', { failedAuth: false, + // totpEnabled: optionService.getOption("hasOTPEnabled"), + totpEnabled: false, assetPath: assetPath, appPath: appPath }); @@ -59,6 +61,7 @@ function setPassword(req: Request, res: Response) { function login(req: AppRequest, res: Response) { const guessedPassword = req.body.password; + const guessedTotp = req.body.token; if (verifyPassword(guessedPassword)) { const rememberMe = req.body.rememberMe; diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 5d9d6c034..6f1eb3f1d 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -156,6 +156,9 @@ function register(app: express.Application) { route(GET, "/setup", [], setupRoute.setupPage); apiRoute(GET, "/api/totp/generate", totp.generateSecret); + apiRoute(GET, "/api/totp/enabled", totp.checkForTOTP); + apiRoute(GET, "/api/totp/enable", totp.enableTOTP); + apiRoute(GET, "/api/totp/disable", totp.disableTOTP); apiRoute(GET, "/api/tree", treeApiRoute.getTree); apiRoute(PST, "/api/tree/load", treeApiRoute.load); diff --git a/src/services/options_init.ts b/src/services/options_init.ts index e2ba1520a..49fa85461 100644 --- a/src/services/options_init.ts +++ b/src/services/options_init.ts @@ -96,7 +96,8 @@ const defaultOptions = [ { name: 'customSearchEngineName', value: 'DuckDuckGo', isSynced: true }, { name: 'customSearchEngineUrl', value: 'https://duckduckgo.com/?q={keyword}', isSynced: true }, { name: 'promotedAttributesOpenInRibbon', value: 'true', isSynced: true }, - { name: 'editedNotesOpenInRibbon', value: 'true', isSynced: true } + { name: 'editedNotesOpenInRibbon', value: 'true', isSynced: true }, + // { name: 'totpEnabled', value: 'false', isSynced: false } ]; function initStartupOptions() { diff --git a/src/views/login.ejs b/src/views/login.ejs index b84780997..4510fef07 100644 --- a/src/views/login.ejs +++ b/src/views/login.ejs @@ -41,6 +41,21 @@ /> + <% if( totpEnabled ) { %> +
+ +
+ +
+ <% } %>