sare26 commited on
Commit
b501cbf
·
verified ·
1 Parent(s): 75af741

Update artifacts/fb-publisher/src/pages/Admin.tsx

Browse files
artifacts/fb-publisher/src/pages/Admin.tsx CHANGED
@@ -6,7 +6,6 @@ import { Button } from "@/components/ui/button";
6
  import { Input } from "@/components/ui/input";
7
  import { useToast } from "@/hooks/use-toast";
8
 
9
- // ========== Supabase Client ==========
10
  const supabase = createClient(
11
  "https://eijwjjbkluurxjiiuhay.supabase.co",
12
  "sb_publishable_VZEm1rag3JF-39xXx2F5og_1oNkjR5h"
@@ -18,6 +17,13 @@ interface UpdateStatus {
18
  message: string | null;
19
  }
20
 
 
 
 
 
 
 
 
21
  async function fetchUpdateStatus(): Promise<UpdateStatus> {
22
  const res = await fetch(apiUrl("extension-update"));
23
  if (!res.ok) return { updateAvailable: false, version: null, message: null };
@@ -25,10 +31,10 @@ async function fetchUpdateStatus(): Promise<UpdateStatus> {
25
  }
26
 
27
  async function setExtensionUpdate(body: { enabled: boolean; version?: string; message?: string }): Promise<void> {
 
28
  const res = await fetch(apiUrl("admin/extension-update"), {
29
  method: "PATCH",
30
- credentials: "include",
31
- headers: { "Content-Type": "application/json" },
32
  body: JSON.stringify(body),
33
  });
34
  if (!res.ok) throw new Error((await res.json()).error ?? "خطأ في التحديث");
@@ -81,82 +87,75 @@ interface BlockedEmail {
81
  }
82
 
83
  async function fetchUsers(): Promise<{ users: UserRow[] }> {
84
- const res = await fetch(apiUrl("admin/users"), { credentials: "include" });
 
85
  if (!res.ok) throw new Error((await res.json()).error ?? "خطأ في الجلب");
86
  return res.json();
87
  }
88
 
89
  async function fetchBlockedEmails(): Promise<{ blockedEmails: BlockedEmail[] }> {
90
- const res = await fetch(apiUrl("admin/blocked-emails"), { credentials: "include" });
 
91
  if (!res.ok) throw new Error((await res.json()).error ?? "خطأ في الجلب");
92
  return res.json();
93
  }
94
 
95
  async function blockEmail(email: string, reason: string): Promise<void> {
 
96
  const res = await fetch(apiUrl("admin/blocked-emails"), {
97
  method: "POST",
98
- credentials: "include",
99
- headers: { "Content-Type": "application/json" },
100
  body: JSON.stringify({ email, reason }),
101
  });
102
  if (!res.ok) throw new Error((await res.json()).error ?? "خطأ في الحظر");
103
  }
104
 
105
  async function cleanupOrphans(): Promise<{ removed: string[]; count: number }> {
106
- const res = await fetch(apiUrl("admin/cleanup-orphans"), {
107
- method: "POST",
108
- credentials: "include",
109
- });
110
  if (!res.ok) throw new Error((await res.json()).error ?? "خطأ في التنظيف");
111
  return res.json();
112
  }
113
 
114
  async function forceSignOutBanned(): Promise<{ usersNotified: number; connectionsSent: number }> {
115
- const res = await fetch(apiUrl("admin/force-signout-banned"), {
116
- method: "POST",
117
- credentials: "include",
118
- });
119
  if (!res.ok) throw new Error((await res.json()).error ?? "خطأ في الطرد");
120
  return res.json();
121
  }
122
 
123
  async function unblockEmail(email: string): Promise<void> {
 
124
  const res = await fetch(apiUrl(`admin/blocked-emails/${encodeURIComponent(email)}`), {
125
  method: "DELETE",
126
- credentials: "include",
127
  });
128
  if (!res.ok) throw new Error((await res.json()).error ?? "خطأ في إلغاء الحظر");
129
  }
130
 
131
  async function banUser(id: string): Promise<void> {
132
- const res = await fetch(apiUrl(`admin/ban/${id}`), {
133
- method: "POST",
134
- credentials: "include",
135
- });
136
  if (!res.ok) throw new Error((await res.json()).error ?? "خطأ في الحظر");
137
  }
138
 
139
  async function unbanUser(id: string): Promise<void> {
140
- const res = await fetch(apiUrl(`admin/unban/${id}`), {
141
- method: "POST",
142
- credentials: "include",
143
- });
144
  if (!res.ok) throw new Error((await res.json()).error ?? "خطأ في رفع الحظر");
145
  }
146
 
147
  async function deleteUser(id: string): Promise<void> {
148
- const res = await fetch(apiUrl(`admin/users/${id}`), {
149
- method: "DELETE",
150
- credentials: "include",
151
- });
152
  if (!res.ok) throw new Error((await res.json()).error ?? "خطأ في الحذف");
153
  }
154
 
155
  async function updateSubscription(userId: string, body: { status?: string; trialEnd?: string | null }): Promise<void> {
 
156
  const res = await fetch(apiUrl(`admin/subscription/${userId}`), {
157
  method: "PATCH",
158
- credentials: "include",
159
- headers: { "Content-Type": "application/json" },
160
  body: JSON.stringify(body),
161
  });
162
  if (!res.ok) throw new Error((await res.json()).error ?? "خطأ في التحديث");
@@ -173,7 +172,6 @@ export default function Admin() {
173
  const [blockEmailInput, setBlockEmailInput] = useState("");
174
  const [blockReasonInput, setBlockReasonInput] = useState("");
175
 
176
- // ========== Supabase Auth State ==========
177
  const [session, setSession] = useState<any>(null);
178
  const [authLoading, setAuthLoading] = useState(true);
179
 
@@ -182,11 +180,9 @@ export default function Admin() {
182
  setSession(session);
183
  setAuthLoading(false);
184
  });
185
-
186
  const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
187
  setSession(session);
188
  });
189
-
190
  return () => subscription.unsubscribe();
191
  }, []);
