|
|
|
|
@@ -108,6 +108,29 @@ export default function SettingsDialog({ open, onClose }: { open: boolean; onClo
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleSetPassword = async () => {
|
|
|
|
|
if (newPassword !== confirmPassword) {
|
|
|
|
|
setPasswordMsg({ type: 'error', text: t('dashboard.settings.passwordMismatch') });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
setPasswordLoading(true);
|
|
|
|
|
setPasswordMsg(null);
|
|
|
|
|
try {
|
|
|
|
|
await apiFetch('/auth/set-password', {
|
|
|
|
|
method: 'POST', body: JSON.stringify({ password: newPassword }),
|
|
|
|
|
});
|
|
|
|
|
setPasswordMsg({ type: 'success', text: t('dashboard.settings.passwordSet') });
|
|
|
|
|
updateUser({ hasPassword: true });
|
|
|
|
|
setNewPassword('');
|
|
|
|
|
setConfirmPassword('');
|
|
|
|
|
setTimeout(() => setPasswordMsg(null), 3000);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setPasswordMsg({ type: 'error', text: err instanceof Error ? err.message : 'Failed to set password' });
|
|
|
|
|
} finally {
|
|
|
|
|
setPasswordLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// API Key handlers
|
|
|
|
|
const handleGenerateKey = async () => {
|
|
|
|
|
setKeyLoading(true);
|
|
|
|
|
@@ -139,6 +162,8 @@ export default function SettingsDialog({ open, onClose }: { open: boolean; onClo
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const hasPassword = user?.hasPassword !== false;
|
|
|
|
|
|
|
|
|
|
const handleVerifyAndAction = async () => {
|
|
|
|
|
setVerifyLoading(true);
|
|
|
|
|
setVerifyError('');
|
|
|
|
|
@@ -307,32 +332,51 @@ export default function SettingsDialog({ open, onClose }: { open: boolean; onClo
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Password prompt inline */}
|
|
|
|
|
{showPasswordPrompt && (
|
|
|
|
|
<div className="p-3 rounded-lg border border-border-default bg-bg-primary space-y-2 animate-fade-in">
|
|
|
|
|
<p className="text-[13px] text-text-secondary">
|
|
|
|
|
{t('dashboard.settings.passwordPrompt', {
|
|
|
|
|
action: showPasswordPrompt === 'copy'
|
|
|
|
|
? t('dashboard.settings.passwordPromptCopy')
|
|
|
|
|
: t('dashboard.settings.passwordPromptReveal'),
|
|
|
|
|
})}
|
|
|
|
|
</p>
|
|
|
|
|
<input
|
|
|
|
|
type="password"
|
|
|
|
|
value={verifyPassword}
|
|
|
|
|
onChange={(e) => setVerifyPassword(e.target.value)}
|
|
|
|
|
onKeyDown={(e) => { if (e.key === 'Enter' && verifyPassword) handleVerifyAndAction(); }}
|
|
|
|
|
className="input-base"
|
|
|
|
|
placeholder={t('dashboard.settings.currentPassword')}
|
|
|
|
|
autoFocus
|
|
|
|
|
/>
|
|
|
|
|
{verifyError && <p className="text-[12px] text-danger">{verifyError}</p>}
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<button onClick={handleVerifyAndAction} disabled={verifyLoading || !verifyPassword} className="btn-primary text-[13px] py-1.5">
|
|
|
|
|
{verifyLoading ? t('dashboard.settings.verifying') : t('common.confirm')}
|
|
|
|
|
</button>
|
|
|
|
|
<button onClick={() => setShowPasswordPrompt(null)} className="btn-ghost text-[13px] py-1.5">{t('common.cancel')}</button>
|
|
|
|
|
</div>
|
|
|
|
|
{hasPassword ? (
|
|
|
|
|
<>
|
|
|
|
|
<p className="text-[13px] text-text-secondary">
|
|
|
|
|
{t('dashboard.settings.passwordPrompt', {
|
|
|
|
|
action: showPasswordPrompt === 'copy'
|
|
|
|
|
? t('dashboard.settings.passwordPromptCopy')
|
|
|
|
|
: t('dashboard.settings.passwordPromptReveal'),
|
|
|
|
|
})}
|
|
|
|
|
</p>
|
|
|
|
|
<input
|
|
|
|
|
type="password"
|
|
|
|
|
value={verifyPassword}
|
|
|
|
|
onChange={(e) => setVerifyPassword(e.target.value)}
|
|
|
|
|
onKeyDown={(e) => { if (e.key === 'Enter' && verifyPassword) handleVerifyAndAction(); }}
|
|
|
|
|
className="input-base"
|
|
|
|
|
placeholder={t('dashboard.settings.currentPassword')}
|
|
|
|
|
autoFocus
|
|
|
|
|
/>
|
|
|
|
|
{verifyError && <p className="text-[12px] text-danger">{verifyError}</p>}
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<button onClick={handleVerifyAndAction} disabled={verifyLoading || !verifyPassword} className="btn-primary text-[13px] py-1.5">
|
|
|
|
|
{verifyLoading ? t('dashboard.settings.verifying') : t('common.confirm')}
|
|
|
|
|
</button>
|
|
|
|
|
<button onClick={() => setShowPasswordPrompt(null)} className="btn-ghost text-[13px] py-1.5">{t('common.cancel')}</button>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<p className="text-[13px] text-text-secondary">{t('dashboard.settings.setPasswordToReveal')}</p>
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setShowPasswordPrompt(null);
|
|
|
|
|
document.getElementById('set-password-section')?.scrollIntoView({ behavior: 'smooth' });
|
|
|
|
|
}}
|
|
|
|
|
className="btn-primary text-[13px] py-1.5"
|
|
|
|
|
>
|
|
|
|
|
{t('dashboard.settings.setPasswordAction')}
|
|
|
|
|
</button>
|
|
|
|
|
<button onClick={() => setShowPasswordPrompt(null)} className="btn-ghost text-[13px] py-1.5">{t('common.cancel')}</button>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
@@ -358,38 +402,72 @@ export default function SettingsDialog({ open, onClose }: { open: boolean; onClo
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
{/* Password */}
|
|
|
|
|
<section className="border-t border-border-default pt-5">
|
|
|
|
|
<p className="section-title">{t('dashboard.settings.changePasswordTitle')}</p>
|
|
|
|
|
<p className="section-desc mb-4">{t('dashboard.settings.changePasswordDesc')}</p>
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-[13px] text-text-secondary mb-1.5">{t('dashboard.settings.currentPasswordLabel')}</label>
|
|
|
|
|
<input type="password" value={currentPassword} onChange={(e) => setCurrentPassword(e.target.value)} className="input-base" placeholder={t('dashboard.settings.enterCurrentPassword')} />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-[13px] text-text-secondary mb-1.5">{t('dashboard.settings.newPasswordLabel')}</label>
|
|
|
|
|
<input type="password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} className="input-base" placeholder={t('dashboard.settings.atLeast8Chars')} minLength={8} />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-[13px] text-text-secondary mb-1.5">{t('dashboard.settings.confirmPasswordLabel')}</label>
|
|
|
|
|
<input type="password" value={confirmPassword} onChange={(e) => setConfirmPassword(e.target.value)} className="input-base" placeholder={t('dashboard.settings.confirmNewPassword')} />
|
|
|
|
|
</div>
|
|
|
|
|
{passwordMsg && (
|
|
|
|
|
<div className={`p-3 rounded-lg text-[13px] flex items-center gap-2 ${passwordMsg.type === 'success' ? 'bg-success-muted text-success' : 'bg-danger-muted text-danger'}`}>
|
|
|
|
|
<svg className="w-4 h-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
|
|
|
{passwordMsg.type === 'success' ? <path d="M5 13l4 4L19 7" /> : <path d="M6 18L18 6M6 6l12 12" />}
|
|
|
|
|
</svg>
|
|
|
|
|
{passwordMsg.text}
|
|
|
|
|
<section id="set-password-section" className="border-t border-border-default pt-5">
|
|
|
|
|
{hasPassword ? (
|
|
|
|
|
<>
|
|
|
|
|
<p className="section-title">{t('dashboard.settings.changePasswordTitle')}</p>
|
|
|
|
|
<p className="section-desc mb-4">{t('dashboard.settings.changePasswordDesc')}</p>
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-[13px] text-text-secondary mb-1.5">{t('dashboard.settings.currentPasswordLabel')}</label>
|
|
|
|
|
<input type="password" value={currentPassword} onChange={(e) => setCurrentPassword(e.target.value)} className="input-base" placeholder={t('dashboard.settings.enterCurrentPassword')} />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-[13px] text-text-secondary mb-1.5">{t('dashboard.settings.newPasswordLabel')}</label>
|
|
|
|
|
<input type="password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} className="input-base" placeholder={t('dashboard.settings.atLeast8Chars')} minLength={8} />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-[13px] text-text-secondary mb-1.5">{t('dashboard.settings.confirmPasswordLabel')}</label>
|
|
|
|
|
<input type="password" value={confirmPassword} onChange={(e) => setConfirmPassword(e.target.value)} className="input-base" placeholder={t('dashboard.settings.confirmNewPassword')} />
|
|
|
|
|
</div>
|
|
|
|
|
{passwordMsg && (
|
|
|
|
|
<div className={`p-3 rounded-lg text-[13px] flex items-center gap-2 ${passwordMsg.type === 'success' ? 'bg-success-muted text-success' : 'bg-danger-muted text-danger'}`}>
|
|
|
|
|
<svg className="w-4 h-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
|
|
|
{passwordMsg.type === 'success' ? <path d="M5 13l4 4L19 7" /> : <path d="M6 18L18 6M6 6l12 12" />}
|
|
|
|
|
</svg>
|
|
|
|
|
{passwordMsg.text}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<button
|
|
|
|
|
onClick={handlePasswordChange}
|
|
|
|
|
disabled={passwordLoading || !currentPassword || !newPassword || newPassword.length < 8}
|
|
|
|
|
className="btn-primary"
|
|
|
|
|
>
|
|
|
|
|
{passwordLoading ? t('dashboard.settings.changingPassword') : t('dashboard.settings.changePassword')}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<button
|
|
|
|
|
onClick={handlePasswordChange}
|
|
|
|
|
disabled={passwordLoading || !currentPassword || !newPassword || newPassword.length < 8}
|
|
|
|
|
className="btn-primary"
|
|
|
|
|
>
|
|
|
|
|
{passwordLoading ? t('dashboard.settings.changingPassword') : t('dashboard.settings.changePassword')}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<p className="section-title">{t('dashboard.settings.setPasswordTitle')}</p>
|
|
|
|
|
<p className="section-desc mb-4">{t('dashboard.settings.setPasswordDesc')}</p>
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-[13px] text-text-secondary mb-1.5">{t('dashboard.settings.newPasswordLabel')}</label>
|
|
|
|
|
<input type="password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} className="input-base" placeholder={t('dashboard.settings.atLeast8Chars')} minLength={8} />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-[13px] text-text-secondary mb-1.5">{t('dashboard.settings.confirmPasswordLabel')}</label>
|
|
|
|
|
<input type="password" value={confirmPassword} onChange={(e) => setConfirmPassword(e.target.value)} className="input-base" placeholder={t('dashboard.settings.confirmNewPassword')} />
|
|
|
|
|
</div>
|
|
|
|
|
{passwordMsg && (
|
|
|
|
|
<div className={`p-3 rounded-lg text-[13px] flex items-center gap-2 ${passwordMsg.type === 'success' ? 'bg-success-muted text-success' : 'bg-danger-muted text-danger'}`}>
|
|
|
|
|
<svg className="w-4 h-4 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
|
|
|
{passwordMsg.type === 'success' ? <path d="M5 13l4 4L19 7" /> : <path d="M6 18L18 6M6 6l12 12" />}
|
|
|
|
|
</svg>
|
|
|
|
|
{passwordMsg.text}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleSetPassword}
|
|
|
|
|
disabled={passwordLoading || !newPassword || newPassword.length < 8 || newPassword !== confirmPassword}
|
|
|
|
|
className="btn-primary"
|
|
|
|
|
>
|
|
|
|
|
{passwordLoading ? t('dashboard.settings.settingPassword') : t('dashboard.settings.setPassword')}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</section>
|
|
|
|
|
</div>
|
|
|
|
|
</dialog>
|
|
|
|
|
|