402 lines
14 KiB
TypeScript
402 lines
14 KiB
TypeScript
"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 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>("");
|
||
|
||
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 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">
|
||
<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>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</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>
|
||
);
|
||
}
|