docs: update README files for clarity on deployment steps and features

This commit is contained in:
shuaiplus
2026-03-01 19:31:03 +08:00
committed by Shuai
parent 9061ab52b6
commit 68f66cf4e6
13 changed files with 164 additions and 100 deletions
+4 -13
View File
@@ -1,7 +1,7 @@
import { useEffect, useMemo, useState } from 'preact/hooks';
import { Link, Route, Switch, useLocation } from 'wouter';
import { useQuery } from '@tanstack/react-query';
import { CircleHelp, LogOut, Send as SendIcon, Settings as SettingsIcon, Shield, ShieldUser, Vault } from 'lucide-preact';
import { CircleHelp, Lock, LogOut, Send as SendIcon, Settings as SettingsIcon, Shield, ShieldUser, Vault } from 'lucide-preact';
import AuthViews from '@/components/AuthViews';
import ConfirmDialog from '@/components/ConfirmDialog';
import ToastHost from '@/components/ToastHost';
@@ -53,7 +53,6 @@ import {
updateSend,
buildSendShareKey,
unlockVaultKey,
updateProfile,
verifyMasterPassword,
} from '@/lib/api';
import { base64ToBytes, decryptBw, decryptStr, hkdf } from '@/lib/crypto';
@@ -580,16 +579,6 @@ export default function App() {
};
}, [session?.symEncKey, session?.symMacKey, foldersQuery.data, ciphersQuery.data, sendsQuery.data]);
async function saveProfileAction(name: string, email: string) {
try {
const updated = await updateProfile(authedFetch, { name: name.trim(), email: email.trim().toLowerCase() });
setProfile(updated);
pushToast('success', t('txt_profile_updated'));
} catch (error) {
pushToast('error', error instanceof Error ? error.message : t('txt_save_profile_failed'));
}
}
async function changePasswordAction(currentPassword: string, nextPassword: string, nextPassword2: string) {
if (!profile) return;
if (!currentPassword || !nextPassword) {
@@ -955,6 +944,9 @@ export default function App() {
<ShieldUser size={16} />
<span>{profile?.email}</span>
</div>
<button type="button" className="btn btn-secondary small" onClick={handleLock}>
<Lock size={14} className="btn-icon" /> {t('txt_lock')}
</button>
<button type="button" className="btn btn-secondary small" onClick={handleLogout}>
<LogOut size={14} className="btn-icon" /> {t('txt_sign_out')}
</button>
@@ -1026,7 +1018,6 @@ export default function App() {
<SettingsPage
profile={profile}
totpEnabled={!!totpStatusQuery.data?.enabled}
onSaveProfile={saveProfileAction}
onChangePassword={changePasswordAction}
onEnableTotp={async (secret, token) => {
await enableTotpAction(secret, token);
+9 -32
View File
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from 'preact/hooks';
import { Clipboard, KeyRound, RefreshCw, Save, ShieldCheck, ShieldOff } from 'lucide-preact';
import { Clipboard, KeyRound, RefreshCw, ShieldCheck, ShieldOff } from 'lucide-preact';
import qrcode from 'qrcode-generator';
import type { Profile } from '@/lib/types';
import { t } from '@/lib/i18n';
@@ -7,7 +7,6 @@ import { t } from '@/lib/i18n';
interface SettingsPageProps {
profile: Profile;
totpEnabled: boolean;
onSaveProfile: (name: string, email: string) => Promise<void>;
onChangePassword: (currentPassword: string, nextPassword: string, nextPassword2: string) => Promise<void>;
onEnableTotp: (secret: string, token: string) => Promise<void>;
onOpenDisableTotp: () => void;
@@ -30,8 +29,6 @@ function buildOtpUri(email: string, secret: string): string {
export default function SettingsPage(props: SettingsPageProps) {
const totpSecretStorageKey = `nodewarden.totp.secret.${props.profile.id}`;
const [name, setName] = useState(props.profile.name || '');
const [email, setEmail] = useState(props.profile.email || '');
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [newPassword2, setNewPassword2] = useState('');
@@ -49,12 +46,13 @@ export default function SettingsPage(props: SettingsPageProps) {
setTotpLocked(true);
}, [props.totpEnabled]);
const qrSvg = useMemo(() => {
const qrDataUrl = useMemo(() => {
const qr = qrcode(0, 'M');
qr.addData(buildOtpUri(email || props.profile.email, secret));
qr.addData(buildOtpUri(props.profile.email, secret));
qr.make();
return qr.createSvgTag({ scalable: true, margin: 0 });
}, [email, props.profile.email, secret]);
const svg = qr.createSvgTag({ scalable: true, margin: 0 });
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
}, [props.profile.email, secret]);
async function enableTotp(): Promise<void> {
await props.onEnableTotp(secret, token);
@@ -70,29 +68,6 @@ export default function SettingsPage(props: SettingsPageProps) {
return (
<div className="stack">
<section className="card">
<h3>{t('txt_profile')}</h3>
<div className="field-grid">
<label className="field">
<span>{t('txt_name')}</span>
<input className="input" value={name} onInput={(e) => setName((e.currentTarget as HTMLInputElement).value)} />
</label>
<label className="field">
<span>{t('txt_email')}</span>
<input
className="input"
type="email"
value={email}
onInput={(e) => setEmail((e.currentTarget as HTMLInputElement).value)}
/>
</label>
</div>
<button type="button" className="btn btn-primary" onClick={() => void props.onSaveProfile(name, email)}>
<Save size={14} className="btn-icon" />
{t('txt_save_profile')}
</button>
</section>
<section className="card">
<h3>{t('txt_change_master_password')}</h3>
<label className="field">
@@ -130,7 +105,9 @@ export default function SettingsPage(props: SettingsPageProps) {
<h3>{t('txt_totp')}</h3>
{totpLocked && <div className="status-ok">{t('txt_totp_is_enabled_for_this_account')}</div>}
<div className="totp-grid">
<div className="totp-qr" dangerouslySetInnerHTML={{ __html: qrSvg }} />
<div className="totp-qr">
<img src={qrDataUrl} alt="TOTP QR" />
</div>
<div>
<div>
<label className="field">
+5 -18
View File
@@ -30,7 +30,11 @@ export function loadSession(): SessionState | null {
if (!raw) return null;
const parsed = JSON.parse(raw) as SessionState;
if (!parsed.accessToken || !parsed.refreshToken) return null;
return parsed;
return {
accessToken: parsed.accessToken,
refreshToken: parsed.refreshToken,
email: parsed.email,
};
} catch {
return null;
}
@@ -45,8 +49,6 @@ export function saveSession(session: SessionState | null): void {
accessToken: session.accessToken,
refreshToken: session.refreshToken,
email: session.email,
symEncKey: session.symEncKey,
symMacKey: session.symMacKey,
};
localStorage.setItem(SESSION_KEY, JSON.stringify(persisted));
}
@@ -328,21 +330,6 @@ export async function getSends(authedFetch: (input: string, init?: RequestInit)
return body?.data || [];
}
export async function updateProfile(
authedFetch: (input: string, init?: RequestInit) => Promise<Response>,
payload: { name: string; email: string }
): Promise<Profile> {
const resp = await authedFetch('/api/accounts/profile', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!resp.ok) throw new Error('Save profile failed');
const body = await parseJson<Profile>(resp);
if (!body) throw new Error('Invalid profile');
return body;
}
export async function changeMasterPassword(
authedFetch: (input: string, init?: RequestInit) => Promise<Response>,
args: {
+5 -2
View File
@@ -196,6 +196,7 @@ const messages: Record<Locale, Record<string, string>> = {
txt_copied: "Copied",
txt_log_in: "Log In",
txt_log_out: "Log Out",
txt_lock: "Lock",
txt_login: "Login",
txt_login_credentials: "Login Credentials",
txt_login_failed: "Login failed",
@@ -368,7 +369,7 @@ const zhCNOverrides: Record<string, string> = {
nav_device_management: '设备管理',
nav_support_center: '支持中心',
support_title: '支持中心',
support_under_construction: '正在搭建中',
support_under_construction: '正在搭建中',
txt_sign_out: '退出登录',
txt_log_in: '登录',
txt_log_out: '退出',
@@ -392,7 +393,7 @@ const zhCNOverrides: Record<string, string> = {
txt_search_your_secure_vault: '搜索你的保险库...',
txt_refresh: '刷新',
txt_sync: '同步',
txt_sync_vault: '同步保险库',
txt_sync_vault: '同步',
txt_add: '新增',
txt_edit: '编辑',
txt_delete: '删除',
@@ -720,6 +721,8 @@ const zhCNOverrides: Record<string, string> = {
txt_copied: '已复制',
};
zhCNOverrides.txt_lock = '\u9501\u5b9a';
messages['zh-CN'] = { ...messages.en, ...zhCNOverrides };
function resolveInitialLocale(): Locale {
+5
View File
@@ -965,6 +965,11 @@ input[type='file'].input::file-selector-button:hover {
height: 180px;
}
.totp-qr img {
width: 180px;
height: 180px;
}
.section-head {
display: flex;
align-items: center;