first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 22:16:43 +03:00
commit 6d95e27114
97 changed files with 15687 additions and 0 deletions

View 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>
);
}

View 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>
);
}

View 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>
);
}