mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-24 00:45:57 +00:00
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:
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
BrainIcon,
|
BrainIcon,
|
||||||
PaletteIcon,
|
PaletteIcon,
|
||||||
SparklesIcon,
|
SparklesIcon,
|
||||||
|
UserIcon,
|
||||||
WrenchIcon,
|
WrenchIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
@@ -18,6 +19,7 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { AboutSettingsPage } from "@/components/workspace/settings/about-settings-page";
|
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 { AppearanceSettingsPage } from "@/components/workspace/settings/appearance-settings-page";
|
||||||
import { MemorySettingsPage } from "@/components/workspace/settings/memory-settings-page";
|
import { MemorySettingsPage } from "@/components/workspace/settings/memory-settings-page";
|
||||||
import { NotificationSettingsPage } from "@/components/workspace/settings/notification-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";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type SettingsSection =
|
type SettingsSection =
|
||||||
|
| "account"
|
||||||
| "appearance"
|
| "appearance"
|
||||||
| "memory"
|
| "memory"
|
||||||
| "tools"
|
| "tools"
|
||||||
@@ -54,6 +57,11 @@ export function SettingsDialog(props: SettingsDialogProps) {
|
|||||||
|
|
||||||
const sections = useMemo(
|
const sections = useMemo(
|
||||||
() => [
|
() => [
|
||||||
|
{
|
||||||
|
id: "account",
|
||||||
|
label: t.settings.sections.account,
|
||||||
|
icon: UserIcon,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "appearance",
|
id: "appearance",
|
||||||
label: t.settings.sections.appearance,
|
label: t.settings.sections.appearance,
|
||||||
@@ -74,6 +82,7 @@ export function SettingsDialog(props: SettingsDialogProps) {
|
|||||||
{ id: "about", label: t.settings.sections.about, icon: InfoIcon },
|
{ id: "about", label: t.settings.sections.about, icon: InfoIcon },
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
t.settings.sections.account,
|
||||||
t.settings.sections.appearance,
|
t.settings.sections.appearance,
|
||||||
t.settings.sections.memory,
|
t.settings.sections.memory,
|
||||||
t.settings.sections.tools,
|
t.settings.sections.tools,
|
||||||
@@ -124,6 +133,7 @@ export function SettingsDialog(props: SettingsDialogProps) {
|
|||||||
</nav>
|
</nav>
|
||||||
<ScrollArea className="h-full min-h-0 rounded-lg border">
|
<ScrollArea className="h-full min-h-0 rounded-lg border">
|
||||||
<div className="space-y-8 p-6">
|
<div className="space-y-8 p-6">
|
||||||
|
{activeSection === "account" && <AccountSettingsPage />}
|
||||||
{activeSection === "appearance" && <AppearanceSettingsPage />}
|
{activeSection === "appearance" && <AppearanceSettingsPage />}
|
||||||
{activeSection === "memory" && <MemorySettingsPage />}
|
{activeSection === "memory" && <MemorySettingsPage />}
|
||||||
{activeSection === "tools" && <ToolSettingsPage />}
|
{activeSection === "tools" && <ToolSettingsPage />}
|
||||||
|
|||||||
@@ -236,6 +236,7 @@ export const enUS: Translations = {
|
|||||||
reportIssue: "Report a issue",
|
reportIssue: "Report a issue",
|
||||||
contactUs: "Contact us",
|
contactUs: "Contact us",
|
||||||
about: "About DeerFlow",
|
about: "About DeerFlow",
|
||||||
|
logout: "Log out",
|
||||||
},
|
},
|
||||||
|
|
||||||
// Conversation
|
// Conversation
|
||||||
@@ -320,6 +321,7 @@ export const enUS: Translations = {
|
|||||||
title: "Settings",
|
title: "Settings",
|
||||||
description: "Adjust how DeerFlow looks and behaves for you.",
|
description: "Adjust how DeerFlow looks and behaves for you.",
|
||||||
sections: {
|
sections: {
|
||||||
|
account: "Account",
|
||||||
appearance: "Appearance",
|
appearance: "Appearance",
|
||||||
memory: "Memory",
|
memory: "Memory",
|
||||||
tools: "Tools",
|
tools: "Tools",
|
||||||
|
|||||||
@@ -168,6 +168,7 @@ export interface Translations {
|
|||||||
reportIssue: string;
|
reportIssue: string;
|
||||||
contactUs: string;
|
contactUs: string;
|
||||||
about: string;
|
about: string;
|
||||||
|
logout: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Conversation
|
// Conversation
|
||||||
@@ -250,6 +251,7 @@ export interface Translations {
|
|||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
sections: {
|
sections: {
|
||||||
|
account: string;
|
||||||
appearance: string;
|
appearance: string;
|
||||||
memory: string;
|
memory: string;
|
||||||
tools: string;
|
tools: string;
|
||||||
|
|||||||
@@ -224,6 +224,7 @@ export const zhCN: Translations = {
|
|||||||
reportIssue: "报告问题",
|
reportIssue: "报告问题",
|
||||||
contactUs: "联系我们",
|
contactUs: "联系我们",
|
||||||
about: "关于 DeerFlow",
|
about: "关于 DeerFlow",
|
||||||
|
logout: "退出登录",
|
||||||
},
|
},
|
||||||
|
|
||||||
// Conversation
|
// Conversation
|
||||||
@@ -305,6 +306,7 @@ export const zhCN: Translations = {
|
|||||||
title: "设置",
|
title: "设置",
|
||||||
description: "根据你的偏好调整 DeerFlow 的界面和行为。",
|
description: "根据你的偏好调整 DeerFlow 的界面和行为。",
|
||||||
sections: {
|
sections: {
|
||||||
|
account: "账号",
|
||||||
appearance: "外观",
|
appearance: "外观",
|
||||||
memory: "记忆",
|
memory: "记忆",
|
||||||
tools: "工具",
|
tools: "工具",
|
||||||
|
|||||||
Reference in New Issue
Block a user