+ this.logger.info(_scope, 'finished', { ctx });
+ }
+
+
+ /**
+ * Page for modifying credentials and OTP.
+ * @param {http.ServerResponse} res
+ * @param {Object} ctx
+ */
+ async getAdminSettings(res, ctx) {
+ const _scope = _fileScope('getAdminSettings');
+ this.logger.debug(_scope, 'called', { ctx });
+
+ try {
+ await this.db.context(async (dbCtx) => {
+ const authData = await this.db.authenticationGet(dbCtx, ctx.authenticationId);
+ if (!authData) {
+ ctx.errors.push('Sorry, you do not seem to exist! <pre>¯\\_(ツ)_/¯</pre> Cannot do anything useful here!');
+ return;
+ }
+ ctx.otpKey = authData.otpKey;
+ }); // dbCtx
+ } catch (e) {
+ this.logger.error(_scope, 'failed', { ctx, error: e });
+ ctx.errors.push('An error was encountered. Sorry that is not very helpful.');
+ }
+
+ res.end(Template.SettingsHTML(ctx, this.options));
+ this.logger.info(_scope, 'finished', { ctx });
+ }
+
+
+ /**
+ * Page for modifying credentials and OTP.
+ * @param {http.ServerResponse} res
+ * @param {Object} ctx
+ */
+ async postAdminSettings(res, ctx) {
+ const _scope = _fileScope('postAdminSettings');
+ this.logger.debug(_scope, 'called', { ctx });
+
+ try {
+ await this.db.context(async (dbCtx) => {
+ const authData = await this.db.authenticationGet(dbCtx, ctx.authenticationId);
+ if (!authData) {
+ ctx.errors.push('Sorry, you do not seem to exist! <pre>¯\\_(ツ)_/¯</pre> Cannot do anything useful here!');
+ return;
+ }
+ ctx.otpKey = authData.otpKey;
+
+ const otpSubmitButton = ctx.parsedBody?.otp;
+ switch (otpSubmitButton) {
+ case 'disable':
+ await this._otpDisable(dbCtx, ctx, authData);
+ return;
+
+ case 'confirm':
+ await this._otpConfirm(dbCtx, ctx);
+ return;
+
+ case 'enable':
+ await this._otpEnable(ctx);
+ return;
+ }
+
+ const credentialSubmitButton = ctx.parsedBody?.credential;
+ switch (credentialSubmitButton) { // eslint-disable-line sonarjs/no-small-switch
+ case 'update':
+ await this._credentialUpdate(dbCtx, ctx, authData);
+ return;
+ }
+ }); // dbCtx
+ } catch (e) {
+ this.logger.error(_scope, 'failed', { ctx, error: e });
+ ctx.errors.push('An error was encountered. Sorry that is not very helpful.');
+ }
+
+ res.end(Template.SettingsHTML(ctx, this.options));
+ this.logger.info(_scope, 'finished', { ctx });
+ }
+
+
+ /**
+ * Submission to disable OTP.
+ * @param {*} dbCtx
+ * @param {*} ctx
+ * @param {AuthInfo} authData
+ */
+ async _otpDisable(dbCtx, ctx, authData) {
+ const _scope = _fileScope('_otpDisable');
+ try {
+ authData.otpKey = null;
+ await this.db.authenticationUpdateOTPKey(dbCtx, ctx.authenticationId, null);
+ ctx.notifications.push('OTP removed!');
+ delete ctx.otpKey;
+ this.logger.info(_scope, 'otp disabled', { identifier: ctx.authenticationId });
+ } catch (e) {
+ this.logger.error(_scope, 'failed', { error: e, ctx });
+ ctx.errors.push('Failed to disable OTP!');
+ }
+ }
+
+
+ /**
+ * Submission to enable OTP.
+ * @param {Object} ctx
+ */
+ async _otpEnable(ctx) {
+ const _scope = _fileScope('_otpEnable');
+ try {
+ ctx.otpConfirmKey = await TOTP.createKey('sha1', 'base32');
+ ctx.otpConfirmBox = await this.mysteryBox.pack({
+ otpKey: ctx.otpConfirmKey,
+ otpAttempt: 0,
+ otpInitiatedMs: Date.now(),
+ });
+ } catch (e) {
+ delete ctx.otpConfirmKey;
+ delete ctx.otpConfirmBox;
+ this.logger.error(_scope, 'failed', { error: e, ctx });
+ ctx.errors.push('Failed to enable OTP!');
+ }
+ }
+
+
+ /**
+ * Submission to confirm enabling OTP.
+ * @param {*} dbCtx
+ * @param {Object} ctx
+ */
+ async _otpConfirm(dbCtx, ctx) {
+ const _scope = _fileScope('_otpConfirm');
+
+ const {
+ 'otp-box': otpConfirmBox,
+ 'otp-token': otpToken,
+ } = ctx.parsedBody;
+ let otpKey, otpAttempt, otpInitiatedMs;
+ try {
+ ({ otpKey, otpAttempt, otpInitiatedMs } = await this.mysteryBox.unpack(otpConfirmBox));
+ } catch (e) {
+ this.logger.debug(_scope, 'failed to unpack otp box', { error: e, ctx });
+ ctx.errors.push('Problem with form data.');
+ return;
+ }
+ if (!otpToken) {
+ // No token entered, just prompt again.
+ ctx.otpConfirmKey = otpKey;
+ ctx.otpConfirmBox = otpConfirmBox;
+ ctx.notifications.push('Please enter the OTP token to enable 2FA.');
+ return;
+ }
+ otpAttempt += 1;
+ const totp = new TOTP({
+ key: otpKey,
+ keyEncoding: 'base32',
+ });
+ if (!totp.validate(otpToken)) {
+ // Bad token, prompt again.
+ ctx.otpConfirmKey = otpKey;
+ ctx.otpConfirmBox = await this.mysteryBox.pack({
+ otpKey,
+ otpAttempt,
+ otpInitiatedMs,
+ });
+ ctx.errors.push('Invalid token!');
+ return;
+ }
+
+ try {
+ await this.db.context(async (dbCtx) => {
+ await this.db.authenticationUpdateOTPKey(dbCtx, ctx.authenticationId, otpKey);
+ ctx.otpKey = otpKey;
+ ctx.notifications.push('OTP enabled!');
+ this.logger.info(_scope, 'otp enabled', { identifier: ctx.authenticationId, otpAttempt, otpInitiatedMs });
+ }); // dbCtx
+ } catch (e) {
+ this.logger.debug(_scope, 'failed', { error: e, ctx });
+ ctx.errors.push('An error occurred, OTP was not enabled. Sorry this is not very helpful.');
+ }
+ }
+
+
+ /**
+ * Submission to set new credential.
+ * @param {*} dbCtx
+ * @param {Object} ctx
+ * @param {AuthInfo} authData
+ */
+ async _credentialUpdate(dbCtx, ctx, authData) {
+ const _scope = _fileScope('_credentialUpdate');
+ try {
+ const {
+ 'credential-new': newCredential,
+ 'credential-new-2': newCredential2,
+ 'credential-current': oldCredential,
+ } = ctx.parsedBody;
+ if (newCredential !== newCredential2) {
+ ctx.errors.push('New password confirmation did not match!');
+ }
+ if (!newCredential) {
+ ctx.errors.push('Password cannot be empty!');
+ }
+ if (! await this.authenticator._validateAuthDataCredential(authData, oldCredential)) {
+ ctx.errors.push('Invalid current password!');
+ }
+ if (ctx.errors.length) {
+ return;
+ }
+ // update credential
+ await this.authenticator.updateCredential(dbCtx, ctx.authenticationId, newCredential);
+ ctx.notifications.push('Password updated!');
+ } catch (e) {
+ this.logger.error(_scope, 'failed', { error: e, ctx });
+ ctx.errors.push('Failed to update password!');
+ }