mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-21 05:10:41 +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:
@@ -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);
|
||||
|
||||
@@ -149,8 +149,7 @@ export function readInitialAppBootstrapState(): InitialAppBootstrapState {
|
||||
};
|
||||
}
|
||||
|
||||
export async function bootstrapAppSession(): Promise<BootstrapAppResult> {
|
||||
const initial = readInitialAppBootstrapState();
|
||||
export async function bootstrapAppSession(initial: InitialAppBootstrapState = readInitialAppBootstrapState()): Promise<BootstrapAppResult> {
|
||||
const defaultKdfIterations = initial.defaultKdfIterations;
|
||||
const jwtWarning = initial.jwtWarning;
|
||||
|
||||
@@ -309,6 +308,7 @@ export async function performRegistration(args: {
|
||||
email: string;
|
||||
name: string;
|
||||
password: string;
|
||||
masterPasswordHint: string;
|
||||
inviteCode: string;
|
||||
fallbackIterations: number;
|
||||
}) {
|
||||
@@ -316,6 +316,7 @@ export async function performRegistration(args: {
|
||||
email: args.email.trim().toLowerCase(),
|
||||
name: args.name.trim(),
|
||||
password: args.password,
|
||||
masterPasswordHint: args.masterPasswordHint.trim(),
|
||||
inviteCode: args.inviteCode.trim(),
|
||||
fallbackIterations: args.fallbackIterations,
|
||||
});
|
||||
|
||||
@@ -458,6 +458,19 @@ const messages: Record<Locale, Record<string, string>> = {
|
||||
txt_password: "Password",
|
||||
txt_password_is_already_verified: "Password is already verified.",
|
||||
txt_passwords_do_not_match: "Passwords do not match",
|
||||
txt_password_hint: "Password Hint",
|
||||
txt_password_hint_optional: "Password Hint (optional)",
|
||||
txt_password_hint_placeholder: "A clue only you would understand",
|
||||
txt_password_hint_register_placeholder: "This hint can be shown directly on the web login page.",
|
||||
txt_password_hint_register_help: "This hint can be shown directly on the web login page. Do not include your master password, recovery code, or anything that can reveal it outright.",
|
||||
txt_password_hint_login_help: "Forgot the master password? Reveal the hint you saved during registration.",
|
||||
txt_password_hint_login_note: "Only a hint is shown here. It should help you remember the password, not expose it.",
|
||||
txt_show_password_hint: "Show Password Hint",
|
||||
txt_hide_password_hint: "Hide Password Hint",
|
||||
txt_loading_password_hint: "Loading hint...",
|
||||
txt_password_hint_not_set: "No password hint is available for this email.",
|
||||
txt_password_hint_load_failed: "Failed to load password hint",
|
||||
txt_password_hint_too_long: "Password hint must be 120 characters or fewer",
|
||||
txt_phone: "Phone",
|
||||
txt_please_input_email_and_password: "Please input email and password",
|
||||
txt_please_input_master_password: "Please input master password",
|
||||
@@ -1095,6 +1108,19 @@ const zhCNOverrides: Record<string, string> = {
|
||||
txt_opera_extension: 'Opera 扩展',
|
||||
txt_password_is_already_verified: '密码已验证',
|
||||
txt_passwords_do_not_match: '两次输入的密码不一致',
|
||||
txt_password_hint: '密码提示',
|
||||
txt_password_hint_optional: '密码提示(可选)',
|
||||
txt_password_hint_placeholder: '写一句只有你自己看得懂的线索',
|
||||
txt_password_hint_register_placeholder: '这个提示可以在网页登录页直接显示。',
|
||||
txt_password_hint_register_help: '这个提示可以在网页登录页直接显示。不要填写主密码、恢复代码,或任何能直接暴露密码的信息。',
|
||||
txt_password_hint_login_help: '忘记主密码时,可以查看注册时保存的提示。',
|
||||
txt_password_hint_login_note: '这里只会显示提示语,不会显示你的主密码本身。',
|
||||
txt_show_password_hint: '查看密码提示',
|
||||
txt_hide_password_hint: '隐藏密码提示',
|
||||
txt_loading_password_hint: '正在加载提示...',
|
||||
txt_password_hint_not_set: '这个邮箱没有可显示的密码提示。',
|
||||
txt_password_hint_load_failed: '加载密码提示失败',
|
||||
txt_password_hint_too_long: '密码提示最多只能输入 120 个字符',
|
||||
txt_phone: '电话',
|
||||
txt_please_input_email_and_password: '请输入邮箱和密码',
|
||||
txt_please_input_master_password: '请输入主密码',
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface Profile {
|
||||
email: string;
|
||||
name: string;
|
||||
key: string;
|
||||
masterPasswordHint?: string | null;
|
||||
privateKey?: string | null;
|
||||
publicKey?: string | null;
|
||||
role: 'admin' | 'user';
|
||||
|
||||
Reference in New Issue
Block a user