mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 13:00:39 +00:00
feat: add master password hint functionality
- Updated user model to include masterPasswordHint. - Modified sync handler to return masterPasswordHint. - Implemented password hint retrieval in public API. - Enhanced user profile management to allow updating of password hint. - Added UI components for displaying and editing password hint. - Updated localization files for new password hint strings. - Improved rate limiting for sensitive public requests. - Adjusted database schema to accommodate master password hint.
This commit is contained in:
+80
-4
@@ -11,6 +11,7 @@ import {
|
||||
createAuthedFetch,
|
||||
getAuthorizedDevices,
|
||||
getCurrentDeviceIdentifier,
|
||||
getPasswordHint,
|
||||
getTotpStatus,
|
||||
saveSession,
|
||||
} from '@/lib/api/auth';
|
||||
@@ -78,8 +79,18 @@ export default function App() {
|
||||
email: '',
|
||||
password: '',
|
||||
password2: '',
|
||||
passwordHint: '',
|
||||
inviteCode: initialInviteCode,
|
||||
});
|
||||
const [loginHintState, setLoginHintState] = useState<{
|
||||
email: string;
|
||||
loading: boolean;
|
||||
hint: string | null;
|
||||
}>({
|
||||
email: '',
|
||||
loading: false,
|
||||
hint: null,
|
||||
});
|
||||
const [inviteCodeFromUrl, setInviteCodeFromUrl] = useState(initialInviteCode);
|
||||
const [unlockPassword, setUnlockPassword] = useState('');
|
||||
const [pendingTotp, setPendingTotp] = useState<PendingTotp | null>(null);
|
||||
@@ -131,6 +142,15 @@ export default function App() {
|
||||
setRegisterValues((prev) => (prev.inviteCode === inviteCodeFromUrl ? prev : { ...prev, inviteCode: inviteCodeFromUrl }));
|
||||
}, [inviteCodeFromUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
const normalizedEmail = loginValues.email.trim().toLowerCase();
|
||||
setLoginHintState((prev) => (
|
||||
prev.email && prev.email !== normalizedEmail
|
||||
? { email: '', loading: false, hint: null }
|
||||
: prev
|
||||
));
|
||||
}, [loginValues.email]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!inviteCodeFromUrl) return;
|
||||
if (phase === 'locked' || phase === 'app') return;
|
||||
@@ -200,7 +220,7 @@ export default function App() {
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
(async () => {
|
||||
const boot = await bootstrapAppSession();
|
||||
const boot = await bootstrapAppSession(initialBootstrap);
|
||||
if (!mounted) return;
|
||||
setDefaultKdfIterations(boot.defaultKdfIterations);
|
||||
setJwtWarning(boot.jwtWarning);
|
||||
@@ -212,7 +232,7 @@ export default function App() {
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, []);
|
||||
}, [initialBootstrap]);
|
||||
|
||||
async function finalizeLogin(login: CompletedLogin) {
|
||||
setSession(login.session);
|
||||
@@ -322,6 +342,7 @@ export default function App() {
|
||||
email: registerValues.email,
|
||||
name: registerValues.name,
|
||||
password: registerValues.password,
|
||||
masterPasswordHint: registerValues.passwordHint,
|
||||
inviteCode: registerValues.inviteCode,
|
||||
fallbackIterations: defaultKdfIterations,
|
||||
});
|
||||
@@ -338,6 +359,56 @@ export default function App() {
|
||||
}
|
||||
}
|
||||
|
||||
function openPasswordHintDialog(hint: string | null) {
|
||||
setConfirm({
|
||||
title: t('txt_password_hint'),
|
||||
message: hint || t('txt_password_hint_not_set'),
|
||||
showIcon: false,
|
||||
confirmText: t('txt_close'),
|
||||
hideCancel: true,
|
||||
onConfirm: () => setConfirm(null),
|
||||
});
|
||||
}
|
||||
|
||||
async function handleTogglePasswordHint() {
|
||||
if (pendingAuthAction) return;
|
||||
const email = loginValues.email.trim().toLowerCase();
|
||||
if (!email) return;
|
||||
|
||||
if (loginHintState.email === email && !loginHintState.loading) {
|
||||
openPasswordHintDialog(loginHintState.hint);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoginHintState({
|
||||
email,
|
||||
loading: true,
|
||||
hint: null,
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await getPasswordHint(email);
|
||||
openPasswordHintDialog(result.masterPasswordHint);
|
||||
setLoginHintState({
|
||||
email,
|
||||
loading: false,
|
||||
hint: result.masterPasswordHint,
|
||||
});
|
||||
} catch (error) {
|
||||
setLoginHintState({
|
||||
email: '',
|
||||
loading: false,
|
||||
hint: null,
|
||||
});
|
||||
pushToast('error', error instanceof Error ? error.message : t('txt_password_hint_load_failed'));
|
||||
}
|
||||
}
|
||||
|
||||
function handleShowLockedPasswordHint() {
|
||||
if (pendingAuthAction) return;
|
||||
openPasswordHintDialog(profile?.masterPasswordHint ?? null);
|
||||
}
|
||||
|
||||
async function handleUnlock() {
|
||||
if (pendingAuthAction) return;
|
||||
if (!session || !profile) return;
|
||||
@@ -804,6 +875,7 @@ export default function App() {
|
||||
},
|
||||
onLogoutNow: logoutNow,
|
||||
onNotify: pushToast,
|
||||
onProfileUpdated: setProfile,
|
||||
onSetConfirm: setConfirm,
|
||||
refetchTotpStatus: totpStatusQuery.refetch,
|
||||
refetchAuthorizedDevices: authorizedDevicesQuery.refetch,
|
||||
@@ -923,6 +995,7 @@ export default function App() {
|
||||
uploadingSendFileName: vaultSendActions.uploadingSendFileName,
|
||||
sendUploadPercent: vaultSendActions.sendUploadPercent,
|
||||
onChangePassword: accountSecurityActions.changePassword,
|
||||
onSavePasswordHint: accountSecurityActions.savePasswordHint,
|
||||
onEnableTotp: async (secret: string, token: string) => {
|
||||
await accountSecurityActions.enableTotp(secret, token);
|
||||
await totpStatusQuery.refetch();
|
||||
@@ -992,6 +1065,7 @@ export default function App() {
|
||||
registerValues={registerValues}
|
||||
unlockPassword={unlockPassword}
|
||||
emailForLock={profile?.email || session?.email || ''}
|
||||
loginHintLoading={loginHintState.loading}
|
||||
onChangeLogin={setLoginValues}
|
||||
onChangeRegister={setRegisterValues}
|
||||
onChangeUnlock={setUnlockPassword}
|
||||
@@ -1010,12 +1084,14 @@ export default function App() {
|
||||
navigate('/register');
|
||||
}}
|
||||
onLogout={logoutNow}
|
||||
onTogglePasswordHint={() => void handleTogglePasswordHint()}
|
||||
onShowLockedPasswordHint={handleShowLockedPasswordHint}
|
||||
/>
|
||||
<AppGlobalOverlays
|
||||
toasts={toasts}
|
||||
onCloseToast={removeToast}
|
||||
confirm={null}
|
||||
onCancelConfirm={() => {}}
|
||||
confirm={confirm}
|
||||
onCancelConfirm={() => setConfirm(null)}
|
||||
pendingTotpOpen={!!pendingTotp}
|
||||
totpCode={totpCode}
|
||||
rememberDevice={rememberDevice}
|
||||
|
||||
Reference in New Issue
Block a user