mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 21:00:41 +00:00
fix: update 2FA support descriptions and improve error handling in TOTP actions
This commit is contained in:
@@ -36,7 +36,7 @@ English:[`README_EN.md`](./README_EN.md)
|
|||||||
| Send | ✅ | ✅ | 已支持文本 Send 与文件 Send |
|
| Send | ✅ | ✅ | 已支持文本 Send 与文件 Send |
|
||||||
| 多用户 | ✅ | ✅ | 完整的用户管理,邀请机制 |
|
| 多用户 | ✅ | ✅ | 完整的用户管理,邀请机制 |
|
||||||
| 组织/集合/成员权限 | ✅ | ❌ | 没必要实现 |
|
| 组织/集合/成员权限 | ✅ | ❌ | 没必要实现 |
|
||||||
| 登录 2FA(TOTP/WebAuthn/Duo/Email) | ✅ | ⚠️ 部分支持 | 仅支持 TOTP(通过 `TOTP_SECRET`) |
|
| 登录 2FA(TOTP/WebAuthn/Duo/Email) | ✅ | ⚠️ 部分支持 | 仅支持用户级 TOTP |
|
||||||
| SSO / SCIM / 企业目录 | ✅ | ❌ | 没必要实现 |
|
| SSO / SCIM / 企业目录 | ✅ | ❌ | 没必要实现 |
|
||||||
| 紧急访问 | ✅ | ❌ | 没必要实现 |
|
| 紧急访问 | ✅ | ❌ | 没必要实现 |
|
||||||
| 管理后台 / 计费订阅 | ✅ | ❌ | 纯免费 |
|
| 管理后台 / 计费订阅 | ✅ | ❌ | 纯免费 |
|
||||||
|
|||||||
+1
-1
@@ -36,7 +36,7 @@
|
|||||||
| Multi-user | ✅ | ✅ | Full user management with invitation mechanism |
|
| Multi-user | ✅ | ✅ | Full user management with invitation mechanism |
|
||||||
| Send | ✅ | ✅ | Text Send and File Send are supported |
|
| Send | ✅ | ✅ | Text Send and File Send are supported |
|
||||||
| Organizations / Collections / Member roles | ✅ | ❌ | Not necessary to implement |
|
| Organizations / Collections / Member roles | ✅ | ❌ | Not necessary to implement |
|
||||||
| Login 2FA (TOTP/WebAuthn/Duo/Email) | ✅ | ⚠️ Partial | TOTP-only via `TOTP_SECRET` |
|
| Login 2FA (TOTP/WebAuthn/Duo/Email) | ✅ | ⚠️ Partial | User-level TOTP only |
|
||||||
| SSO / SCIM / Enterprise directory | ✅ | ❌ | Not necessary to implement |
|
| SSO / SCIM / Enterprise directory | ✅ | ❌ | Not necessary to implement |
|
||||||
| Emergency access | ✅ | ❌ | Not necessary to implement |
|
| Emergency access | ✅ | ❌ | Not necessary to implement |
|
||||||
| Admin console / Billing & subscription | ✅ | ❌ | Free only |
|
| Admin console / Billing & subscription | ✅ | ❌ | Free only |
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ function toProfile(user: User, env: Env): ProfileResponse {
|
|||||||
usesKeyConnector: false,
|
usesKeyConnector: false,
|
||||||
masterPasswordHint: null,
|
masterPasswordHint: null,
|
||||||
culture: 'en-US',
|
culture: 'en-US',
|
||||||
twoFactorEnabled: !!user.totpSecret || isTotpEnabled(env.TOTP_SECRET),
|
twoFactorEnabled: !!user.totpSecret,
|
||||||
key: user.key,
|
key: user.key,
|
||||||
privateKey: user.privateKey,
|
privateKey: user.privateKey,
|
||||||
accountKeys: null,
|
accountKeys: null,
|
||||||
|
|||||||
@@ -19,13 +19,10 @@ const TWO_FACTOR_PROVIDER_RECOVERY_CODE_RESPONSE = '-1';
|
|||||||
const TWO_FACTOR_PROVIDER_RECOVERY_CODE_LEGACY = 8;
|
const TWO_FACTOR_PROVIDER_RECOVERY_CODE_LEGACY = 8;
|
||||||
const TWO_FACTOR_PROVIDER_RECOVERY_CODE_ANDROID_REQUEST = 100;
|
const TWO_FACTOR_PROVIDER_RECOVERY_CODE_ANDROID_REQUEST = 100;
|
||||||
|
|
||||||
function resolveTotpSecret(userSecret: string | null, envSecret: string | undefined): string | null {
|
function resolveTotpSecret(userSecret: string | null): string | null {
|
||||||
if (userSecret && isTotpEnabled(userSecret)) {
|
if (userSecret && isTotpEnabled(userSecret)) {
|
||||||
return userSecret;
|
return userSecret;
|
||||||
}
|
}
|
||||||
if (isTotpEnabled(envSecret)) {
|
|
||||||
return envSecret!;
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,9 +152,9 @@ export async function handleToken(request: Request, env: Env): Promise<Response>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional 2FA: enabled per-user secret first, then falls back to global env secret for compatibility.
|
// Optional 2FA: enabled only by per-user secret.
|
||||||
let trustedTwoFactorTokenToReturn: string | undefined;
|
let trustedTwoFactorTokenToReturn: string | undefined;
|
||||||
const effectiveTotpSecret = resolveTotpSecret(user.totpSecret, env.TOTP_SECRET);
|
const effectiveTotpSecret = resolveTotpSecret(user.totpSecret);
|
||||||
if (effectiveTotpSecret) {
|
if (effectiveTotpSecret) {
|
||||||
const canUseRecoveryCode = !!user.totpRecoveryCode;
|
const canUseRecoveryCode = !!user.totpRecoveryCode;
|
||||||
const normalizedTwoFactorProvider = String(twoFactorProvider ?? '').trim();
|
const normalizedTwoFactorProvider = String(twoFactorProvider ?? '').trim();
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { errorResponse } from '../utils/response';
|
|||||||
import { cipherToResponse } from './ciphers';
|
import { cipherToResponse } from './ciphers';
|
||||||
import { sendToResponse } from './sends';
|
import { sendToResponse } from './sends';
|
||||||
import { LIMITS } from '../config/limits';
|
import { LIMITS } from '../config/limits';
|
||||||
import { isTotpEnabled } from '../utils/totp';
|
|
||||||
|
|
||||||
interface SyncCacheEntry {
|
interface SyncCacheEntry {
|
||||||
body: string;
|
body: string;
|
||||||
@@ -76,7 +75,7 @@ export async function handleSync(request: Request, env: Env, userId: string): Pr
|
|||||||
usesKeyConnector: false,
|
usesKeyConnector: false,
|
||||||
masterPasswordHint: null,
|
masterPasswordHint: null,
|
||||||
culture: 'en-US',
|
culture: 'en-US',
|
||||||
twoFactorEnabled: !!user.totpSecret || isTotpEnabled(env.TOTP_SECRET),
|
twoFactorEnabled: !!user.totpSecret,
|
||||||
key: user.key,
|
key: user.key,
|
||||||
privateKey: user.privateKey,
|
privateKey: user.privateKey,
|
||||||
accountKeys: null,
|
accountKeys: null,
|
||||||
|
|||||||
+4
-2
@@ -611,14 +611,16 @@ export default function App() {
|
|||||||
|
|
||||||
async function enableTotpAction(secret: string, token: string) {
|
async function enableTotpAction(secret: string, token: string) {
|
||||||
if (!secret.trim() || !token.trim()) {
|
if (!secret.trim() || !token.trim()) {
|
||||||
pushToast('error', t('txt_secret_and_code_are_required'));
|
const error = new Error(t('txt_secret_and_code_are_required'));
|
||||||
return;
|
pushToast('error', error.message);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await setTotp(authedFetch, { enabled: true, secret: secret.trim(), token: token.trim() });
|
await setTotp(authedFetch, { enabled: true, secret: secret.trim(), token: token.trim() });
|
||||||
pushToast('success', t('txt_totp_enabled'));
|
pushToast('success', t('txt_totp_enabled'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
pushToast('error', error instanceof Error ? error.message : t('txt_enable_totp_failed'));
|
pushToast('error', error instanceof Error ? error.message : t('txt_enable_totp_failed'));
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,10 +55,14 @@ export default function SettingsPage(props: SettingsPageProps) {
|
|||||||
}, [props.profile.email, secret]);
|
}, [props.profile.email, secret]);
|
||||||
|
|
||||||
async function enableTotp(): Promise<void> {
|
async function enableTotp(): Promise<void> {
|
||||||
|
try {
|
||||||
await props.onEnableTotp(secret, token);
|
await props.onEnableTotp(secret, token);
|
||||||
// Secret is now stored on the server; remove plaintext copy from localStorage.
|
// Secret is now stored on the server; remove plaintext copy from localStorage.
|
||||||
localStorage.removeItem(totpSecretStorageKey);
|
localStorage.removeItem(totpSecretStorageKey);
|
||||||
setTotpLocked(true);
|
setTotpLocked(true);
|
||||||
|
} catch {
|
||||||
|
// Keep inputs editable after a failed attempt.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadRecoveryCode(): Promise<void> {
|
async function loadRecoveryCode(): Promise<void> {
|
||||||
|
|||||||
Reference in New Issue
Block a user