From 77f62b94cc09902ad86d6b00e76640897b443518 Mon Sep 17 00:00:00 2001 From: Jin <22962980+JYC333@users.noreply.github.com> Date: Sat, 29 Mar 2025 01:40:17 +0100 Subject: [PATCH] =?UTF-8?q?refactor:=20=F0=9F=92=A1=20refact=20recovery=20?= =?UTF-8?q?code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../options/multi_factor_authentication.ts | 22 ++++++++++++------- src/public/translations/cn/translation.json | 2 ++ src/public/translations/en/translation.json | 2 ++ src/routes/api/recovery_codes.ts | 21 ++++++++++++++---- src/services/encryption/recovery_codes.ts | 22 ++----------------- 5 files changed, 37 insertions(+), 32 deletions(-) diff --git a/src/public/app/widgets/type_widgets/options/multi_factor_authentication.ts b/src/public/app/widgets/type_widgets/options/multi_factor_authentication.ts index b1ff1e070..c369e676f 100644 --- a/src/public/app/widgets/type_widgets/options/multi_factor_authentication.ts +++ b/src/public/app/widgets/type_widgets/options/multi_factor_authentication.ts @@ -126,9 +126,9 @@ interface TOTPStatus { interface RecoveryKeysResponse { success: boolean; - recoveryCodes?: string; + recoveryCodes?: string[]; keysExist?: boolean; - usedRecoveryCodes?: string; + usedRecoveryCodes?: string[]; } export default class MultiFactorAuthenticationOptions extends OptionsWidget { @@ -231,6 +231,7 @@ export default class MultiFactorAuthenticationOptions extends OptionsWidget { } const usedResult = await server.get("totp_recovery/used"); + if (usedResult.usedRecoveryCodes) { this.keyFiller(usedResult.usedRecoveryCodes); this.$generateRecoveryCodeButton.text(t("multi_factor_authentication.recovery_keys_regenerate")); @@ -239,14 +240,19 @@ export default class MultiFactorAuthenticationOptions extends OptionsWidget { } } - private keyFiller(values: string) { - const keys = values.split(',').slice(0, 8); - + private keyFiller(values: string[]) { this.fillKeys(""); - keys.forEach((key, index) => { - if (index < 8 && key && typeof key === 'string') { - this.$recoveryKeys[index].text(key.trim()); + values.forEach((key, index) => { + if (typeof key === 'string') { + const date = new Date(key.replace(/\//g, '-')); + if (isNaN(date.getTime())) { + this.$recoveryKeys[index].text(key); + } else { + this.$recoveryKeys[index].text(t("multi_factor_authentication.recovery_keys_used", { date: key.replace(/\//g, '-') })); + } + } else { + this.$recoveryKeys[index].text(t("multi_factor_authentication.recovery_keys_unused", { index: key })); } }); } diff --git a/src/public/translations/cn/translation.json b/src/public/translations/cn/translation.json index a807c2bfc..7d4531990 100644 --- a/src/public/translations/cn/translation.json +++ b/src/public/translations/cn/translation.json @@ -1322,6 +1322,8 @@ "recovery_keys_no_key_set": "未设置恢复代码", "recovery_keys_generate": "生成恢复代码", "recovery_keys_regenerate": "重新生成恢复代码", + "recovery_keys_used": "已使用: {{date}}", + "recovery_keys_unused": "恢复代码 {{index}} 未使用", "oauth_title": "OAuth/OpenID 认证", "oauth_description": "OpenID 是一种标准化方式,允许您使用其他服务(如 Google)的账户登录网站,以验证您的身份。请参阅这些 指南 通过 Google 设置 OpenID 服务。", "oauth_description_warning": "要启用 OAuth/OpenID,您需要设置 config.ini 文件中的 OAuth/OpenID 基础 URL、客户端 ID 和客户端密钥,并重新启动应用程序。", diff --git a/src/public/translations/en/translation.json b/src/public/translations/en/translation.json index 8b3308575..7028ca80d 100644 --- a/src/public/translations/en/translation.json +++ b/src/public/translations/en/translation.json @@ -1333,6 +1333,8 @@ "recovery_keys_no_key_set": "No recovery codes set", "recovery_keys_generate": "Generate Recovery Codes", "recovery_keys_regenerate": "Regenerate Recovery Codes", + "recovery_keys_used": "Used: {{date}}", + "recovery_keys_unused": "Recovery code {{index}} is unused", "oauth_title": "OAuth/OpenID", "oauth_description": "OpenID is a standardized way to let you log into websites using an account from another service, like Google, to verify your identity. Follow these instructions to setup an OpenID service through Google.", "oauth_description_warning": "To enable OAuth/OpenID, you need to set the OAuth/OpenID base URL, client ID and client secret in the config.ini file and restart the application.", diff --git a/src/routes/api/recovery_codes.ts b/src/routes/api/recovery_codes.ts index dc8276715..a8487eab3 100644 --- a/src/routes/api/recovery_codes.ts +++ b/src/routes/api/recovery_codes.ts @@ -3,7 +3,7 @@ import type { Request } from 'express'; import { randomBytes } from 'crypto'; function setRecoveryCodes(req: Request) { - const success = recovery_codes.setRecoveryCodes(req.body.recoveryCodes); + const success = recovery_codes.setRecoveryCodes(req.body.recoveryCodes.join(',')); return { success: success, message: 'Recovery codes set!' }; } @@ -31,15 +31,28 @@ function generateRecoveryCodes() { randomBytes(16).toString('base64') ]; - recovery_codes.setRecoveryCodes(recoveryKeys.toString()); + recovery_codes.setRecoveryCodes(recoveryKeys.join(',')); - return { success: true, recoveryCodes: recoveryKeys.toString() }; + return { success: true, recoveryCodes: recoveryKeys }; } function getUsedRecoveryCodes() { + if (!recovery_codes.isRecoveryCodeSet()) { + return [] + } + + const dateRegex = RegExp(/^\d{4}\/\d{2}\/\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/gm); + const recoveryCodes = recovery_codes.getRecoveryCodes(); + const usedStatus: string[] = []; + + recoveryCodes.forEach((recoveryKey: string) => { + if (dateRegex.test(recoveryKey)) usedStatus.push(recoveryKey); + else usedStatus.push(recoveryCodes.indexOf(recoveryKey)); + }); + return { success: true, - usedRecoveryCodes: recovery_codes.getUsedRecoveryCodes().toString() + usedRecoveryCodes: usedStatus }; } diff --git a/src/services/encryption/recovery_codes.ts b/src/services/encryption/recovery_codes.ts index dde277741..72c0012a8 100644 --- a/src/services/encryption/recovery_codes.ts +++ b/src/services/encryption/recovery_codes.ts @@ -1,5 +1,3 @@ -'use strict'; - import sql from '../sql.js'; import optionService from '../options.js'; import crypto from 'crypto'; @@ -26,7 +24,7 @@ function setRecoveryCodes(recoveryCodes: string) { function getRecoveryCodes() { if (!isRecoveryCodeSet()) { - return Array(8).fill("Keys not set") + return [] } return sql.transactional(() => { @@ -67,25 +65,9 @@ function verifyRecoveryCode(recoveryCodeGuess: string) { return loginSuccess; } -function getUsedRecoveryCodes() { - if (!isRecoveryCodeSet()) { - return Array(8).fill("Recovery code not set") - } - - const dateRegex = RegExp(/^\d{4}\/\d{2}\/\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/gm); - const recoveryCodes = getRecoveryCodes(); - const usedStatus: string[] = []; - - recoveryCodes.forEach((recoveryKey: string) => { - if (dateRegex.test(recoveryKey)) usedStatus.push('Used: ' + recoveryKey); - else usedStatus.push('Recovery code ' + recoveryCodes.indexOf(recoveryKey) + ' is unused'); - }); - return usedStatus; -} - export default { setRecoveryCodes, + getRecoveryCodes, verifyRecoveryCode, - getUsedRecoveryCodes, isRecoveryCodeSet }; \ No newline at end of file