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
This commit is contained in:
greatmengqi 2026-04-08 09:47:00 +08:00
parent f942e4e597
commit 2531cce0d1
5 changed files with 148 additions and 0 deletions

View File

@ -0,0 +1,132 @@
"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>
);
}

View File

@ -6,6 +6,7 @@ import {
BrainIcon,
PaletteIcon,
SparklesIcon,
UserIcon,
WrenchIcon,
} from "lucide-react";
import { useEffect, useMemo, useState } from "react";
@ -18,6 +19,7 @@ import {
} from "@/components/ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area";
import { AboutSettingsPage } from "@/components/workspace/settings/about-settings-page";
import { AccountSettingsPage } from "@/components/workspace/settings/account-settings-page";
import { AppearanceSettingsPage } from "@/components/workspace/settings/appearance-settings-page";
import { MemorySettingsPage } from "@/components/workspace/settings/memory-settings-page";
import { NotificationSettingsPage } from "@/components/workspace/settings/notification-settings-page";
@ -27,6 +29,7 @@ import { useI18n } from "@/core/i18n/hooks";
import { cn } from "@/lib/utils";
type SettingsSection =
| "account"
| "appearance"
| "memory"
| "tools"
@ -54,6 +57,11 @@ export function SettingsDialog(props: SettingsDialogProps) {
const sections = useMemo(
() => [
{
id: "account",
label: t.settings.sections.account,
icon: UserIcon,
},
{
id: "appearance",
label: t.settings.sections.appearance,
@ -74,6 +82,7 @@ export function SettingsDialog(props: SettingsDialogProps) {
{ id: "about", label: t.settings.sections.about, icon: InfoIcon },
],
[
t.settings.sections.account,
t.settings.sections.appearance,
t.settings.sections.memory,
t.settings.sections.tools,
@ -124,6 +133,7 @@ export function SettingsDialog(props: SettingsDialogProps) {
</nav>
<ScrollArea className="h-full min-h-0 rounded-lg border">
<div className="space-y-8 p-6">
{activeSection === "account" && <AccountSettingsPage />}
{activeSection === "appearance" && <AppearanceSettingsPage />}
{activeSection === "memory" && <MemorySettingsPage />}
{activeSection === "tools" && <ToolSettingsPage />}

View File

@ -236,6 +236,7 @@ export const enUS: Translations = {
reportIssue: "Report a issue",
contactUs: "Contact us",
about: "About DeerFlow",
logout: "Log out",
},
// Conversation
@ -320,6 +321,7 @@ export const enUS: Translations = {
title: "Settings",
description: "Adjust how DeerFlow looks and behaves for you.",
sections: {
account: "Account",
appearance: "Appearance",
memory: "Memory",
tools: "Tools",

View File

@ -168,6 +168,7 @@ export interface Translations {
reportIssue: string;
contactUs: string;
about: string;
logout: string;
};
// Conversation
@ -250,6 +251,7 @@ export interface Translations {
title: string;
description: string;
sections: {
account: string;
appearance: string;
memory: string;
tools: string;

View File

@ -224,6 +224,7 @@ export const zhCN: Translations = {
reportIssue: "报告问题",
contactUs: "联系我们",
about: "关于 DeerFlow",
logout: "退出登录",
},
// Conversation
@ -305,6 +306,7 @@ export const zhCN: Translations = {
title: "设置",
description: "根据你的偏好调整 DeerFlow 的界面和行为。",
sections: {
account: "账号",
appearance: "外观",
memory: "记忆",
tools: "工具",