Wire production domain, CORS for tenant subdomains, celery volume mounts, and nginx reverse proxy configs for apex, API, identity, auth, and wildcard tenants. Co-authored-by: Cursor <cursoragent@cursor.com>
332 lines
10 KiB
TypeScript
332 lines
10 KiB
TypeScript
"use client";
|
||
|
||
import { FormEvent, useCallback, useEffect, useState } from "react";
|
||
import Link from "next/link";
|
||
import { getStoredToken } from "@/lib/auth";
|
||
import { identityApi, type AccountProfile } from "@/lib/identity";
|
||
import { Button, Container } from "@/components/ui";
|
||
|
||
const inputClass =
|
||
"mt-1 w-full rounded-xl border border-gray-200 px-4 py-2.5 focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary disabled:bg-gray-50";
|
||
|
||
const labelClass = "block text-sm font-medium text-secondary";
|
||
|
||
interface AccountSettingsProps {
|
||
backHref?: string;
|
||
backLabel?: string;
|
||
}
|
||
|
||
export function AccountSettings({ backHref, backLabel }: AccountSettingsProps) {
|
||
const [profile, setProfile] = useState<AccountProfile | null>(null);
|
||
const [loading, setLoading] = useState(true);
|
||
const [loadError, setLoadError] = useState("");
|
||
|
||
const load = useCallback(async () => {
|
||
const token = getStoredToken();
|
||
if (!token) {
|
||
setLoadError("توکن احراز هویت یافت نشد. لطفاً دوباره وارد شوید.");
|
||
setLoading(false);
|
||
return;
|
||
}
|
||
setLoading(true);
|
||
setLoadError("");
|
||
try {
|
||
const data = await identityApi.profile(token);
|
||
setProfile(data);
|
||
} catch (err) {
|
||
setLoadError(err instanceof Error ? err.message : "دریافت پروفایل ناموفق بود");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
void load();
|
||
}, [load]);
|
||
|
||
return (
|
||
<Container className="py-10">
|
||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-secondary">تنظیمات حساب</h1>
|
||
<p className="mt-1 text-sm text-gray-600">
|
||
مدیریت پروفایل و رمز عبور حساب کاربری شما
|
||
</p>
|
||
</div>
|
||
{backHref && (
|
||
<Link href={backHref}>
|
||
<Button variant="outline" size="sm">
|
||
{backLabel || "بازگشت"}
|
||
</Button>
|
||
</Link>
|
||
)}
|
||
</div>
|
||
|
||
{loadError && (
|
||
<p className="mt-6 rounded-lg bg-red-50 px-4 py-3 text-sm text-red-600">
|
||
{loadError}
|
||
</p>
|
||
)}
|
||
|
||
{loading ? (
|
||
<p className="mt-8 text-sm text-gray-500">در حال بارگذاری...</p>
|
||
) : profile ? (
|
||
<div className="mt-8 grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||
<ProfileForm profile={profile} onUpdated={setProfile} />
|
||
<PasswordForm />
|
||
</div>
|
||
) : null}
|
||
</Container>
|
||
);
|
||
}
|
||
|
||
function ProfileForm({
|
||
profile,
|
||
onUpdated,
|
||
}: {
|
||
profile: AccountProfile;
|
||
onUpdated: (p: AccountProfile) => void;
|
||
}) {
|
||
const [displayName, setDisplayName] = useState(profile.display_name || "");
|
||
const [email, setEmail] = useState(profile.email || "");
|
||
const [saving, setSaving] = useState(false);
|
||
const [error, setError] = useState("");
|
||
const [success, setSuccess] = useState("");
|
||
|
||
const handleSubmit = async (e: FormEvent) => {
|
||
e.preventDefault();
|
||
setError("");
|
||
setSuccess("");
|
||
const token = getStoredToken();
|
||
if (!token) {
|
||
setError("توکن احراز هویت یافت نشد.");
|
||
return;
|
||
}
|
||
setSaving(true);
|
||
try {
|
||
const updated = await identityApi.updateProfile(token, {
|
||
display_name: displayName.trim() || null,
|
||
email: email.trim() || undefined,
|
||
});
|
||
onUpdated(updated);
|
||
setSuccess("پروفایل با موفقیت ذخیره شد.");
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : "ذخیره پروفایل ناموفق بود");
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<form
|
||
onSubmit={handleSubmit}
|
||
className="rounded-2xl border border-gray-100 bg-white p-6 shadow-sm"
|
||
>
|
||
<h2 className="text-lg font-bold text-secondary">اطلاعات پروفایل</h2>
|
||
<p className="mt-1 text-xs text-gray-500">
|
||
نام نمایشی و ایمیل خود را ویرایش کنید.
|
||
</p>
|
||
|
||
<div className="mt-6 space-y-4">
|
||
<div>
|
||
<label className={labelClass} htmlFor="display_name">
|
||
نام نمایشی
|
||
</label>
|
||
<input
|
||
id="display_name"
|
||
className={inputClass}
|
||
value={displayName}
|
||
onChange={(e) => setDisplayName(e.target.value)}
|
||
placeholder="نام و نام خانوادگی"
|
||
maxLength={255}
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className={labelClass} htmlFor="email">
|
||
ایمیل
|
||
</label>
|
||
<input
|
||
id="email"
|
||
type="email"
|
||
dir="ltr"
|
||
className={inputClass}
|
||
value={email}
|
||
onChange={(e) => setEmail(e.target.value)}
|
||
placeholder="you@example.com"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className={labelClass}>شماره موبایل</label>
|
||
<input
|
||
className={`${inputClass} font-mono`}
|
||
dir="ltr"
|
||
value={profile.mobile || "—"}
|
||
disabled
|
||
readOnly
|
||
/>
|
||
<p className="mt-1 text-xs text-gray-400">
|
||
{profile.mobile_verified
|
||
? "شماره موبایل تأیید شده است."
|
||
: "برای تغییر شماره موبایل با پشتیبانی تماس بگیرید."}
|
||
</p>
|
||
</div>
|
||
|
||
{profile.username && (
|
||
<div>
|
||
<label className={labelClass}>نام کاربری</label>
|
||
<input
|
||
className={`${inputClass} font-mono`}
|
||
dir="ltr"
|
||
value={profile.username}
|
||
disabled
|
||
readOnly
|
||
/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{error && (
|
||
<p className="mt-4 rounded-lg bg-red-50 px-3 py-2 text-sm text-red-600">
|
||
{error}
|
||
</p>
|
||
)}
|
||
{success && (
|
||
<p className="mt-4 rounded-lg bg-green-50 px-3 py-2 text-sm text-green-700">
|
||
{success}
|
||
</p>
|
||
)}
|
||
|
||
<div className="mt-6">
|
||
<Button type="submit" loading={saving} loadingText="در حال ذخیره...">
|
||
ذخیره تغییرات
|
||
</Button>
|
||
</div>
|
||
</form>
|
||
);
|
||
}
|
||
|
||
function PasswordForm() {
|
||
const [currentPassword, setCurrentPassword] = useState("");
|
||
const [newPassword, setNewPassword] = useState("");
|
||
const [confirmPassword, setConfirmPassword] = useState("");
|
||
const [saving, setSaving] = useState(false);
|
||
const [error, setError] = useState("");
|
||
const [success, setSuccess] = useState("");
|
||
|
||
const handleSubmit = async (e: FormEvent) => {
|
||
e.preventDefault();
|
||
setError("");
|
||
setSuccess("");
|
||
|
||
if (newPassword.length < 8) {
|
||
setError("رمز عبور جدید باید حداقل ۸ کاراکتر باشد.");
|
||
return;
|
||
}
|
||
if (newPassword !== confirmPassword) {
|
||
setError("رمز عبور جدید و تکرار آن یکسان نیستند.");
|
||
return;
|
||
}
|
||
|
||
const token = getStoredToken();
|
||
if (!token) {
|
||
setError("توکن احراز هویت یافت نشد.");
|
||
return;
|
||
}
|
||
|
||
setSaving(true);
|
||
try {
|
||
await identityApi.changePassword(token, {
|
||
current_password: currentPassword.trim() || undefined,
|
||
new_password: newPassword,
|
||
});
|
||
setSuccess("رمز عبور با موفقیت بهروزرسانی شد.");
|
||
setCurrentPassword("");
|
||
setNewPassword("");
|
||
setConfirmPassword("");
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : "تغییر رمز عبور ناموفق بود");
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<form
|
||
onSubmit={handleSubmit}
|
||
className="rounded-2xl border border-gray-100 bg-white p-6 shadow-sm"
|
||
>
|
||
<h2 className="text-lg font-bold text-secondary">تغییر رمز عبور</h2>
|
||
<p className="mt-1 text-xs text-gray-500">
|
||
اگر تاکنون رمز عبوری تنظیم نکردهاید، فیلد رمز فعلی را خالی بگذارید.
|
||
</p>
|
||
|
||
<div className="mt-6 space-y-4">
|
||
<div>
|
||
<label className={labelClass} htmlFor="current_password">
|
||
رمز عبور فعلی (اختیاری)
|
||
</label>
|
||
<input
|
||
id="current_password"
|
||
type="password"
|
||
dir="ltr"
|
||
className={inputClass}
|
||
value={currentPassword}
|
||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||
autoComplete="current-password"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className={labelClass} htmlFor="new_password">
|
||
رمز عبور جدید
|
||
</label>
|
||
<input
|
||
id="new_password"
|
||
type="password"
|
||
dir="ltr"
|
||
className={inputClass}
|
||
value={newPassword}
|
||
onChange={(e) => setNewPassword(e.target.value)}
|
||
autoComplete="new-password"
|
||
placeholder="حداقل ۸ کاراکتر"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className={labelClass} htmlFor="confirm_password">
|
||
تکرار رمز عبور جدید
|
||
</label>
|
||
<input
|
||
id="confirm_password"
|
||
type="password"
|
||
dir="ltr"
|
||
className={inputClass}
|
||
value={confirmPassword}
|
||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||
autoComplete="new-password"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{error && (
|
||
<p className="mt-4 rounded-lg bg-red-50 px-3 py-2 text-sm text-red-600">
|
||
{error}
|
||
</p>
|
||
)}
|
||
{success && (
|
||
<p className="mt-4 rounded-lg bg-green-50 px-3 py-2 text-sm text-green-700">
|
||
{success}
|
||
</p>
|
||
)}
|
||
|
||
<div className="mt-6">
|
||
<Button type="submit" loading={saving} loadingText="در حال ذخیره...">
|
||
بهروزرسانی رمز عبور
|
||
</Button>
|
||
</div>
|
||
</form>
|
||
);
|
||
}
|