TorbatYar/frontend/app/admin/users/page.tsx
Mortezakoohjani 800b0ba2c5 Deploy TorbatYar for torbatyar.ir with nginx multi-tenant routing.
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>
2026-07-21 21:43:33 +03:30

224 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { FormEvent, useCallback, useEffect, useState } from "react";
import { getStoredToken } from "@/lib/auth";
import { identityApi, type AdminUser } from "@/lib/identity";
import { normalizePhone } from "@/lib/phone";
import {
AdminPageHeader,
Banner,
Button,
EmptyState,
Modal,
StatusBadge,
TextField,
} from "@/components/admin/controls";
export default function AdminUsersPage() {
const [users, setUsers] = useState<AdminUser[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [modalOpen, setModalOpen] = useState(false);
const load = useCallback(async () => {
const token = getStoredToken();
if (!token) {
setError("توکن یافت نشد.");
setLoading(false);
return;
}
setLoading(true);
setError("");
try {
const rows = await identityApi.listUsers(token, { limit: 100 });
setUsers(rows);
} catch (e) {
setError(e instanceof Error ? e.message : "خطا در بارگذاری کاربران");
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
void load();
}, [load]);
return (
<div className="space-y-6">
<AdminPageHeader
title="مدیریت کاربران"
subtitle="کاربران ثبت‌شده در هویت مرکزی"
action={<Button onClick={() => setModalOpen(true)}>کاربر جدید</Button>}
/>
{error && <Banner variant="error">{error}</Banner>}
<section className="overflow-hidden rounded-2xl border border-gray-100 bg-white shadow-sm">
{loading ? (
<EmptyState message="در حال بارگذاری..." />
) : users.length === 0 ? (
<EmptyState message="هنوز کاربری ثبت نشده است." />
) : (
<div className="overflow-x-auto">
<table className="w-full min-w-[720px] text-sm">
<thead className="bg-gray-50 text-right">
<tr>
<th className="px-4 py-3 font-semibold text-secondary">نام</th>
<th className="px-4 py-3 font-semibold text-secondary">نام کاربری</th>
<th className="px-4 py-3 font-semibold text-secondary">موبایل</th>
<th className="px-4 py-3 font-semibold text-secondary">ایمیل</th>
<th className="px-4 py-3 font-semibold text-secondary">وضعیت</th>
</tr>
</thead>
<tbody>
{users.map((u) => (
<tr key={u.id} className="border-t border-gray-100">
<td className="px-4 py-3 font-medium text-secondary">
{u.display_name || u.username || "بدون نام"}
</td>
<td className="px-4 py-3 font-mono text-gray-600" dir="ltr">
{u.username || "—"}
</td>
<td className="px-4 py-3 font-mono text-gray-600" dir="ltr">
{u.mobile || "—"}
</td>
<td className="px-4 py-3 font-mono text-gray-600" dir="ltr">
{u.email}
</td>
<td className="px-4 py-3">
<div className="flex flex-wrap gap-2">
<StatusBadge status={u.status} />
<StatusBadge
status={u.mobile_verified ? "active" : "pending_activation"}
/>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</section>
<CreateUserModal
open={modalOpen}
onClose={() => setModalOpen(false)}
onCreated={() => {
setModalOpen(false);
void load();
}}
/>
</div>
);
}
function CreateUserModal({
open,
onClose,
onCreated,
}: {
open: boolean;
onClose: () => void;
onCreated: () => void;
}) {
const [displayName, setDisplayName] = useState("");
const [username, setUsername] = useState("");
const [email, setEmail] = useState("");
const [mobile, setMobile] = useState("");
const [password, setPassword] = useState("");
const [saving, setSaving] = useState(false);
const [error, setError] = useState("");
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
setError("");
const token = getStoredToken();
if (!token) {
setError("توکن یافت نشد.");
return;
}
if (password.length < 8) {
setError("رمز عبور باید حداقل ۸ کاراکتر باشد.");
return;
}
setSaving(true);
try {
await identityApi.createUser(token, {
email: email.trim(),
username: username.trim(),
password,
mobile: normalizePhone(mobile),
display_name: displayName.trim() || undefined,
});
setDisplayName("");
setUsername("");
setEmail("");
setMobile("");
setPassword("");
onCreated();
} catch (err) {
setError(err instanceof Error ? err.message : "ایجاد کاربر ناموفق بود");
} finally {
setSaving(false);
}
};
return (
<Modal open={open} title="ایجاد کاربر جدید" onClose={onClose}>
<form onSubmit={handleSubmit} className="space-y-4">
<TextField label="نام نمایشی" value={displayName} onValue={setDisplayName} disabled={saving} />
<TextField
label="نام کاربری"
required
dir="ltr"
className="font-mono"
value={username}
onValue={setUsername}
disabled={saving}
minLength={3}
/>
<TextField
label="ایمیل"
required
type="email"
dir="ltr"
className="font-mono"
value={email}
onValue={setEmail}
disabled={saving}
/>
<TextField
label="موبایل"
required
dir="ltr"
className="font-mono"
value={mobile}
onValue={setMobile}
disabled={saving}
placeholder="09xxxxxxxxx"
/>
<TextField
label="رمز عبور"
required
type="password"
dir="ltr"
value={password}
onValue={setPassword}
disabled={saving}
hint="حداقل ۸ کاراکتر"
/>
{error && <Banner variant="error">{error}</Banner>}
<div className="flex justify-end gap-3 pt-2">
<Button variant="outline" onClick={onClose} type="button">
انصراف
</Button>
<Button type="submit" loading={saving} loadingText="در حال ایجاد...">
ایجاد کاربر
</Button>
</div>
</form>
</Modal>
);
}