diff --git a/backend/src/routes/repositoryRoutes.js b/backend/src/routes/repositoryRoutes.js index 6763d18..a1c0b10 100644 --- a/backend/src/routes/repositoryRoutes.js +++ b/backend/src/routes/repositoryRoutes.js @@ -289,6 +289,77 @@ router.get( }, ); +// Delete a specific repository (admin only) +router.delete( + "/:repositoryId", + authenticateToken, + requireManageHosts, + async (req, res) => { + try { + const { repositoryId } = req.params; + + // Check if repository exists first + const existingRepository = await prisma.repositories.findUnique({ + where: { id: repositoryId }, + select: { + id: true, + name: true, + url: true, + _count: { + select: { + host_repositories: true, + }, + }, + }, + }); + + if (!existingRepository) { + return res.status(404).json({ + error: "Repository not found", + details: "The repository may have been deleted or does not exist", + }); + } + + // Delete repository and all related data (cascade will handle host_repositories) + await prisma.repositories.delete({ + where: { id: repositoryId }, + }); + + res.json({ + message: "Repository deleted successfully", + deletedRepository: { + id: existingRepository.id, + name: existingRepository.name, + url: existingRepository.url, + hostCount: existingRepository._count.host_repositories, + }, + }); + } catch (error) { + console.error("Repository deletion error:", error); + + // Handle specific Prisma errors + if (error.code === "P2025") { + return res.status(404).json({ + error: "Repository not found", + details: "The repository may have been deleted or does not exist", + }); + } + + if (error.code === "P2003") { + return res.status(400).json({ + error: "Cannot delete repository due to foreign key constraints", + details: "The repository has related data that prevents deletion", + }); + } + + res.status(500).json({ + error: "Failed to delete repository", + details: error.message || "An unexpected error occurred", + }); + } + }, +); + // Cleanup orphaned repositories (admin only) router.delete( "/cleanup/orphaned", diff --git a/frontend/src/pages/Repositories.jsx b/frontend/src/pages/Repositories.jsx index 88b0fd5..812b2f0 100644 --- a/frontend/src/pages/Repositories.jsx +++ b/frontend/src/pages/Repositories.jsx @@ -1,4 +1,4 @@ -import { useQuery } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { AlertTriangle, ArrowDown, @@ -7,7 +7,6 @@ import { Check, Columns, Database, - Eye, GripVertical, Lock, RefreshCw, @@ -15,20 +14,24 @@ import { Server, Shield, ShieldCheck, + Trash2, Unlock, X, } from "lucide-react"; import { useMemo, useState } from "react"; -import { Link } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import { repositoryAPI } from "../utils/api"; const Repositories = () => { + const queryClient = useQueryClient(); + const navigate = useNavigate(); const [searchTerm, setSearchTerm] = useState(""); const [filterType, setFilterType] = useState("all"); // all, secure, insecure const [filterStatus, setFilterStatus] = useState("all"); // all, active, inactive const [sortField, setSortField] = useState("name"); const [sortDirection, setSortDirection] = useState("asc"); const [showColumnSettings, setShowColumnSettings] = useState(false); + const [deleteModalData, setDeleteModalData] = useState(null); // Column configuration const [columnConfig, setColumnConfig] = useState(() => { @@ -79,6 +82,15 @@ const Repositories = () => { queryFn: () => repositoryAPI.getStats().then((res) => res.data), }); + // Delete repository mutation + const deleteRepositoryMutation = useMutation({ + mutationFn: (repositoryId) => repositoryAPI.delete(repositoryId), + onSuccess: () => { + queryClient.invalidateQueries(["repositories"]); + queryClient.invalidateQueries(["repository-stats"]); + }, + }); + // Get visible columns in order const visibleColumns = columnConfig .filter((col) => col.visible) @@ -137,6 +149,32 @@ const Repositories = () => { updateColumnConfig(defaultConfig); }; + const handleDeleteRepository = (repo, e) => { + e.preventDefault(); + e.stopPropagation(); + + setDeleteModalData({ + id: repo.id, + name: repo.name, + hostCount: repo.hostCount || 0, + }); + }; + + const handleRowClick = (repo) => { + navigate(`/repositories/${repo.id}`); + }; + + const confirmDelete = () => { + if (deleteModalData) { + deleteRepositoryMutation.mutate(deleteModalData.id); + setDeleteModalData(null); + } + }; + + const cancelDelete = () => { + setDeleteModalData(null); + }; + // Filter and sort repositories const filteredAndSortedRepositories = useMemo(() => { if (!repositories) return []; @@ -224,6 +262,56 @@ const Repositories = () => { return (
+ Are you sure you want to delete{" "} + "{deleteModalData.name}"? +
+ {deleteModalData.hostCount > 0 && ( ++ ⚠️ This repository is currently assigned to{" "} + {deleteModalData.hostCount} host + {deleteModalData.hostCount !== 1 ? "s" : ""}. +
+ )} ++ This action cannot be undone. +
++ Are you sure you want to delete{" "} + "{repository?.name}"? +
+ {repository?.host_repositories?.length > 0 && ( ++ ⚠️ This repository is currently assigned to{" "} + {repository.host_repositories.length} host + {repository.host_repositories.length !== 1 ? "s" : ""}. +
+ )} ++ This action cannot be undone. +
+