diff --git a/packages/server/src/routes/auth.ts b/packages/server/src/routes/auth.ts
index 0dd6f07..f3af704 100644
--- a/packages/server/src/routes/auth.ts
+++ b/packages/server/src/routes/auth.ts
@@ -90,6 +90,33 @@ router.post('/refresh', async (req, res) => {
}
});
+const setPasswordSchema = z.object({
+ password: z.string().min(8),
+});
+
+router.post('/set-password', requireAuth, async (req, res) => {
+ const parsed = setPasswordSchema.safeParse(req.body);
+ if (!parsed.success) {
+ res.status(400).json({ success: false, error: { code: 'VALIDATION', message: parsed.error.issues[0].message } });
+ return;
+ }
+
+ const user = await prisma.user.findUnique({ where: { id: req.user!.userId } });
+ if (!user) {
+ res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: 'User not found' } });
+ return;
+ }
+
+ if (user.passwordHash) {
+ res.status(400).json({ success: false, error: { code: 'ALREADY_HAS_PASSWORD', message: 'Password already set. Use change-password instead.' } });
+ return;
+ }
+
+ const passwordHash = await hashPassword(parsed.data.password);
+ await prisma.user.update({ where: { id: user.id }, data: { passwordHash } });
+ res.json({ success: true, data: { message: 'Password set successfully' } });
+});
+
const changePasswordSchema = z.object({
currentPassword: z.string(),
newPassword: z.string().min(8),
@@ -143,13 +170,14 @@ router.put('/profile', requireAuth, async (req, res) => {
router.get('/me', requireAuth, async (req, res) => {
const user = await prisma.user.findUnique({
where: { id: req.user!.userId },
- select: { id: true, email: true, name: true, avatarUrl: true },
+ select: { id: true, email: true, name: true, avatarUrl: true, passwordHash: true },
});
if (!user) {
res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: 'User not found' } });
return;
}
- res.json({ success: true, data: user });
+ const { passwordHash, ...rest } = user;
+ res.json({ success: true, data: { ...rest, hasPassword: !!passwordHash } });
});
// --- API Key Management ---
diff --git a/packages/web/src/components/SettingsDialog.tsx b/packages/web/src/components/SettingsDialog.tsx
index 64187bd..f5235b1 100644
--- a/packages/web/src/components/SettingsDialog.tsx
+++ b/packages/web/src/components/SettingsDialog.tsx
@@ -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
- {/* Password prompt inline */}
{showPasswordPrompt && (
-
- {t('dashboard.settings.passwordPrompt', {
- action: showPasswordPrompt === 'copy'
- ? t('dashboard.settings.passwordPromptCopy')
- : t('dashboard.settings.passwordPromptReveal'),
- })}
-
-
setVerifyPassword(e.target.value)}
- onKeyDown={(e) => { if (e.key === 'Enter' && verifyPassword) handleVerifyAndAction(); }}
- className="input-base"
- placeholder={t('dashboard.settings.currentPassword')}
- autoFocus
- />
- {verifyError &&
{verifyError}
}
-
-
-
-
+ {hasPassword ? (
+ <>
+
+ {t('dashboard.settings.passwordPrompt', {
+ action: showPasswordPrompt === 'copy'
+ ? t('dashboard.settings.passwordPromptCopy')
+ : t('dashboard.settings.passwordPromptReveal'),
+ })}
+
+
setVerifyPassword(e.target.value)}
+ onKeyDown={(e) => { if (e.key === 'Enter' && verifyPassword) handleVerifyAndAction(); }}
+ className="input-base"
+ placeholder={t('dashboard.settings.currentPassword')}
+ autoFocus
+ />
+ {verifyError &&
{verifyError}
}
+
+
+
+
+ >
+ ) : (
+ <>
+
{t('dashboard.settings.setPasswordToReveal')}
+
+
+
+
+ >
+ )}
)}
@@ -358,38 +402,72 @@ export default function SettingsDialog({ open, onClose }: { open: boolean; onClo
{/* Password */}
-
- {t('dashboard.settings.changePasswordTitle')}
- {t('dashboard.settings.changePasswordDesc')}
-
-
-
- setCurrentPassword(e.target.value)} className="input-base" placeholder={t('dashboard.settings.enterCurrentPassword')} />
-
-
-
- setNewPassword(e.target.value)} className="input-base" placeholder={t('dashboard.settings.atLeast8Chars')} minLength={8} />
-
-
-
- setConfirmPassword(e.target.value)} className="input-base" placeholder={t('dashboard.settings.confirmNewPassword')} />
-
- {passwordMsg && (
-
-
- {passwordMsg.text}
+
+ {hasPassword ? (
+ <>
+ {t('dashboard.settings.changePasswordTitle')}
+ {t('dashboard.settings.changePasswordDesc')}
+
+
+
+ setCurrentPassword(e.target.value)} className="input-base" placeholder={t('dashboard.settings.enterCurrentPassword')} />
+
+
+
+ setNewPassword(e.target.value)} className="input-base" placeholder={t('dashboard.settings.atLeast8Chars')} minLength={8} />
+
+
+
+ setConfirmPassword(e.target.value)} className="input-base" placeholder={t('dashboard.settings.confirmNewPassword')} />
+
+ {passwordMsg && (
+
+
+ {passwordMsg.text}
+
+ )}
+
- )}
-
-
+ >
+ ) : (
+ <>
+
{t('dashboard.settings.setPasswordTitle')}
+
{t('dashboard.settings.setPasswordDesc')}
+
+
+
+ setNewPassword(e.target.value)} className="input-base" placeholder={t('dashboard.settings.atLeast8Chars')} minLength={8} />
+
+
+
+ setConfirmPassword(e.target.value)} className="input-base" placeholder={t('dashboard.settings.confirmNewPassword')} />
+
+ {passwordMsg && (
+
+
+ {passwordMsg.text}
+
+ )}
+
+
+ >
+ )}
diff --git a/packages/web/src/lib/auth.tsx b/packages/web/src/lib/auth.tsx
index 47ae7f7..e06649b 100644
--- a/packages/web/src/lib/auth.tsx
+++ b/packages/web/src/lib/auth.tsx
@@ -1,7 +1,7 @@
import { createContext, useContext, useState, useEffect, type ReactNode } from 'react';
import { getAccessToken, clearTokens, setTokens, apiFetch } from './api';
-type User = { id: string; email: string; name: string };
+type User = { id: string; email: string; name: string; hasPassword?: boolean };
type AuthContextType = {
user: User | null;
diff --git a/packages/web/src/lib/i18n/en.ts b/packages/web/src/lib/i18n/en.ts
index 3949da1..8443204 100644
--- a/packages/web/src/lib/i18n/en.ts
+++ b/packages/web/src/lib/i18n/en.ts
@@ -375,6 +375,13 @@ const en = {
'dashboard.settings.enterCurrentPassword': 'Enter current password',
'dashboard.settings.atLeast8Chars': 'At least 8 characters',
'dashboard.settings.confirmNewPassword': 'Confirm new password',
+ 'dashboard.settings.setPasswordTitle': 'Set Password',
+ 'dashboard.settings.setPasswordDesc': 'You signed in with a third-party account. Set a password to reveal or copy your API key.',
+ 'dashboard.settings.setPassword': 'Set Password',
+ 'dashboard.settings.settingPassword': 'Setting...',
+ 'dashboard.settings.passwordSet': 'Password set successfully',
+ 'dashboard.settings.setPasswordToReveal': 'Set a password first to reveal your API key.',
+ 'dashboard.settings.setPasswordAction': 'Set Password',
};
export default en;
diff --git a/packages/web/src/lib/i18n/zh.ts b/packages/web/src/lib/i18n/zh.ts
index 9cabad0..4cb447d 100644
--- a/packages/web/src/lib/i18n/zh.ts
+++ b/packages/web/src/lib/i18n/zh.ts
@@ -377,6 +377,13 @@ const zh: Record = {
'dashboard.settings.enterCurrentPassword': '输入当前密码',
'dashboard.settings.atLeast8Chars': '至少 8 个字符',
'dashboard.settings.confirmNewPassword': '确认新密码',
+ 'dashboard.settings.setPasswordTitle': '设置密码',
+ 'dashboard.settings.setPasswordDesc': '您通过第三方账号登录,设置密码后可以查看或复制 API Key。',
+ 'dashboard.settings.setPassword': '设置密码',
+ 'dashboard.settings.settingPassword': '设置中...',
+ 'dashboard.settings.passwordSet': '密码设置成功',
+ 'dashboard.settings.setPasswordToReveal': '请先设置密码才能查看 API Key。',
+ 'dashboard.settings.setPasswordAction': '设置密码',
};
export default zh;