Files
image-apiv3/app/admin/page.tsx
Beyhan Oğur 031582ea2c first commit
2026-04-26 22:11:03 +03:00

620 lines
22 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 { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import Swal from "sweetalert2";
interface User {
id: string;
name: string | null;
email: string;
role: string;
emailVerified: boolean;
createdAt: string;
}
interface AdminApiKeyRow {
id: string;
name: string;
keyPreview: string;
expiresAt: string | null;
daysRemaining: number | null;
remainingLabel: string;
lastUsedAt: string | null;
isActive: boolean;
createdAt: string;
}
interface Session {
user: {
id: string;
email: string;
name?: string;
role?: string;
};
}
export default function AdminPanel() {
const router = useRouter();
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [currentUserRole, setCurrentUserRole] = useState<string>("");
const [keysModalUser, setKeysModalUser] = useState<User | null>(null);
const [userKeys, setUserKeys] = useState<AdminApiKeyRow[]>([]);
const [keysLoading, setKeysLoading] = useState(false);
const [keysPatching, setKeysPatching] = useState<string | null>(null);
const [expiryDraft, setExpiryDraft] = useState<Record<string, string>>({});
useEffect(() => {
checkAuth();
}, []);
const checkAuth = async () => {
try {
const res = await fetch("/api/auth/get-session", {
credentials: "include",
});
if (!res.ok) {
router.push("/login");
return;
}
const session: Session = await res.json();
if (!session.user || session.user.role !== "admin") {
await Swal.fire({
icon: "error",
title: "Erişim Engellendi",
text: "Bu sayfaya erişim yetkiniz yok. Sadece adminler görebilir.",
confirmButtonColor: "#3b82f6",
});
router.push("/");
return;
}
setCurrentUserRole(session.user.role || "user");
await fetchUsers();
} catch (err) {
router.push("/login");
}
};
const fetchUsers = async () => {
setLoading(true);
try {
const res = await fetch("/api/admin/users", {
credentials: "include",
});
if (!res.ok) {
if (res.status === 401) {
router.push("/login");
return;
}
if (res.status === 403) {
await Swal.fire({
icon: "error",
title: "Yetki Hatası",
text: "Bu sayfaya erişim yetkiniz yok.",
confirmButtonColor: "#3b82f6",
});
return;
}
throw new Error("Kullanıcılar yüklenemedi");
}
const data = await res.json();
setUsers(data.data.users);
} catch (err: any) {
await Swal.fire({
icon: "error",
title: "Hata",
text: err.message,
confirmButtonColor: "#3b82f6",
});
} finally {
setLoading(false);
}
};
const changeRole = async (userId: string, newRole: string, currentRole: string) => {
const result = await Swal.fire({
title: "Rol Değiştir",
text: `Bu kullanıcının rolünü "${currentRole}" → "${newRole}" olarak değiştirmek istediğinizden emin misiniz?`,
icon: "question",
showCancelButton: true,
confirmButtonColor: "#3b82f6",
cancelButtonColor: "#6b7280",
confirmButtonText: "Evet, değiştir",
cancelButtonText: "İptal",
});
if (!result.isConfirmed) {
await fetchUsers();
return;
}
try {
const res = await fetch(`/api/admin/users/${userId}/role`, {
method: "PATCH",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ role: newRole }),
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.error || "Rol güncellenemedi");
}
await Swal.fire({
icon: "success",
title: "Başarılı!",
text: "Kullanıcı rolü güncellendi",
timer: 2000,
showConfirmButton: false,
});
await fetchUsers();
} catch (err: any) {
await Swal.fire({
icon: "error",
title: "Hata",
text: err.message,
confirmButtonColor: "#3b82f6",
});
await fetchUsers();
}
};
const toggleEmailVerification = async (userId: string, currentStatus: boolean, email: string) => {
const newStatus = !currentStatus;
const result = await Swal.fire({
title: "Email Doğrulama",
text: `${email} için email doğrulamasını ${newStatus ? "aktif" : "pasif"} yapmak istiyor musunuz?`,
icon: "question",
showCancelButton: true,
confirmButtonColor: "#3b82f6",
cancelButtonColor: "#6b7280",
confirmButtonText: "Evet, değiştir",
cancelButtonText: "İptal",
});
if (!result.isConfirmed) {
return;
}
try {
const res = await fetch(`/api/admin/users/${userId}/verification`, {
method: "PATCH",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ emailVerified: newStatus }),
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.error || "Doğrulama güncellenemedi");
}
await Swal.fire({
icon: "success",
title: "Başarılı!",
text: `Email doğrulama ${newStatus ? "aktif edildi" : "pasif edildi"}`,
timer: 2000,
showConfirmButton: false,
});
await fetchUsers();
} catch (err: any) {
await Swal.fire({
icon: "error",
title: "Hata",
text: err.message,
confirmButtonColor: "#3b82f6",
});
}
};
const fetchUserKeysList = async (userId: string) => {
setKeysLoading(true);
try {
const res = await fetch(`/api/v1/admin/users/${userId}/api-keys`, {
credentials: "include",
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || "Anahtarlar yüklenemedi");
}
setUserKeys(data.data?.keys || []);
} finally {
setKeysLoading(false);
}
};
const openApiKeysModal = async (u: User) => {
setKeysModalUser(u);
setUserKeys([]);
setExpiryDraft({});
try {
await fetchUserKeysList(u.id);
} catch (err: any) {
await Swal.fire({
icon: "error",
title: "Hata",
text: err.message,
confirmButtonColor: "#3b82f6",
});
setKeysModalUser(null);
}
};
const patchKeyExpiry = async (userId: string, keyId: string, draft: string) => {
let expiresInDays: number | null;
const t = draft.trim();
if (t === "") {
expiresInDays = null;
} else {
const n = parseInt(t, 10);
if (!Number.isFinite(n) || n < 0) {
await Swal.fire({
icon: "warning",
title: "Geçersiz",
text: "Gün sayısı boş (süresiz) veya 0 veya pozitif tam sayı olmalı.",
confirmButtonColor: "#3b82f6",
});
return;
}
expiresInDays = n === 0 ? null : n;
}
setKeysPatching(keyId);
try {
const res = await fetch(
`/api/v1/admin/users/${userId}/api-keys/${keyId}`,
{
method: "PATCH",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ expiresInDays }),
}
);
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || "Güncellenemedi");
}
await Swal.fire({
icon: "success",
title: "Güncellendi",
text: "API anahtarı süresi kaydedildi.",
timer: 1800,
showConfirmButton: false,
});
setExpiryDraft((prev) => {
const next = { ...prev };
delete next[keyId];
return next;
});
await fetchUserKeysList(userId);
} catch (err: any) {
await Swal.fire({
icon: "error",
title: "Hata",
text: err.message,
confirmButtonColor: "#3b82f6",
});
} finally {
setKeysPatching(null);
}
};
const deleteUser = async (userId: string, email: string) => {
const result = await Swal.fire({
title: "Kullanıcıyı Sil",
html: `<b>${email}</b> kullanıcısını silmek istediğinizden emin misiniz?<br><br>
<span style="color: #ef4444; font-weight: 600;">⚠️ Bu işlem geri alınamaz!</span><br>
<small>Kullanıcının tüm resimleri ve verileri silinecek.</small>`,
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#ef4444",
cancelButtonColor: "#6b7280",
confirmButtonText: "Evet, sil!",
cancelButtonText: "İptal",
});
if (!result.isConfirmed) {
return;
}
try {
const res = await fetch(`/api/admin/users/${userId}`, {
method: "DELETE",
credentials: "include",
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.error || "Kullanıcı silinemedi");
}
await Swal.fire({
icon: "success",
title: "Silindi!",
text: "Kullanıcı başarıyla silindi",
timer: 2000,
showConfirmButton: false,
});
await fetchUsers();
} catch (err: any) {
await Swal.fire({
icon: "error",
title: "Hata",
text: err.message,
confirmButtonColor: "#3b82f6",
});
}
};
const getRoleBadgeColor = (role: string) => {
switch (role) {
case "admin":
return "bg-red-100 text-red-800 border-red-200";
case "moderator":
return "bg-blue-100 text-blue-800 border-blue-200";
default:
return "bg-gray-100 text-gray-800 border-gray-200";
}
};
if (loading) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Yükleniyor...</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 p-8">
<div className="max-w-7xl mx-auto">
<div className="mb-8">
<h1 className="text-4xl font-bold text-gray-900 mb-2">Admin Panel</h1>
<p className="text-gray-600">Kullanıcı yönetimi ve rol atama</p>
</div>
<div className="bg-white rounded-2xl shadow-xl overflow-hidden border border-gray-100">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gradient-to-r from-blue-600 to-indigo-600 text-white">
<tr>
<th className="px-6 py-4 text-left text-sm font-semibold">Kullanıcı</th>
<th className="px-6 py-4 text-left text-sm font-semibold">Email</th>
<th className="px-6 py-4 text-left text-sm font-semibold">Rol</th>
<th className="px-6 py-4 text-left text-sm font-semibold">Doğrulama</th>
<th className="px-6 py-4 text-left text-sm font-semibold">Kayıt Tarihi</th>
<th className="px-6 py-4 text-left text-sm font-semibold">İşlemler</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{users.map((user) => (
<tr key={user.id} className="hover:bg-gray-50 transition-colors">
<td className="px-6 py-4">
<div className="font-medium text-gray-900">
{user.name || "İsimsiz"}
</div>
<div className="text-xs text-gray-500">{user.id.substring(0, 8)}</div>
</td>
<td className="px-6 py-4 text-gray-700">{user.email}</td>
<td className="px-6 py-4">
<select
value={user.role}
onChange={(e) => changeRole(user.id, e.target.value, user.role)}
className={`px-3 py-1 rounded-full text-sm font-medium border ${getRoleBadgeColor(
user.role
)} focus:outline-none focus:ring-2 focus:ring-blue-500 cursor-pointer transition-all hover:shadow-md`}
>
<option value="user">User</option>
<option value="moderator">Moderator</option>
<option value="admin">Admin</option>
</select>
</td>
<td className="px-6 py-4">
<button
onClick={() => toggleEmailVerification(user.id, user.emailVerified, user.email)}
className={`inline-flex items-center gap-1 px-3 py-1.5 rounded-full text-xs font-medium transition-all cursor-pointer hover:shadow-md ${
user.emailVerified
? "bg-green-100 text-green-700 hover:bg-green-200"
: "bg-yellow-100 text-yellow-700 hover:bg-yellow-200"
}`}
>
{user.emailVerified ? (
<>
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
Doğrulandı
</>
) : (
<>
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path
fillRule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clipRule="evenodd"
/>
</svg>
Beklemede
</>
)}
</button>
</td>
<td className="px-6 py-4 text-sm text-gray-600">
{new Date(user.createdAt).toLocaleDateString("tr-TR")}
</td>
<td className="px-6 py-4">
<div className="flex flex-wrap gap-2">
<button
type="button"
onClick={() => openApiKeysModal(user)}
className="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-sm font-medium transition-colors"
>
API Keys
</button>
<button
type="button"
onClick={() => deleteUser(user.id, user.email)}
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg text-sm font-medium transition-colors"
>
Sil
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{keysModalUser && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
<div className="max-h-[90vh] w-full max-w-3xl overflow-y-auto rounded-2xl bg-white p-6 shadow-2xl">
<div className="mb-4 flex items-start justify-between gap-4">
<div>
<h2 className="text-xl font-bold text-gray-900">API anahtarları</h2>
<p className="text-sm text-gray-600">
{keysModalUser.email} süre: bugünden itibaren gün sayısı (boş veya 0 =
süresiz)
</p>
</div>
<button
type="button"
onClick={() => setKeysModalUser(null)}
className="rounded-lg px-3 py-1 text-sm text-gray-600 hover:bg-gray-100"
>
Kapat
</button>
</div>
{keysLoading ? (
<p className="text-gray-600">Yükleniyor</p>
) : userKeys.length === 0 ? (
<p className="text-gray-600">Bu kullanıcının henüz API anahtarı yok.</p>
) : (
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-gray-200">
<th className="py-2 pr-3">İsim</th>
<th className="py-2 pr-3">Önizleme</th>
<th className="py-2 pr-3">Mevcut bitiş</th>
<th className="py-2 pr-3">Kalan süre</th>
<th className="py-2 pr-3">Yeni süre (gün)</th>
<th className="py-2" />
</tr>
</thead>
<tbody>
{userKeys.map((k) => (
<tr key={k.id} className="border-b border-gray-100">
<td className="py-2 pr-3 font-medium">{k.name}</td>
<td className="py-2 pr-3 font-mono text-xs">{k.keyPreview}</td>
<td className="py-2 pr-3 text-xs">
{k.expiresAt
? new Date(k.expiresAt).toLocaleString("tr-TR")
: "—"}
</td>
<td className="py-2 pr-3">
<span
className={
k.remainingLabel === "Süresi doldu"
? "font-medium text-red-600"
: k.remainingLabel === "Süresiz"
? "text-gray-600"
: "font-medium text-emerald-700"
}
>
{k.remainingLabel}
</span>
</td>
<td className="py-2 pr-3">
<input
type="number"
min={0}
placeholder="0=süresiz"
value={expiryDraft[k.id] ?? ""}
onChange={(e) =>
setExpiryDraft((prev) => ({
...prev,
[k.id]: e.target.value,
}))
}
className="w-28 rounded border border-gray-300 px-2 py-1 text-sm"
/>
</td>
<td className="py-2">
<button
type="button"
disabled={keysPatching === k.id}
onClick={() =>
patchKeyExpiry(
keysModalUser.id,
k.id,
expiryDraft[k.id] ?? ""
)
}
className="rounded bg-blue-600 px-3 py-1 text-xs font-medium text-white hover:bg-blue-700 disabled:opacity-50"
>
{keysPatching === k.id ? "…" : "Kaydet"}
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
)}
<div className="mt-8 grid grid-cols-1 md:grid-cols-4 gap-6">
<div className="bg-white p-6 rounded-xl shadow-lg border border-gray-100">
<h3 className="text-sm font-medium text-gray-600 mb-2">Toplam Kullanıcı</h3>
<p className="text-3xl font-bold text-gray-900">{users.length}</p>
</div>
<div className="bg-white p-6 rounded-xl shadow-lg border border-gray-100">
<h3 className="text-sm font-medium text-gray-600 mb-2">Admin Sayısı</h3>
<p className="text-3xl font-bold text-red-600">
{users.filter((u) => u.role === "admin").length}
</p>
</div>
<div className="bg-white p-6 rounded-xl shadow-lg border border-gray-100">
<h3 className="text-sm font-medium text-gray-600 mb-2">Moderatör Sayısı</h3>
<p className="text-3xl font-bold text-blue-600">
{users.filter((u) => u.role === "moderator").length}
</p>
</div>
<div className="bg-white p-6 rounded-xl shadow-lg border border-gray-100">
<h3 className="text-sm font-medium text-gray-600 mb-2">Doğrulananlar</h3>
<p className="text-3xl font-bold text-green-600">
{users.filter((u) => u.emailVerified).length}
</p>
</div>
</div>
</div>
</div>
);
}