192
 
@@ -337,9 +333,7 @@ export default function Admin() {
337
  <div className="flex min-h-screen items-center justify-center bg-[#0f0f1e]">
338
  <div className="text-center space-y-3">
339
  <div className="text-red-400 text-2xl">⛔ غير مصرح بالدخول</div>
340
- <div className="text-gray-500 text-sm">
341
- البريد المسجل: {userEmail}
342
- </div>
343
  </div>
344
  </div>
345
  );
@@ -364,7 +358,8 @@ export default function Admin() {
364
  size="sm"
365
  onClick={async () => {
366
  if (!confirm("مسح إعدادات PayPal المخزّنة؟ ستُعاد إنشاؤها تلقائياً عند أول اشتراك.")) return;
367
- const res = await fetch(apiUrl("admin/paypal-config"), { method: "DELETE", credentials: "include" });
 
368
  const data = await res.json();
369
  toast({ title: res.ok ? "✅ تم المسح" : "❌ خطأ", description: data.message ?? data.error });
370
  }}
@@ -400,31 +395,17 @@ export default function Admin() {
400
  variant="outline"
401
  size="sm"
402
  onClick={() => {
403
- const emails = (users ?? [])
404
- .map((u) => u.email)
405
- .filter(Boolean)
406
- .join("\n");
407
- if (!emails) {
408
- toast({ title: "لا توجد إيميلات", variant: "destructive" });
409
- return;
410
- }
411
  navigator.clipboard.writeText(emails).then(() => {
412
- toast({
413
- title: `✅ تم النسخ`,
414
- description: `${emails.split("\n").length} إيميل في الحافظة`,
415
- });
416
  });
417
  }}
418
  className="border-blue-700 text-blue-400 hover:bg-blue-900/20 text-xs"
419
  >
420
  📋 نسخ الإيميلات
421
  </Button>
422
- <Button
423
- variant="outline"
424
- size="sm"
425
- onClick={() => refetch()}
426
- className="border-[#3a3a55] text-gray-300 hover:text-white"
427
- >
428
  تحديث
429
  </Button>
430
  </div>
@@ -441,42 +422,17 @@ export default function Admin() {
441
  )}
442
  </div>
443
  <div className="flex gap-2">
444
- <Button
445
- size="sm"
446
- disabled={updateExtMutation.isPending}
447
- onClick={() => updateExtMutation.mutate({
448
- enabled: true,
449
- version: newVersion.trim() || undefined,
450
- message: newMessage.trim() || undefined,
451
- })}
452
- className="bg-[#bb86fc] hover:bg-[#a070e0] text-black font-bold text-xs"
453
- >
454
  📢 إرسال الإشعار للجميع
455
  </Button>
456
- <Button
457
- size="sm"
458
- variant="outline"
459
- disabled={updateExtMutation.isPending || !updateStatus?.updateAvailable}
460
- onClick={() => updateExtMutation.mutate({ enabled: false })}
461
- className="border-red-800 text-red-400 hover:bg-red-900/20 text-xs"
462
- >
463
  ✕ إيقاف الإشعار
464
  </Button>
465
  </div>
466
  </div>
467
  <div className="flex gap-2 flex-wrap">
468
- <Input
469
- placeholder="رقم الإصدار — مثال: 1.3.0"
470
- value={newVersion}
471
- onChange={(e) => setNewVersion(e.target.value)}
472
- className="bg-[#0f0f1e] border-[#3a3a55] text-white placeholder:text-gray-600 text-sm flex-1 min-w-[140px]"
473
- />
474
- <Input
475
- placeholder="رسالة اختيارية للمستخدمين"
476
- value={newMessage}
477
- onChange={(e) => setNewMessage(e.target.value)}
478
- className="bg-[#0f0f1e] border-[#3a3a55] text-white placeholder:text-gray-600 text-sm flex-1 min-w-[200px]"
479
- />
480
  </div>
481
  {updateStatus?.updateAvailable && (
482
  <div className="mt-2 text-xs text-gray-500">
@@ -489,83 +445,35 @@ export default function Admin() {
489
  <div className="mb-6 rounded-xl border border-[#3a3a55] bg-[#1a1a30] p-4">
490
  <div className="flex items-center gap-2 mb-3">
491
  <span className="text-base font-semibold text-red-400">🚫 البريد الإلكتروني المحظور</span>
492
- <span className="rounded-full bg-red-900/30 px-2 py-0.5 text-xs text-red-400 border border-red-800">
493
- {blockedData?.blockedEmails.length ?? 0}
494
- </span>
495
  </div>
496
  <div className="flex gap-2 flex-wrap mb-3">
497
- <Input
498
- placeholder="بريد إلكتروني للحظر"
499
- value={blockEmailInput}
500
- onChange={(e) => setBlockEmailInput(e.target.value)}
501
- onKeyDown={(e) => {
502
- if (e.key === "Enter" && blockEmailInput.includes("@")) {
503
- blockMutation.mutate({ email: blockEmailInput, reason: blockReasonInput });
504
- }
505
- }}
506
- className="bg-[#0f0f1e] border-[#3a3a55] text-white placeholder:text-gray-600 text-sm flex-1 min-w-[200px]"
507
- />
508
- <Input
509
- placeholder="سبب الحظر (اختياري)"
510
- value={blockReasonInput}
511
- onChange={(e) => setBlockReasonInput(e.target.value)}
512
- className="bg-[#0f0f1e] border-[#3a3a55] text-white placeholder:text-gray-600 text-sm flex-1 min-w-[150px]"
513
- />
514
- <Button
515
- size="sm"
516
- disabled={blockMutation.isPending || !blockEmailInput.includes("@")}
517
- onClick={() => blockMutation.mutate({ email: blockEmailInput, reason: blockReasonInput })}
518
- className="bg-red-700 hover:bg-red-800 text-white text-xs font-bold"
519
- >
520
  🚫 حظر البريد
521
  </Button>
522
  </div>
523
- {blockedData?.blockedEmails.length === 0 && (
524
- <div className="text-xs text-gray-600 text-center py-2">لا توجد بريد محظور</div>
525
- )}
526
  {(blockedData?.blockedEmails.length ?? 0) > 0 && (
527
  <div className="space-y-1 max-h-48 overflow-y-auto">
528
  {blockedData!.blockedEmails.map((b) => (
529
  <div key={b.email} className="flex items-center justify-between rounded-lg bg-[#0f0f1e] px-3 py-2 text-sm">
530
- <div>
531
- <span className="text-red-300 font-mono text-xs">{b.email}</span>
532
- {b.reason && <span className="text-gray-500 text-xs mr-2">— {b.reason}</span>}
533
- </div>
534
- <Button
535
- size="sm"
536
- variant="outline"
537
- disabled={unblockMutation.isPending}
538
- onClick={() => unblockMutation.mutate(b.email)}
539
- className="text-xs border-gray-700 text-gray-400 hover:text-white h-6 px-2"
540
- >
541
- رفع الحظر
542
- </Button>
543
  </div>
544
  ))}
545
  </div>
546
  )}
547
  </div>
548
  <div className="mb-4">
549
- <Input
550
- placeholder="ابحث بالبريد أو المعرّف..."
551
- value={search}
552
- onChange={(e) => setSearch(e.target.value)}
553
- className="bg-[#1f1f3a] border-[#3a3a55] text-white placeholder:text-gray-500"
554
- />
555
  </div>
556
- {isLoading && (
557
- <div className="text-center text-gray-400 py-16">جارٍ تحميل المستخدمين...</div>
558
- )}
559
- {error && (
560
- <div className="text-center text-red-400 py-16">
561
- خطأ: {(error as any).message}
562
- </div>
563
- )}
564
  {!isLoading && !error && (
565
  <>
566
- <div className="text-sm text-gray-500 mb-3">
567
- إجمالي المستخدمين: {users.length} | المعروض: {filtered.length}
568
- </div>
569
  <div className="rounded-xl border border-[#3a3a55] overflow-hidden">
570
  <table className="w-full text-sm">
571
  <thead className="bg-[#1f1f3a] text-gray-400">
@@ -579,140 +487,32 @@ export default function Admin() {
579
  </tr>
580
  </thead>
581
  <tbody>
582
- {filtered.length === 0 && (
583
- <tr>
584
- <td colSpan={5} className="text-center text-gray-500 py-10">
585
- لا يوجد مستخدمون
586
- </td>
587
- </tr>
588
- )}
589
  {filtered.map((u) => {
590
  const sl = statusLabel(u.subscription);
591
  const isMe = u.email?.toLowerCase() === OWNER_EMAIL.toLowerCase();
592
  return (
593
- <tr
594
- key={u.id}
595
- style={u.isBanned ? { opacity: 0.7, background: "rgba(234,88,12,0.05)" } : undefined}
596
- className="border-t border-[#2a2a45] hover:bg-[#1a1a30] transition-colors"
597
- >
598
  <td className="px-4 py-3">
599
  <div className="font-medium flex items-center gap-2 flex-wrap">
600
  {u.email ?? <span className="text-gray-500">بدون بريد</span>}
601
- {u.isBanned && (
602
- <span className="text-xs bg-orange-900/40 text-orange-400 border border-orange-700 rounded px-1.5 py-0.5">🚫 محظور</span>
603
- )}
604
  </div>
605
- {isMe && (
606
- <span className="text-xs text-[#bb86fc]">أنت (المالك)</span>
607
- )}
608
  </td>
609
  <td className="px-4 py-3">
610
  <span className={sl.color}>{sl.text}</span>
611
- {u.subscription?.trialEnd && u.subscription.status === "trialing" && (
612
- <div className="text-xs text-gray-500">
613
- تنتهي: {formatDate(u.subscription.trialEnd)}
614
- </div>
615
- )}
616
- </td>
617
- <td className="px-4 py-3">
618
- {u.ipAddress ? (
619
- <div className="flex flex-col gap-1">
620
- <span className="font-mono text-xs text-gray-300 select-all">{u.ipAddress}</span>
621
- {!isMe && (
622
- <button
623
- className="text-xs text-red-400 hover:text-red-300 underline text-right"
624
- onClick={() => blockMutation.mutate({ email: u.ipAddress!, reason: "حظر بالـ IP" })}
625
- disabled={blockMutation.isPending}
626
- >
627
- 🚫 حظر IP
628
- </button>
629
- )}
630
- </div>
631
- ) : (
632
- <span className="text-gray-600 text-xs">—</span>
633
- )}
634
- </td>
635
- <td className="px-4 py-3 text-gray-400">
636
- {formatDate(u.createdAt)}
637
- </td>
638
- <td className="px-4 py-3 text-gray-400">
639
- {u.lastSeenAt ? (
640
- <div>
641
- <div>{formatDate(u.lastSeenAt)}</div>
642
- <div className="text-xs text-gray-600">يُحدَّث مرة/يوم</div>
643
- </div>
644
- ) : (
645
- <span className="text-gray-600">—</span>
646
- )}
647
  </td>
 
 
 
648
  <td className="px-4 py-3">
649
  <div className="flex flex-col gap-1">
650
- {(u.subscription?.status === "trialing" || isMe) && (
651
- <Button
652
- size="sm"
653
- variant="outline"
654
- disabled={updateSubMutation.isPending}
655
- onClick={() => updateSubMutation.mutate({
656
- userId: u.id,
657
- body: { status: "trial_expired", trialEnd: new Date(Date.now() - 60000).toISOString() }
658
- })}
659
- className="text-xs border-yellow-700 text-yellow-400 hover:bg-yellow-900/20"
660
- >
661
- ⏰ إنهاء التجربة
662
- </Button>
663
- )}
664
- {u.subscription && u.subscription.status !== "trialing" && (
665
- <Button
666
- size="sm"
667
- variant="outline"
668
- disabled={updateSubMutation.isPending}
669
- onClick={() => updateSubMutation.mutate({
670
- userId: u.id,
671
- body: { status: "trialing", trialEnd: new Date(Date.now() + 30 * 24 * 3600 * 1000).toISOString() }
672
- })}
673
- className="text-xs border-blue-700 text-blue-400 hover:bg-blue-900/20"
674
- >
675
- 🔄 إعادة التجربة
676
- </Button>
677
- )}
678
- {!isMe && (
679
- u.isBanned ? (
680
- <Button
681
- size="sm"
682
- variant="outline"
683
- disabled={unbanMutation.isPending}
684
- onClick={() => unbanMutation.mutate(u.id)}
685
- className="text-xs border-green-700 text-green-400 hover:bg-green-900/20"
686
- >
687
- {unbanMutation.isPending ? "⏳" : "✅ رفع الحظر"}
688
- </Button>
689
- ) : (
690
- <Button
691
- size="sm"
692
- variant="outline"
693
- disabled={banMutation.isPending}
694
- onClick={() => {
695
- if (!confirm(`حظر ${u.email ?? u.id}؟ يمكن فك الحظر لاحقاً.`)) return;
696
- banMutation.mutate(u.id);
697
- }}
698
- className="text-xs border-orange-700 text-orange-400 hover:bg-orange-900/20"
699
- >
700
- {banMutation.isPending ? "⏳" : "🚫 حظر الحساب"}
701
- </Button>
702
- )
703
- )}
704
- {!isMe && confirmId === u.id ? (
705
- <div className="flex gap-1">
706
- <Button size="sm" variant="destructive" onClick={() => deleteMutation.mutate(u.id)} disabled={deleteMutation.isPending} className="text-xs">
707
- {deleteMutation.isPending ? "جارٍ..." : "تأكيد"}
708
- </Button>
709
- <Button size="sm" variant="outline" onClick={() => setConfirmId(null)} className="text-xs border-[#3a3a55] text-gray-300">إلغاء</Button>
710
- </div>
711
- ) : !isMe ? (
712
- <Button size="sm" variant="outline" onClick={() => setConfirmId(u.id)} className="text-xs border-red-800 text-red-400 hover:bg-red-900/20">
713
- 🗑️ حظر وحذف البيانات
714
- </Button>
715
- ) : null}
716
  </div>
717
  </td>
718
  </tr>
 
6
  import { Input } from "@/components/ui/input";
7
  import { useToast } from "@/hooks/use-toast";
8
 
 
9
  const supabase = createClient(
10
  "https://eijwjjbkluurxjiiuhay.supabase.co",
11
  "sb_publishable_VZEm1rag3JF-39xXx2F5og_1oNkjR5h"
 
17
  message: string | null;
18
  }
19
 
20
+ // ========== دالة مساعدة لجلب الـ token ==========
21
+ async function getAuthHeaders(): Promise<Record<string, string>> {
22
+ const { data: { session } } = await supabase.auth.getSession();
23
+ const token = session?.access_token;
24
+ return token ? { Authorization: `Bearer ${token}`, "Content-Type": "application/json" } : { "Content-Type": "application/json" };
25
+ }
26
+
27
  async function fetchUpdateStatus(): Promise<UpdateStatus> {
28
  const res = await fetch(apiUrl("extension-update"));
29
  if (!res.ok) return { updateAvailable: false, version: null, message: null };
 
31
  }
32
 
33
  async function setExtensionUpdate(body: { enabled: boolean; version?: string; message?: string }): Promise<void> {
34
+ const headers = await getAuthHeaders();
35
  const res = await fetch(apiUrl("admin/extension-update"), {
36
  method: "PATCH",
37
+ headers,
 
38
  body: JSON.stringify(body),
39
  });
40
  if (!res.ok) throw new Error((await res.json()).error ?? "خطأ في التحديث");
 
87
  }
88
 
89
  async function fetchUsers(): Promise<{ users: UserRow[] }> {
90
+ const headers = await getAuthHeaders();
91
+ const res = await fetch(apiUrl("admin/users"), { headers });
92
  if (!res.ok) throw new Error((await res.json()).error ?? "خطأ في الجلب");
93
  return res.json();
94
  }
95
 
96
  async function fetchBlockedEmails(): Promise<{ blockedEmails: BlockedEmail[] }> {
97
+ const headers = await getAuthHeaders();
98
+ const res = await fetch(apiUrl("admin/blocked-emails"), { headers });
99
  if (!res.ok) throw new Error((await res.json()).error ?? "خطأ في الجلب");
100
  return res.json();
101
  }
102
 
103
  async function blockEmail(email: string, reason: string): Promise<void> {
104
+ const headers = await getAuthHeaders();
105
  const res = await fetch(apiUrl("admin/blocked-emails"), {
106
  method: "POST",
107
+ headers,
 
108
  body: JSON.stringify({ email, reason }),
109
  });
110
  if (!res.ok) throw new Error((await res.json()).error ?? "خطأ في الحظر");
111
  }
112
 
113
  async function cleanupOrphans(): Promise<{ removed: string[]; count: number }> {
114
+ const headers = await getAuthHeaders();
115
+ const res = await fetch(apiUrl("admin/cleanup-orphans"), { method: "POST", headers });
 
 
116
  if (!res.ok) throw new Error((await res.json()).error ?? "خطأ في التنظيف");
117
  return res.json();
118
  }
119
 
120
  async function forceSignOutBanned(): Promise<{ usersNotified: number; connectionsSent: number }> {
121
+ const headers = await getAuthHeaders();
122
+ const res = await fetch(apiUrl("admin/force-signout-banned"), { method: "POST", headers });
 
 
123
  if (!res.ok) throw new Error((await res.json()).error ?? "خطأ في الطرد");
124
  return res.json();
125
  }
126
 
127
  async function unblockEmail(email: string): Promise<void> {
128
+ const headers = await getAuthHeaders();
129
  const res = await fetch(apiUrl(`admin/blocked-emails/${encodeURIComponent(email)}`), {
130
  method: "DELETE",
131
+ headers,
132
  });
133
  if (!res.ok) throw new Error((await res.json()).error ?? "خطأ في إلغاء الحظر");
134
  }
135
 
136
  async function banUser(id: string): Promise<void> {
137
+ const headers = await getAuthHeaders();
138
+ const res = await fetch(apiUrl(`admin/ban/${id}`), { method: "POST", headers });
 
 
139
  if (!res.ok) throw new Error((await res.json()).error ?? "خطأ في الحظر");
140
  }
141
 
142
  async function unbanUser(id: string): Promise<void> {
143
+ const headers = await getAuthHeaders();
144
+ const res = await fetch(apiUrl(`admin/unban/${id}`), { method: "POST", headers });
 
 
145
  if (!res.ok) throw new Error((await res.json()).error ?? "خطأ في رفع الحظر");
146
  }
147
 
148
  async function deleteUser(id: string): Promise<void> {
149
+ const headers = await getAuthHeaders();
150
+ const res = await fetch(apiUrl(`admin/users/${id}`), { method: "DELETE", headers });
 
 
151
  if (!res.ok) throw new Error((await res.json()).error ?? "خطأ في الحذف");
152
  }
153
 
154
  async function updateSubscription(userId: string, body: { status?: string; trialEnd?: string | null }): Promise<void> {
155
+ const headers = await getAuthHeaders();
156
  const res = await fetch(apiUrl(`admin/subscription/${userId}`), {
157
  method: "PATCH",
158
+ headers,
 
159
  body: JSON.stringify(body),
160
  });
161
  if (!res.ok) throw new Error((await res.json()).error ?? "خطأ في التحديث");
 
172
  const [blockEmailInput, setBlockEmailInput] = useState("");
173
  const [blockReasonInput, setBlockReasonInput] = useState("");
174
 
 
175
  const [session, setSession] = useState<any>(null);
176
  const [authLoading, setAuthLoading] = useState(true);
177
 
 
180
  setSession(session);
181
  setAuthLoading(false);
182
  });
 
183
  const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
184
  setSession(session);
185
  });
 
186
  return () => subscription.unsubscribe();
187
  }, []);
188
 
 
333
  <div className="flex min-h-screen items-center justify-center bg-[#0f0f1e]">
334
  <div className="text-center space-y-3">
335
  <div className="text-red-400 text-2xl">⛔ غير مصرح بالدخول</div>
336
+ <div className="text-gray-500 text-sm">البريد المسجل: {userEmail}</div>
 
 
337
  </div>
338
  </div>
339
  );
 
358
  size="sm"
359
  onClick={async () => {
360
  if (!confirm("مسح إعدادات PayPal المخزّنة؟ ستُعاد إنشاؤها تلقائياً عند أول اشتراك.")) return;
361
+ const headers = await getAuthHeaders();
362
+ const res = await fetch(apiUrl("admin/paypal-config"), { method: "DELETE", headers });
363
  const data = await res.json();
364
  toast({ title: res.ok ? "✅ تم المسح" : "❌ خطأ", description: data.message ?? data.error });
365
  }}
 
395
  variant="outline"
396
  size="sm"
397
  onClick={() => {
398
+ const emails = (users ?? []).map((u) => u.email).filter(Boolean).join("\n");
399
+ if (!emails) { toast({ title: "لا توجد إيميلات", variant: "destructive" }); return; }
 
 
 
 
 
 
400
  navigator.clipboard.writeText(emails).then(() => {
401
+ toast({ title: `✅ تم النسخ`, description: `${emails.split("\n").length} إيميل في الحافظة` });
 
 
 
402
  });
403
  }}
404
  className="border-blue-700 text-blue-400 hover:bg-blue-900/20 text-xs"
405
  >
406
  📋 نسخ الإيميلات
407
  </Button>
408
+ <Button variant="outline" size="sm" onClick={() => refetch()} className="border-[#3a3a55] text-gray-300 hover:text-white">
 
 
 
 
 
409
  تحديث
410
  </Button>
411
  </div>
 
422
  )}
423
  </div>
424
  <div className="flex gap-2">
425
+ <Button size="sm" disabled={updateExtMutation.isPending} onClick={() => updateExtMutation.mutate({ enabled: true, version: newVersion.trim() || undefined, message: newMessage.trim() || undefined })} className="bg-[#bb86fc] hover:bg-[#a070e0] text-black font-bold text-xs">
 
 
 
 
 
 
 
 
 
426
  📢 إرسال الإشعار للجميع
427
  </Button>
428
+ <Button size="sm" variant="outline" disabled={updateExtMutation.isPending || !updateStatus?.updateAvailable} onClick={() => updateExtMutation.mutate({ enabled: false })} className="border-red-800 text-red-400 hover:bg-red-900/20 text-xs">
 
 
 
 
 
 
429
  ✕ إيقاف الإشعار
430
  </Button>
431
  </div>
432
  </div>
433
  <div className="flex gap-2 flex-wrap">
434
+ <Input placeholder="رقم الإصدار — مثال: 1.3.0" value={newVersion} onChange={(e) => setNewVersion(e.target.value)} className="bg-[#0f0f1e] border-[#3a3a55] text-white placeholder:text-gray-600 text-sm flex-1 min-w-[140px]" />
435
+ <Input placeholder="رسالة اختيارية للمستخدمين" value={newMessage} onChange={(e) => setNewMessage(e.target.value)} className="bg-[#0f0f1e] border-[#3a3a55] text-white placeholder:text-gray-600 text-sm flex-1 min-w-[200px]" />
 
 
 
 
 
 
 
 
 
 
436
  </div>
437
  {updateStatus?.updateAvailable && (
438
  <div className="mt-2 text-xs text-gray-500">
 
445
  <div className="mb-6 rounded-xl border border-[#3a3a55] bg-[#1a1a30] p-4">
446
  <div className="flex items-center gap-2 mb-3">
447
  <span className="text-base font-semibold text-red-400">🚫 البريد الإلكتروني المحظور</span>
448
+ <span className="rounded-full bg-red-900/30 px-2 py-0.5 text-xs text-red-400 border border-red-800">{blockedData?.blockedEmails.length ?? 0}</span>
 
 
449
  </div>
450
  <div className="flex gap-2 flex-wrap mb-3">
451
+ <Input placeholder="بريد إلكتروني للحظر" value={blockEmailInput} onChange={(e) => setBlockEmailInput(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && blockEmailInput.includes("@")) blockMutation.mutate({ email: blockEmailInput, reason: blockReasonInput }); }} className="bg-[#0f0f1e] border-[#3a3a55] text-white placeholder:text-gray-600 text-sm flex-1 min-w-[200px]" />
452
+ <Input placeholder="سبب الحظر (اختياري)" value={blockReasonInput} onChange={(e) => setBlockReasonInput(e.target.value)} className="bg-[#0f0f1e] border-[#3a3a55] text-white placeholder:text-gray-600 text-sm flex-1 min-w-[150px]" />
453
+ <Button size="sm" disabled={blockMutation.isPending || !blockEmailInput.includes("@")} onClick={() => blockMutation.mutate({ email: blockEmailInput, reason: blockReasonInput })} className="bg-red-700 hover:bg-red-800 text-white text-xs font-bold">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
454
  🚫 حظر البريد
455
  </Button>
456
  </div>
457
+ {blockedData?.blockedEmails.length === 0 && <div className="text-xs text-gray-600 text-center py-2">لا توجد بريد محظور</div>}
 
 
458
  {(blockedData?.blockedEmails.length ?? 0) > 0 && (
459
  <div className="space-y-1 max-h-48 overflow-y-auto">
460
  {blockedData!.blockedEmails.map((b) => (
461
  <div key={b.email} className="flex items-center justify-between rounded-lg bg-[#0f0f1e] px-3 py-2 text-sm">
462
+ <div><span className="text-red-300 font-mono text-xs">{b.email}</span>{b.reason && <span className="text-gray-500 text-xs mr-2">— {b.reason}</span>}</div>
463
+ <Button size="sm" variant="outline" disabled={unblockMutation.isPending} onClick={() => unblockMutation.mutate(b.email)} className="text-xs border-gray-700 text-gray-400 hover:text-white h-6 px-2">رفع الحظر</Button>
 
 
 
 
 
 
 
 
 
 
 
464
  </div>
465
  ))}
466
  </div>
467
  )}
468
  </div>
469
  <div className="mb-4">
470
+ <Input placeholder="ابحث بالبريد أو المعرّف..." value={search} onChange={(e) => setSearch(e.target.value)} className="bg-[#1f1f3a] border-[#3a3a55] text-white placeholder:text-gray-500" />
 
 
 
 
 
471
  </div>
472
+ {isLoading && <div className="text-center text-gray-400 py-16">جارٍ تحميل المستخدمين...</div>}
473
+ {error && <div className="text-center text-red-400 py-16">خطأ: {(error as any).message}</div>}
 
 
 
 
 
 
474
  {!isLoading && !error && (
475
  <>
476
+ <div className="text-sm text-gray-500 mb-3">إجمالي المستخدمين: {users.length} | المعروض: {filtered.length}</div>
 
 
477
  <div className="rounded-xl border border-[#3a3a55] overflow-hidden">
478
  <table className="w-full text-sm">
479
  <thead className="bg-[#1f1f3a] text-gray-400">
 
487
  </tr>
488
  </thead>
489
  <tbody>
490
+ {filtered.length === 0 && <tr><td colSpan={5} className="text-center text-gray-500 py-10">لا يوجد مستخدمون</td></tr>}
 
 
 
 
 
 
491
  {filtered.map((u) => {
492
  const sl = statusLabel(u.subscription);
493
  const isMe = u.email?.toLowerCase() === OWNER_EMAIL.toLowerCase();
494
  return (
495
+ <tr key={u.id} style={u.isBanned ? { opacity: 0.7, background: "rgba(234,88,12,0.05)" } : undefined} className="border-t border-[#2a2a45] hover:bg-[#1a1a30] transition-colors">
 
 
 
 
496
  <td className="px-4 py-3">
497
  <div className="font-medium flex items-center gap-2 flex-wrap">
498
  {u.email ?? <span className="text-gray-500">بدون بريد</span>}
499
+ {u.isBanned && <span className="text-xs bg-orange-900/40 text-orange-400 border border-orange-700 rounded px-1.5 py-0.5">🚫 محظور</span>}
 
 
500
  </div>
501
+ {isMe && <span className="text-xs text-[#bb86fc]">أنت (المالك)</span>}
 
 
502
  </td>
503
  <td className="px-4 py-3">
504
  <span className={sl.color}>{sl.text}</span>
505
+ {u.subscription?.trialEnd && u.subscription.status === "trialing" && <div className="text-xs text-gray-500">تنتهي: {formatDate(u.subscription.trialEnd)}</div>}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
506
  </td>
507
+ <td className="px-4 py-3">{u.ipAddress ? <div className="flex flex-col gap-1"><span className="font-mono text-xs text-gray-300 select-all">{u.ipAddress}</span>{!isMe && <button className="text-xs text-red-400 hover:text-red-300 underline text-right" onClick={() => blockMutation.mutate({ email: u.ipAddress!, reason: "حظر بالـ IP" })} disabled={blockMutation.isPending}>🚫 حظر IP</button>}</div> : <span className="text-gray-600 text-xs">—</span>}</td>
508
+ <td className="px-4 py-3 text-gray-400">{formatDate(u.createdAt)}</td>
509
+ <td className="px-4 py-3 text-gray-400">{u.lastSeenAt ? <div><div>{formatDate(u.lastSeenAt)}</div><div className="text-xs text-gray-600">يُحدَّث مرة/يوم</div></div> : <span className="text-gray-600">—</span>}</td>
510
  <td className="px-4 py-3">
511
  <div className="flex flex-col gap-1">
512
+ {(u.subscription?.status === "trialing" || isMe) && <Button size="sm" variant="outline" disabled={updateSubMutation.isPending} onClick={() => updateSubMutation.mutate({ userId: u.id, body: { status: "trial_expired", trialEnd: new Date(Date.now() - 60000).toISOString() } })} className="text-xs border-yellow-700 text-yellow-400 hover:bg-yellow-900/20">⏰ إنهاء التجربة</Button>}
513
+ {u.subscription && u.subscription.status !== "trialing" && <Button size="sm" variant="outline" disabled={updateSubMutation.isPending} onClick={() => updateSubMutation.mutate({ userId: u.id, body: { status: "trialing", trialEnd: new Date(Date.now() + 30 * 24 * 3600 * 1000).toISOString() } })} className="text-xs border-blue-700 text-blue-400 hover:bg-blue-900/20">🔄 إعادة التجربة</Button>}
514
+ {!isMe && (u.isBanned ? <Button size="sm" variant="outline" disabled={unbanMutation.isPending} onClick={() => unbanMutation.mutate(u.id)} className="text-xs border-green-700 text-green-400 hover:bg-green-900/20">{unbanMutation.isPending ? "⏳" : "✅ رفع الحظر"}</Button> : <Button size="sm" variant="outline" disabled={banMutation.isPending} onClick={() => { if (!confirm(`حظر ${u.email ?? u.id}؟ يمكن فك الحظر لاحقاً.`)) return; banMutation.mutate(u.id); }} className="text-xs border-orange-700 text-orange-400 hover:bg-orange-900/20">{banMutation.isPending ? "⏳" : "🚫 حظر الحساب"}</Button>)}
515
+ {!isMe && confirmId === u.id ? <div className="flex gap-1"><Button size="sm" variant="destructive" onClick={() => deleteMutation.mutate(u.id)} disabled={deleteMutation.isPending} className="text-xs">{deleteMutation.isPending ? "جارٍ..." : "تأكيد"}</Button><Button size="sm" variant="outline" onClick={() => setConfirmId(null)} className="text-xs border-[#3a3a55] text-gray-300">إلغاء</Button></div> : !isMe && <Button size="sm" variant="outline" onClick={() => setConfirmId(u.id)} className="text-xs border-red-800 text-red-400 hover:bg-red-900/20">🗑️ حظر وحذف البيانات</Button>}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
516
  </div>
517
  </td>
518
  </tr>