first commit
This commit is contained in:
178
app/(admin)/admin/cors/page.tsx
Normal file
178
app/(admin)/admin/cors/page.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from "@/components/ui/breadcrumb";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { useAppDispatch, useAppSelector } from "@/lib/hooks";
|
||||
import {
|
||||
fetchWhitelists,
|
||||
fetchBlacklists,
|
||||
createWhitelist,
|
||||
createBlacklist,
|
||||
deleteWhitelist,
|
||||
deleteBlacklist,
|
||||
updateWhitelist,
|
||||
updateBlacklist,
|
||||
CorsEntry,
|
||||
} from "@/lib/features/cors/corsSlice";
|
||||
import { CorsTable } from "@/components/cors/cors-table";
|
||||
import { CorsDialog } from "@/components/cors/cors-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Plus } from "lucide-react";
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
export default function CorsPage() {
|
||||
const dispatch = useAppDispatch();
|
||||
const { whitelist, blacklist } = useAppSelector((state) => state.cors);
|
||||
const [activeTab, setActiveTab] = useState<"whitelist" | "blacklist">("whitelist");
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
// Dialog state
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [editingEntry, setEditingEntry] = useState<CorsEntry | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
dispatch(fetchWhitelists());
|
||||
dispatch(fetchBlacklists());
|
||||
}, [dispatch]);
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
Swal.fire({
|
||||
title: 'Emin misiniz?',
|
||||
text: "Bu kaydı silmek istediğinize emin misiniz?",
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#3085d6',
|
||||
cancelButtonColor: '#d33',
|
||||
confirmButtonText: 'Evet, sil!',
|
||||
cancelButtonText: 'İptal'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
if (activeTab === "whitelist") {
|
||||
dispatch(deleteWhitelist(id)).then(() => {
|
||||
Swal.fire('Silindi!', 'Kayıt başarıyla silindi.', 'success');
|
||||
});
|
||||
} else {
|
||||
dispatch(deleteBlacklist(id)).then(() => {
|
||||
Swal.fire('Silindi!', 'Kayıt başarıyla silindi.', 'success');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleToggleActive = (id: string, currentStatus: boolean) => {
|
||||
const data = { is_active: !currentStatus };
|
||||
if (activeTab === "whitelist") {
|
||||
dispatch(updateWhitelist({ id, data }));
|
||||
} else {
|
||||
dispatch(updateBlacklist({ id, data }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (entry: CorsEntry) => {
|
||||
setEditingEntry(entry);
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleAddClick = () => {
|
||||
setEditingEntry(null);
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDialogSubmit = async (origin: string, note: string) => {
|
||||
if (editingEntry) {
|
||||
// Update existing
|
||||
const data: Partial<CorsEntry> = activeTab === "whitelist"
|
||||
? { origin, description: note }
|
||||
: { origin, reason: note };
|
||||
|
||||
if (activeTab === "whitelist") {
|
||||
await dispatch(updateWhitelist({ id: editingEntry.id, data })).unwrap();
|
||||
} else {
|
||||
await dispatch(updateBlacklist({ id: editingEntry.id, data })).unwrap();
|
||||
}
|
||||
Swal.fire('Güncellendi!', 'Kayıt başarıyla güncellendi.', 'success');
|
||||
} else {
|
||||
// Create new
|
||||
if (activeTab === "whitelist") {
|
||||
await dispatch(createWhitelist({ origin, description: note })).unwrap();
|
||||
} else {
|
||||
await dispatch(createBlacklist({ origin, reason: note })).unwrap();
|
||||
}
|
||||
Swal.fire('Eklendi!', 'Yeni kayıt başarıyla eklendi.', 'success');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Header / Breadcrumb Section */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem className="hidden md:block">
|
||||
<BreadcrumbLink href="/admin">Admin</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator className="hidden md:block" />
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>CORS Ayarları</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Actions & Dialog */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-x-2">
|
||||
<Button
|
||||
variant={activeTab === "whitelist" ? "default" : "outline"}
|
||||
onClick={() => setActiveTab("whitelist")}
|
||||
>
|
||||
Whitelist
|
||||
</Button>
|
||||
<Button
|
||||
variant={activeTab === "blacklist" ? "default" : "outline"}
|
||||
onClick={() => setActiveTab("blacklist")}
|
||||
>
|
||||
Blacklist
|
||||
</Button>
|
||||
</div>
|
||||
<Button onClick={handleAddClick}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Yeni Ekle
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<CorsDialog
|
||||
open={dialogOpen}
|
||||
onOpenChange={setDialogOpen}
|
||||
type={activeTab}
|
||||
entry={editingEntry}
|
||||
onSubmit={handleDialogSubmit}
|
||||
/>
|
||||
|
||||
{/* Table */}
|
||||
<div className="rounded-xl border bg-card">
|
||||
<CorsTable
|
||||
data={activeTab === "whitelist" ? whitelist : blacklist}
|
||||
type={activeTab}
|
||||
onDelete={handleDelete}
|
||||
onEdit={handleEdit}
|
||||
onToggleActive={handleToggleActive}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
61
app/(admin)/admin/page.tsx
Normal file
61
app/(admin)/admin/page.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
import { useAppSelector } from "@/lib/hooks";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { getAvatarUrl } from "@/lib/utils";
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { user } = useAppSelector((state) => state.auth);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium">Hoşgeldin, {user?.username}</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Admin paneli kontrol merkezi.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Toplam Kullanıcı</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">128</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
+4% geçen aydan beri
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Aktif Roller</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
{user?.roles?.map(r => r.name).join(", ") || "Yok"}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Profil Bilgileri</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex items-center gap-4">
|
||||
<Avatar className="h-20 w-20">
|
||||
<AvatarImage src={getAvatarUrl(user?.avatar_url)} />
|
||||
<AvatarFallback>{user?.username?.slice(0, 2).toUpperCase()}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<div className="font-semibold text-lg">{user?.username}</div>
|
||||
<div className="text-muted-foreground">{user?.email}</div>
|
||||
<div className="text-xs text-muted-foreground mt-1">ID: {user?.id}</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
223
app/(admin)/admin/users/page.tsx
Normal file
223
app/(admin)/admin/users/page.tsx
Normal file
@@ -0,0 +1,223 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useAppDispatch, useAppSelector } from "@/lib/hooks";
|
||||
import { fetchUsers, fetchDeletedUsers, createUser, updateUser, deleteUser, restoreUser, CreateUserRequest, UpdateUserRequest } from "@/lib/features/users/usersSlice";
|
||||
import { UserTable } from "@/components/users/user-table";
|
||||
import { DeletedUserTable } from "@/components/users/deleted-user-table";
|
||||
import { UserDialog } from "@/components/users/user-dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Plus, Trash, Trash2 } from "lucide-react";
|
||||
import Swal from "sweetalert2";
|
||||
import { User } from "@/lib/features/auth/authSlice";
|
||||
|
||||
export default function UsersPage() {
|
||||
const dispatch = useAppDispatch();
|
||||
const { users, deletedUsers, isLoading, error } = useAppSelector((state) => state.users);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchUsers());
|
||||
dispatch(fetchDeletedUsers());
|
||||
}, [dispatch]);
|
||||
|
||||
const handleCreate = () => {
|
||||
setSelectedUser(null);
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleEdit = (user: User) => {
|
||||
setSelectedUser(user);
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
Swal.fire({
|
||||
title: "Soft Delete",
|
||||
text: "Bu kullanıcı 'silindi' olarak işaretlenecek (Soft Delete). Geri alınabilir.",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#f97316", // Orange
|
||||
cancelButtonColor: "#3085d6",
|
||||
confirmButtonText: "Evet, Sil",
|
||||
cancelButtonText: "İptal",
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
dispatch(deleteUser(id))
|
||||
.unwrap()
|
||||
.then(() => {
|
||||
Swal.fire("Silindi!", "Kullanıcı başarıyla silindi (Soft).", "success");
|
||||
// Refresh both lists because a user moved from active to deleted
|
||||
dispatch(fetchUsers());
|
||||
dispatch(fetchDeletedUsers());
|
||||
})
|
||||
.catch((err) => {
|
||||
Swal.fire("Hata!", err || "Silme işlemi başarısız.", "error");
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleHardDelete = (id: string) => {
|
||||
Swal.fire({
|
||||
title: "KALICI OLARAK SİL?",
|
||||
text: "Bu işlem GERİ ALINAMAZ! Kullanıcı ve tüm verileri veritabanından tamamen silinecek (Hard Delete).",
|
||||
icon: "error", // Red
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#d33", // Red
|
||||
cancelButtonColor: "#3085d6",
|
||||
confirmButtonText: "Evet, KALICI SİL!",
|
||||
cancelButtonText: "İptal",
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
const isSoftDeleted = deletedUsers.some(u => u.id === id);
|
||||
|
||||
if (isSoftDeleted) {
|
||||
// WORKAROUND: Backend soft-deleted kullanıcıları bulamıyor olabilir.
|
||||
// Önce restore et, sonra hard delete yap.
|
||||
dispatch(restoreUser(id))
|
||||
.unwrap()
|
||||
.then(() => {
|
||||
return dispatch(deleteUser({ id, hard: true })).unwrap();
|
||||
})
|
||||
.then(() => {
|
||||
Swal.fire("Silindi!", "Kullanıcı kalıcı olarak silindi.", "success");
|
||||
})
|
||||
.catch((err) => {
|
||||
Swal.fire("Hata!", err || "Silme işlemi başarısız.", "error");
|
||||
});
|
||||
} else {
|
||||
// Normal Hard Delete (Aktif kullanıcı)
|
||||
dispatch(deleteUser({ id, hard: true }))
|
||||
.unwrap()
|
||||
.then(() => {
|
||||
Swal.fire("Silindi!", "Kullanıcı kalıcı olarak silindi.", "success");
|
||||
})
|
||||
.catch((err) => {
|
||||
Swal.fire("Hata!", err || "Silme işlemi başarısız.", "error");
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleRestore = (id: string) => {
|
||||
Swal.fire({
|
||||
title: "Geri Yükle?",
|
||||
text: "Kullanıcı tekrar aktif edilecek.",
|
||||
icon: "question",
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: "#16a34a", // Green
|
||||
cancelButtonColor: "#d33",
|
||||
confirmButtonText: "Evet, Geri Yükle",
|
||||
cancelButtonText: "İptal",
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
dispatch(restoreUser(id))
|
||||
.unwrap()
|
||||
.then(() => {
|
||||
Swal.fire("Başarılı!", "Kullanıcı geri yüklendi.", "success");
|
||||
dispatch(fetchUsers());
|
||||
dispatch(fetchDeletedUsers());
|
||||
})
|
||||
.catch((err) => {
|
||||
Swal.fire("Hata!", err || "Geri yükleme başarısız.", "error");
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = (data: CreateUserRequest | UpdateUserRequest) => {
|
||||
if ("id" in data) {
|
||||
dispatch(updateUser(data as UpdateUserRequest))
|
||||
.unwrap()
|
||||
.then(() => {
|
||||
setDialogOpen(false);
|
||||
Swal.fire("Başarılı", "Kullanıcı güncellendi.", "success");
|
||||
dispatch(fetchUsers());
|
||||
})
|
||||
.catch((err) => {
|
||||
Swal.fire("Hata", err || "Güncelleme başarısız.", "error");
|
||||
});
|
||||
} else {
|
||||
dispatch(createUser(data as CreateUserRequest))
|
||||
.unwrap()
|
||||
.then(() => {
|
||||
setDialogOpen(false);
|
||||
Swal.fire("Başarılı", "Kullanıcı oluşturuldu.", "success");
|
||||
dispatch(fetchUsers());
|
||||
})
|
||||
.catch((err) => {
|
||||
Swal.fire("Hata", err || "Oluşturma başarısız.", "error");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-12">
|
||||
{/* Active Users Section */}
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-md font-bold tracking-tight">Kullanıcılar</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Sistemdeki aktif kullanıcıları yönetin.
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={() => { setSelectedUser(null); setDialogOpen(true); }}>
|
||||
<Plus className="mr-2 h-4 w-4" /> Yeni Kullanıcı
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-md bg-destructive/15 p-4 text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<UserTable
|
||||
users={users}
|
||||
onEdit={(u) => { setSelectedUser(u); setDialogOpen(true); }}
|
||||
onDelete={handleDelete}
|
||||
onHardDelete={handleHardDelete}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Deleted Users Section */}
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold tracking-tight text-destructive flex items-center gap-2">
|
||||
<Trash2 className="h-6 w-6" /> Çöp Kutusu
|
||||
</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Silinmiş kullanıcıları geri yükleyin veya kalıcı olarak silin.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<DeletedUserTable
|
||||
users={deletedUsers}
|
||||
onRestore={handleRestore}
|
||||
onHardDelete={handleHardDelete}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<UserDialog
|
||||
open={dialogOpen}
|
||||
onOpenChange={setDialogOpen}
|
||||
user={selectedUser}
|
||||
onSubmit={(data) => {
|
||||
// Re-implementing handleSubmit logic inline or keep it separate as before if preferred,
|
||||
// but for brevity I'll assume the previous handleSubmit function is available in scope
|
||||
// or I should include it in replacement if I am replacing the whole return block.
|
||||
// Ideally, I should keep the existing handleSubmit and just pass it.
|
||||
// Since I am replacing the whole return, I must ensure handleSubmit is defined above or passed correctly.
|
||||
// Wait, I am replacing a range that includes imports and component setup? No, just the function body?
|
||||
// Let's modify the instruction to be safer.
|
||||
handleSubmit(data);
|
||||
}}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
83
app/(admin)/layout.tsx
Normal file
83
app/(admin)/layout.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { useAppSelector } from "@/lib/hooks";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import Cookies from "js-cookie";
|
||||
import { SidebarProvider, SidebarInset, SidebarTrigger } from "@/components/ui/sidebar";
|
||||
import { AppSidebar } from "@/components/app-sidebar";
|
||||
|
||||
export default function AdminLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { isAuthenticated, isLoading, user } = useAppSelector((state) => state.auth);
|
||||
const router = useRouter();
|
||||
const [authorized, setAuthorized] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// If we're not loading and not authenticated, redirect
|
||||
// We wait for initial restoration to complete (managed by StoreProvider and authSlice)
|
||||
const checkAuth = () => {
|
||||
const token = Cookies.get("access_token");
|
||||
if (!token) {
|
||||
router.push("/login");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get user from state or localStorage
|
||||
let currentUser = user;
|
||||
if (!currentUser && typeof window !== 'undefined') {
|
||||
const userStr = localStorage.getItem("user");
|
||||
if (userStr) {
|
||||
try {
|
||||
currentUser = JSON.parse(userStr);
|
||||
} catch (e) { }
|
||||
}
|
||||
}
|
||||
|
||||
if (currentUser) {
|
||||
const isAdmin = currentUser.roles?.some((r: any) => r.name === "admin");
|
||||
if (isAdmin) {
|
||||
setAuthorized(true);
|
||||
} else {
|
||||
// Redirect non-admins to home page
|
||||
router.push("/");
|
||||
}
|
||||
} else {
|
||||
// Token exists but user data is missing/loading.
|
||||
// If strictly protected, redirect to login.
|
||||
router.push("/login");
|
||||
}
|
||||
};
|
||||
checkAuth();
|
||||
}, [isAuthenticated, user, router]);
|
||||
|
||||
if (!authorized) {
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarProvider style={{ "--sidebar-width": "16rem" } as React.CSSProperties}>
|
||||
<AppSidebar />
|
||||
<SidebarInset>
|
||||
<header className="flex h-14 shrink-0 items-center gap-2 px-4">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<div className="h-4 w-[1px] bg-slate-200 dark:bg-slate-800" />
|
||||
<span className="text-sm font-medium">Yönetim Paneli</span>
|
||||
</header>
|
||||
|
||||
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
|
||||
{children}
|
||||
</div>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user