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:
shuaiplus
2026-03-19 00:38:56 +08:00
parent 8bc43b8f0c
commit facd0ea5f7
26 changed files with 460 additions and 26 deletions
+37 -1
View File
@@ -201,11 +201,12 @@ export async function registerAccount(args: {
email: string;
name: string;
password: string;
masterPasswordHint?: string;
inviteCode?: string;
fallbackIterations: number;
}): Promise<{ ok: true } | { ok: false; message: string }> {
try {
const { email, name, password, inviteCode, fallbackIterations } = args;
const { email, name, password, masterPasswordHint, inviteCode, fallbackIterations } = args;
const masterKey = await pbkdf2(password, email, fallbackIterations, 32);
const masterHash = await pbkdf2(masterKey, password, 1, 32);
const encKey = await hkdfExpand(masterKey, 'enc', 32);
@@ -233,6 +234,7 @@ export async function registerAccount(args: {
body: JSON.stringify({
email: email.toLowerCase(),
name,
masterPasswordHint: String(masterPasswordHint || '').trim() || undefined,
masterPasswordHash: bytesToBase64(masterHash),
key: encryptedVaultKey,
kdf: 0,
@@ -255,6 +257,20 @@ export async function registerAccount(args: {
}
}
export async function getPasswordHint(email: string): Promise<{ masterPasswordHint: string | null }> {
const resp = await fetch('/api/accounts/password-hint', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: email.trim().toLowerCase() }),
});
if (!resp.ok) {
const body = await parseJson<TokenError>(resp);
throw new Error(body?.error_description || body?.error || 'Failed to load password hint');
}
const body = (await parseJson<{ masterPasswordHint?: string | null }>(resp)) || {};
return { masterPasswordHint: body.masterPasswordHint ?? null };
}
export function createAuthedFetch(getSession: () => SessionState | null, setSession: SessionSetter) {
return async function authedFetch(input: string, init: RequestInit = {}): Promise<Response> {
const session = getSession();
@@ -294,6 +310,26 @@ export async function getProfile(authedFetch: AuthedFetch): Promise<Profile> {
return body;
}
export async function updateProfile(
authedFetch: AuthedFetch,
payload: { masterPasswordHint: string }
): Promise<Profile> {
const resp = await authedFetch('/api/accounts/profile', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
masterPasswordHint: String(payload.masterPasswordHint || '').trim() || null,
}),
});
if (!resp.ok) {
const body = await parseJson<TokenError>(resp);
throw new Error(body?.error_description || body?.error || 'Save profile failed');
}
const body = await parseJson<Profile>(resp);
if (!body) throw new Error('Invalid profile');
return body;
}
export async function unlockVaultKey(profileKey: string, masterKey: Uint8Array): Promise<{ symEncKey: string; symMacKey: string }> {
const encKey = await hkdfExpand(masterKey, 'enc', 32);
const macKey = await hkdfExpand(masterKey, 'mac', 32);