Files
deer-flow/frontend/src/components/workspace/settings/account-settings-page.tsx
T
greatmengqi 2531cce0d1 feat(auth): account settings page + i18n
- Port account-settings-page.tsx (change password, change email, logout)
- Wire into settings-dialog.tsx as new "account" section with UserIcon,
  rendered first in the section list
- Add i18n keys:
  - en-US/zh-CN: settings.sections.account ("Account" / "账号")
  - en-US/zh-CN: button.logout ("Log out" / "退出登录")
  - types.ts: matching type declarations
2026-04-08 09:47:00 +08:00

133 lines
4.0 KiB
TypeScript

"use client";
import { LogOutIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { fetchWithAuth, getCsrfHeaders } from "@/core/api/fetcher";
import { useAuth } from "@/core/auth/AuthProvider";
import { parseAuthError } from "@/core/auth/types";
import { SettingsSection } from "./settings-section";
export function AccountSettingsPage() {
const { user, logout } = useAuth();
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [message, setMessage] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const handleChangePassword = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
setMessage("");
if (newPassword !== confirmPassword) {
setError("New passwords do not match");
return;
}
if (newPassword.length < 8) {
setError("Password must be at least 8 characters");
return;
}
setLoading(true);
try {
const res = await fetchWithAuth("/api/v1/auth/change-password", {
method: "POST",
headers: {
"Content-Type": "application/json",
...getCsrfHeaders(),
},
body: JSON.stringify({
current_password: currentPassword,
new_password: newPassword,
}),
});
if (!res.ok) {
const data = await res.json();
const authError = parseAuthError(data);
setError(authError.message);
return;
}
setMessage("Password changed successfully");
setCurrentPassword("");
setNewPassword("");
setConfirmPassword("");
} catch {
setError("Network error. Please try again.");
} finally {
setLoading(false);
}
};
return (
<div className="space-y-8">
<SettingsSection title="Profile">
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-muted-foreground text-sm">Email</span>
<span className="text-sm font-medium">{user?.email ?? "—"}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground text-sm">Role</span>
<span className="text-sm font-medium capitalize">
{user?.system_role ?? "—"}
</span>
</div>
</div>
</SettingsSection>
<SettingsSection title="Change Password">
<form onSubmit={handleChangePassword} className="max-w-sm space-y-3">
<Input
type="password"
placeholder="Current password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
required
/>
<Input
type="password"
placeholder="New password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
required
minLength={8}
/>
<Input
type="password"
placeholder="Confirm new password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
minLength={8}
/>
{error && <p className="text-sm text-red-500">{error}</p>}
{message && <p className="text-sm text-green-500">{message}</p>}
<Button type="submit" variant="outline" size="sm" disabled={loading}>
{loading ? "Updating..." : "Update Password"}
</Button>
</form>
</SettingsSection>
<SettingsSection title="Session">
<Button
variant="destructive"
size="sm"
onClick={logout}
className="gap-2"
>
<LogOutIcon className="size-4" />
Sign Out
</Button>
</SettingsSection>
</div>
);
}