mirror of
https://github.com/freedbygrace/ActiveDirectoryManager.git
synced 2026-07-26 11:59:14 +00:00
Add LDAP query builder feature with UI and API endpoints for managing LDAP attributes and filters.
Replit-Commit-Author: Agent Replit-Commit-Session-Id: 705f2157-ef97-4fbd-89e4-8c7f2ecaea90 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7ed01c5f-a82d-405a-b728-b2e3d127c60c/12f8d069-cc9a-4b87-88eb-6a7e3bcb8c9c.jpg
This commit is contained in:
@@ -11,6 +11,7 @@ import ComputersPage from "@/pages/computers-page";
|
||||
import DomainsPage from "@/pages/domains-page";
|
||||
import ApiTokensPage from "@/pages/api-tokens-page";
|
||||
import LdapConnectionsPage from "@/pages/ldap-connections-page";
|
||||
import LdapQueryBuilderPage from "@/pages/ldap-query-builder-page";
|
||||
import SettingsPage from "@/pages/settings-page";
|
||||
import UserManagementPage from "@/pages/user-management-page";
|
||||
import { ProtectedRoute } from "./lib/protected-route";
|
||||
@@ -30,6 +31,7 @@ function Router() {
|
||||
<ProtectedRoute path="/domains" component={DomainsPage} />
|
||||
<ProtectedRoute path="/api-tokens" component={ApiTokensPage} />
|
||||
<ProtectedRoute path="/ldap-connections" component={LdapConnectionsPage} />
|
||||
<ProtectedRoute path="/ldap-query-builder" component={LdapQueryBuilderPage} />
|
||||
<ProtectedRoute path="/settings" component={SettingsPage} />
|
||||
<ProtectedRoute path="/user-management" component={UserManagementPage} />
|
||||
<Route component={NotFound} />
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
Key,
|
||||
Server,
|
||||
ShieldAlert,
|
||||
Filter,
|
||||
} from "lucide-react";
|
||||
import { useMobile } from "@/hooks/use-mobile";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
@@ -67,6 +68,7 @@ const menuSections: MenuSection[] = [
|
||||
items: [
|
||||
{ title: "API Tokens", path: "/api-tokens", icon: <Key className="h-4 w-4" /> },
|
||||
{ title: "LDAP Connections", path: "/ldap-connections", icon: <Server className="h-4 w-4" /> },
|
||||
{ title: "LDAP Query Builder", path: "/ldap-query-builder", icon: <Filter className="h-4 w-4" /> },
|
||||
{ title: "Settings", path: "/settings", icon: <Settings className="h-4 w-4" /> },
|
||||
{ title: "User Management", path: "/user-management", icon: <ShieldAlert className="h-4 w-4" /> },
|
||||
],
|
||||
|
||||
@@ -0,0 +1,970 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
||||
import { DashboardLayout } from "@/layouts/dashboard-layout";
|
||||
import { DataTable } from "@/components/ui/data-table";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/components/ui/tabs";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Plus, Pencil, Trash2, Save, History, RotateCcw, Play, Filter } from "lucide-react";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { format } from "date-fns";
|
||||
|
||||
// Types for LDAP objects
|
||||
interface LdapAttribute {
|
||||
id: number;
|
||||
connectionId: number;
|
||||
name: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
type: string;
|
||||
multiValued: boolean;
|
||||
objectClass: string;
|
||||
isIndexed: boolean;
|
||||
}
|
||||
|
||||
interface LdapFilter {
|
||||
id: number;
|
||||
connectionId: number;
|
||||
name: string;
|
||||
description: string;
|
||||
objectClass: string;
|
||||
filter: any;
|
||||
ldapFilter: string;
|
||||
createdAt: string;
|
||||
createdBy: number;
|
||||
modifiedAt: string;
|
||||
modifiedBy: number;
|
||||
currentVersion: number;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
interface LdapFilterRevision {
|
||||
id: number;
|
||||
filterId: number;
|
||||
version: number;
|
||||
filter: any;
|
||||
ldapFilter: string;
|
||||
createdAt: string;
|
||||
createdBy: number;
|
||||
comment: string;
|
||||
}
|
||||
|
||||
interface LdapConnection {
|
||||
id: number;
|
||||
name: string;
|
||||
server: string;
|
||||
domain: string;
|
||||
port: number;
|
||||
useSSL: boolean;
|
||||
status: string;
|
||||
}
|
||||
|
||||
interface FilterCondition {
|
||||
attribute: string;
|
||||
operator: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface FilterGroup {
|
||||
type: "AND" | "OR";
|
||||
conditions: (FilterCondition | FilterGroup)[];
|
||||
}
|
||||
|
||||
export default function LdapQueryBuilderPage() {
|
||||
const { toast } = useToast();
|
||||
const [selectedConnection, setSelectedConnection] = useState<number | null>(null);
|
||||
const [selectedObjectClass, setSelectedObjectClass] = useState<string>("user");
|
||||
const [filterToEdit, setFilterToEdit] = useState<LdapFilter | null>(null);
|
||||
const [filterToDelete, setFilterToDelete] = useState<LdapFilter | null>(null);
|
||||
const [filterName, setFilterName] = useState("");
|
||||
const [filterDescription, setFilterDescription] = useState("");
|
||||
const [ldapFilter, setLdapFilter] = useState("");
|
||||
const [showHistory, setShowHistory] = useState(false);
|
||||
const [currentFilter, setCurrentFilter] = useState<FilterGroup>({
|
||||
type: "AND",
|
||||
conditions: []
|
||||
});
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [testResults, setTestResults] = useState<any[]>([]);
|
||||
const [revisionToRevert, setRevisionToRevert] = useState<LdapFilterRevision | null>(null);
|
||||
|
||||
// Get all LDAP connections
|
||||
const { data: connections = [], isLoading: isLoadingConnections } = useQuery<LdapConnection[]>({
|
||||
queryKey: ["/api/ldap-connections"],
|
||||
});
|
||||
|
||||
// Get attributes for the selected object class
|
||||
const { data: attributes = [], isLoading: isLoadingAttributes } = useQuery<LdapAttribute[]>({
|
||||
queryKey: ["/api/connections", selectedConnection, "ldap-attributes", selectedObjectClass],
|
||||
queryFn: async () => {
|
||||
if (!selectedConnection) return [];
|
||||
const res = await apiRequest("GET", `/api/connections/${selectedConnection}/ldap-attributes?objectClass=${selectedObjectClass}`);
|
||||
return await res.json();
|
||||
},
|
||||
enabled: !!selectedConnection,
|
||||
});
|
||||
|
||||
// Get saved filters for the selected connection
|
||||
const { data: savedFilters = [], isLoading: isLoadingSavedFilters } = useQuery<LdapFilter[]>({
|
||||
queryKey: ["/api/connections", selectedConnection, "ldap-filters"],
|
||||
queryFn: async () => {
|
||||
if (!selectedConnection) return [];
|
||||
const res = await apiRequest("GET", `/api/connections/${selectedConnection}/ldap-filters`);
|
||||
return await res.json();
|
||||
},
|
||||
enabled: !!selectedConnection,
|
||||
});
|
||||
|
||||
// Get revision history for the selected filter
|
||||
const { data: filterRevisions = [], isLoading: isLoadingRevisions } = useQuery<LdapFilterRevision[]>({
|
||||
queryKey: ["/api/ldap-filters", filterToEdit?.id, "revisions"],
|
||||
queryFn: async () => {
|
||||
if (!filterToEdit) return [];
|
||||
const res = await apiRequest("GET", `/api/ldap-filters/${filterToEdit.id}/revisions`);
|
||||
return await res.json();
|
||||
},
|
||||
enabled: !!filterToEdit && showHistory,
|
||||
});
|
||||
|
||||
// Create new filter
|
||||
const createFilterMutation = useMutation({
|
||||
mutationFn: async (filterData: any) => {
|
||||
const res = await apiRequest("POST", `/api/connections/${selectedConnection}/ldap-filters`, filterData);
|
||||
return await res.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: "Filter created",
|
||||
description: "The LDAP filter has been successfully created.",
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/connections", selectedConnection, "ldap-filters"] });
|
||||
resetFilterForm();
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: `Failed to create filter: ${error.message}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Update filter
|
||||
const updateFilterMutation = useMutation({
|
||||
mutationFn: async ({ id, data }: { id: number; data: any }) => {
|
||||
const res = await apiRequest("PUT", `/api/ldap-filters/${id}`, data);
|
||||
return await res.json();
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: "Filter updated",
|
||||
description: "The LDAP filter has been successfully updated.",
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/connections", selectedConnection, "ldap-filters"] });
|
||||
resetFilterForm();
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: `Failed to update filter: ${error.message}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Delete filter
|
||||
const deleteFilterMutation = useMutation({
|
||||
mutationFn: async (id: number) => {
|
||||
await apiRequest("DELETE", `/api/ldap-filters/${id}`);
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
title: "Filter deleted",
|
||||
description: "The LDAP filter has been successfully deleted.",
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/connections", selectedConnection, "ldap-filters"] });
|
||||
setFilterToDelete(null);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: `Failed to delete filter: ${error.message}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Test filter
|
||||
const testFilterMutation = useMutation({
|
||||
mutationFn: async (data: { ldapFilter: string; objectClass: string }) => {
|
||||
if (!selectedConnection) throw new Error("No connection selected");
|
||||
const res = await apiRequest("POST", `/api/connections/${selectedConnection}/test-filter`, data);
|
||||
return await res.json();
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
setTestResults(data);
|
||||
toast({
|
||||
title: "Filter tested",
|
||||
description: `Found ${data.length} results.`,
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
title: "Test failed",
|
||||
description: `Failed to test filter: ${error.message}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Revert to a previous revision
|
||||
const revertToRevisionMutation = useMutation({
|
||||
mutationFn: async ({ filterId, revisionId }: { filterId: number; revisionId: number }) => {
|
||||
const res = await apiRequest("POST", `/api/ldap-filters/${filterId}/revert/${revisionId}`);
|
||||
return await res.json();
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
toast({
|
||||
title: "Filter reverted",
|
||||
description: "The filter has been reverted to the selected revision.",
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/ldap-filters", filterToEdit?.id] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/ldap-filters", filterToEdit?.id, "revisions"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["/api/connections", selectedConnection, "ldap-filters"] });
|
||||
setRevisionToRevert(null);
|
||||
|
||||
// Update the current filter with the reverted data
|
||||
if (filterToEdit) {
|
||||
setFilterToEdit(data);
|
||||
setCurrentFilter(data.filter);
|
||||
setLdapFilter(data.ldapFilter);
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
title: "Revert failed",
|
||||
description: `Failed to revert filter: ${error.message}`,
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Handle filter selection
|
||||
const handleSelectFilter = (filter: LdapFilter) => {
|
||||
setFilterToEdit(filter);
|
||||
setFilterName(filter.name);
|
||||
setFilterDescription(filter.description || "");
|
||||
setCurrentFilter(filter.filter);
|
||||
setLdapFilter(filter.ldapFilter);
|
||||
setSelectedObjectClass(filter.objectClass);
|
||||
};
|
||||
|
||||
// Convert the filter object to LDAP filter string
|
||||
const buildLdapFilter = (filter: FilterGroup): string => {
|
||||
if (!filter.conditions || filter.conditions.length === 0) {
|
||||
return "(objectClass=*)";
|
||||
}
|
||||
|
||||
const operator = filter.type === "AND" ? "&" : "|";
|
||||
const conditions = filter.conditions.map(condition => {
|
||||
if ('type' in condition) {
|
||||
// This is a nested group
|
||||
return buildLdapFilter(condition);
|
||||
} else {
|
||||
// This is a basic condition
|
||||
let value = condition.value;
|
||||
let attr = condition.attribute;
|
||||
|
||||
switch (condition.operator) {
|
||||
case "equals":
|
||||
return `(${attr}=${value})`;
|
||||
case "notEquals":
|
||||
return `(!(${attr}=${value}))`;
|
||||
case "contains":
|
||||
return `(${attr}=*${value}*)`;
|
||||
case "startsWith":
|
||||
return `(${attr}=${value}*)`;
|
||||
case "endsWith":
|
||||
return `(${attr}=*${value})`;
|
||||
case "exists":
|
||||
return `(${attr}=*)`;
|
||||
case "notExists":
|
||||
return `(!(${attr}=*))`;
|
||||
default:
|
||||
return `(${attr}=${value})`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (conditions.length === 1) {
|
||||
return conditions[0];
|
||||
}
|
||||
|
||||
return `(${operator}${conditions.join('')})`;
|
||||
};
|
||||
|
||||
// Add a new condition to the filter
|
||||
const addCondition = () => {
|
||||
// Find the first attribute for default
|
||||
const defaultAttribute = attributes.length > 0 ? attributes[0].name : "";
|
||||
|
||||
const newCondition: FilterCondition = {
|
||||
attribute: defaultAttribute,
|
||||
operator: "equals",
|
||||
value: ""
|
||||
};
|
||||
|
||||
setCurrentFilter({
|
||||
...currentFilter,
|
||||
conditions: [...currentFilter.conditions, newCondition]
|
||||
});
|
||||
};
|
||||
|
||||
// Add a new group to the filter
|
||||
const addGroup = () => {
|
||||
const newGroup: FilterGroup = {
|
||||
type: "AND",
|
||||
conditions: []
|
||||
};
|
||||
|
||||
setCurrentFilter({
|
||||
...currentFilter,
|
||||
conditions: [...currentFilter.conditions, newGroup]
|
||||
});
|
||||
};
|
||||
|
||||
// Update a condition in the filter
|
||||
const updateCondition = (index: number, field: keyof FilterCondition, value: string) => {
|
||||
const updatedConditions = [...currentFilter.conditions];
|
||||
const condition = updatedConditions[index] as FilterCondition;
|
||||
|
||||
if ('attribute' in condition) {
|
||||
(condition as any)[field] = value;
|
||||
setCurrentFilter({
|
||||
...currentFilter,
|
||||
conditions: updatedConditions
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Update a group in the filter
|
||||
const updateGroup = (index: number, type: "AND" | "OR") => {
|
||||
const updatedConditions = [...currentFilter.conditions];
|
||||
const group = updatedConditions[index] as FilterGroup;
|
||||
|
||||
if ('type' in group) {
|
||||
group.type = type;
|
||||
setCurrentFilter({
|
||||
...currentFilter,
|
||||
conditions: updatedConditions
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Remove a condition from the filter
|
||||
const removeCondition = (index: number) => {
|
||||
const updatedConditions = [...currentFilter.conditions];
|
||||
updatedConditions.splice(index, 1);
|
||||
|
||||
setCurrentFilter({
|
||||
...currentFilter,
|
||||
conditions: updatedConditions
|
||||
});
|
||||
};
|
||||
|
||||
// Handle saving the filter
|
||||
const handleSaveFilter = () => {
|
||||
if (!selectedConnection) {
|
||||
toast({
|
||||
title: "No connection selected",
|
||||
description: "Please select an LDAP connection first.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!filterName) {
|
||||
toast({
|
||||
title: "Missing filter name",
|
||||
description: "Please provide a name for the filter.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const generatedLdapFilter = buildLdapFilter(currentFilter);
|
||||
|
||||
const filterData = {
|
||||
name: filterName,
|
||||
description: filterDescription,
|
||||
objectClass: selectedObjectClass,
|
||||
filter: currentFilter,
|
||||
ldapFilter: generatedLdapFilter
|
||||
};
|
||||
|
||||
if (filterToEdit) {
|
||||
updateFilterMutation.mutate({ id: filterToEdit.id, data: filterData });
|
||||
} else {
|
||||
createFilterMutation.mutate(filterData);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle testing the filter
|
||||
const handleTestFilter = () => {
|
||||
if (!selectedConnection) {
|
||||
toast({
|
||||
title: "No connection selected",
|
||||
description: "Please select an LDAP connection first.",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const generatedLdapFilter = buildLdapFilter(currentFilter);
|
||||
|
||||
testFilterMutation.mutate({
|
||||
ldapFilter: generatedLdapFilter,
|
||||
objectClass: selectedObjectClass
|
||||
});
|
||||
};
|
||||
|
||||
// Reset the filter form
|
||||
const resetFilterForm = () => {
|
||||
setFilterToEdit(null);
|
||||
setFilterName("");
|
||||
setFilterDescription("");
|
||||
setCurrentFilter({
|
||||
type: "AND",
|
||||
conditions: []
|
||||
});
|
||||
setLdapFilter("");
|
||||
setShowHistory(false);
|
||||
setTestResults([]);
|
||||
};
|
||||
|
||||
// Handle filter type change
|
||||
const handleFilterTypeChange = (type: "AND" | "OR") => {
|
||||
setCurrentFilter({
|
||||
...currentFilter,
|
||||
type
|
||||
});
|
||||
};
|
||||
|
||||
// Update LDAP filter when filter conditions change
|
||||
useEffect(() => {
|
||||
const generatedFilter = buildLdapFilter(currentFilter);
|
||||
setLdapFilter(generatedFilter);
|
||||
}, [currentFilter]);
|
||||
|
||||
// Columns for the saved filters table
|
||||
const filterColumns: Array<{
|
||||
header: string;
|
||||
accessorKey: keyof LdapFilter | ((row: LdapFilter) => React.ReactNode);
|
||||
cell?: (row: LdapFilter) => React.ReactNode;
|
||||
}> = [
|
||||
{
|
||||
header: "Name",
|
||||
accessorKey: "name",
|
||||
},
|
||||
{
|
||||
header: "Object Class",
|
||||
accessorKey: "objectClass",
|
||||
},
|
||||
{
|
||||
header: "Description",
|
||||
accessorKey: "description",
|
||||
},
|
||||
{
|
||||
header: "Last Modified",
|
||||
accessorKey: "modifiedAt",
|
||||
cell: (row: LdapFilter) => format(new Date(row.modifiedAt), 'MMM dd, yyyy HH:mm'),
|
||||
},
|
||||
{
|
||||
header: "Version",
|
||||
accessorKey: "currentVersion",
|
||||
},
|
||||
{
|
||||
header: "Actions",
|
||||
accessorKey: "id", // Use a valid property but render with cell
|
||||
cell: (row: LdapFilter) => (
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleSelectFilter(row);
|
||||
}}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setFilterToDelete(row);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
// Columns for the revisions table
|
||||
const revisionColumns: Array<{
|
||||
header: string;
|
||||
accessorKey: keyof LdapFilterRevision | ((row: LdapFilterRevision) => React.ReactNode);
|
||||
cell?: (row: LdapFilterRevision) => React.ReactNode;
|
||||
}> = [
|
||||
{
|
||||
header: "Version",
|
||||
accessorKey: "version",
|
||||
},
|
||||
{
|
||||
header: "Created At",
|
||||
accessorKey: "createdAt",
|
||||
cell: (row: LdapFilterRevision) => format(new Date(row.createdAt), 'MMM dd, yyyy HH:mm'),
|
||||
},
|
||||
{
|
||||
header: "Comment",
|
||||
accessorKey: "comment",
|
||||
},
|
||||
{
|
||||
header: "Actions",
|
||||
accessorKey: "id",
|
||||
cell: (row: LdapFilterRevision) => (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setRevisionToRevert(row)}
|
||||
>
|
||||
<RotateCcw className="h-4 w-4 mr-2" />
|
||||
Revert to this version
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
// Columns for the test results table
|
||||
const testResultColumns: Array<{
|
||||
header: string;
|
||||
accessorKey: string;
|
||||
cell?: (row: any) => React.ReactNode;
|
||||
}> = [
|
||||
{
|
||||
header: "Name",
|
||||
accessorKey: "name",
|
||||
},
|
||||
{
|
||||
header: "Distinguished Name",
|
||||
accessorKey: "dn",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<DashboardLayout title="LDAP Query Builder" description="Create, manage, and test LDAP filters">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div className="max-w-2xl">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Build complex LDAP queries with a visual interface. Save filters for later use, test them against your Active Directory, and manage filter versions.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<Button onClick={resetFilterForm} variant="outline">
|
||||
New Filter
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-12">
|
||||
{/* Connection and Object Class Selection */}
|
||||
<Card className="md:col-span-12">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle>Connection Settings</CardTitle>
|
||||
<CardDescription>Select a connection and object class for your filter</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<Label htmlFor="connection">LDAP Connection</Label>
|
||||
<Select
|
||||
value={selectedConnection?.toString() || ""}
|
||||
onValueChange={(value) => setSelectedConnection(Number(value))}
|
||||
>
|
||||
<SelectTrigger id="connection">
|
||||
<SelectValue placeholder="Select a connection" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{connections.map((connection) => (
|
||||
<SelectItem key={connection.id} value={connection.id.toString()}>
|
||||
{connection.name} ({connection.server})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="objectClass">Object Class</Label>
|
||||
<Select
|
||||
value={selectedObjectClass}
|
||||
onValueChange={setSelectedObjectClass}
|
||||
>
|
||||
<SelectTrigger id="objectClass">
|
||||
<SelectValue placeholder="Select object class" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="user">User</SelectItem>
|
||||
<SelectItem value="group">Group</SelectItem>
|
||||
<SelectItem value="organizationalUnit">Organizational Unit</SelectItem>
|
||||
<SelectItem value="computer">Computer</SelectItem>
|
||||
<SelectItem value="domain">Domain</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Filter Builder */}
|
||||
<Card className="md:col-span-8">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle>Filter Builder</CardTitle>
|
||||
<CardDescription>Build your LDAP filter by adding conditions</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="mb-4 grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<Label htmlFor="filterName">Filter Name</Label>
|
||||
<Input
|
||||
id="filterName"
|
||||
value={filterName}
|
||||
onChange={(e) => setFilterName(e.target.value)}
|
||||
placeholder="Enter a name for this filter"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="filterDescription">Description (Optional)</Label>
|
||||
<Input
|
||||
id="filterDescription"
|
||||
value={filterDescription}
|
||||
onChange={(e) => setFilterDescription(e.target.value)}
|
||||
placeholder="Enter a description"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<Label>Filter Type</Label>
|
||||
<div className="mt-2 flex space-x-2">
|
||||
<Button
|
||||
variant={currentFilter.type === "AND" ? "default" : "outline"}
|
||||
onClick={() => handleFilterTypeChange("AND")}
|
||||
size="sm"
|
||||
>
|
||||
Match ALL conditions (AND)
|
||||
</Button>
|
||||
<Button
|
||||
variant={currentFilter.type === "OR" ? "default" : "outline"}
|
||||
onClick={() => handleFilterTypeChange("OR")}
|
||||
size="sm"
|
||||
>
|
||||
Match ANY condition (OR)
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{currentFilter.conditions.map((condition, index) => {
|
||||
if ('type' in condition) {
|
||||
// This is a nested group (not implementing in this version)
|
||||
return (
|
||||
<div key={index} className="rounded-md border p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Label>Group Type</Label>
|
||||
<Select
|
||||
value={condition.type}
|
||||
onValueChange={(value) => updateGroup(index, value as "AND" | "OR")}
|
||||
>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="AND">AND</SelectItem>
|
||||
<SelectItem value="OR">OR</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => removeCondition(index)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Nested groups are not fully implemented in this version.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={index} className="grid grid-cols-1 gap-2 rounded-md border p-4 md:grid-cols-12">
|
||||
<div className="md:col-span-4">
|
||||
<Label>Attribute</Label>
|
||||
<Select
|
||||
value={condition.attribute}
|
||||
onValueChange={(value) => updateCondition(index, 'attribute', value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{attributes.map((attr) => (
|
||||
<SelectItem key={attr.id} value={attr.name}>
|
||||
{attr.displayName || attr.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="md:col-span-3">
|
||||
<Label>Operator</Label>
|
||||
<Select
|
||||
value={condition.operator}
|
||||
onValueChange={(value) => updateCondition(index, 'operator', value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="equals">Equals</SelectItem>
|
||||
<SelectItem value="notEquals">Not equals</SelectItem>
|
||||
<SelectItem value="contains">Contains</SelectItem>
|
||||
<SelectItem value="startsWith">Starts with</SelectItem>
|
||||
<SelectItem value="endsWith">Ends with</SelectItem>
|
||||
<SelectItem value="exists">Exists</SelectItem>
|
||||
<SelectItem value="notExists">Not exists</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="md:col-span-4">
|
||||
<Label>Value</Label>
|
||||
<Input
|
||||
value={condition.value}
|
||||
onChange={(e) => updateCondition(index, 'value', e.target.value)}
|
||||
placeholder="Value"
|
||||
disabled={["exists", "notExists"].includes(condition.operator)}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-end md:col-span-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => removeCondition(index)}
|
||||
className="self-end"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<Button onClick={addCondition} variant="outline" size="sm">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add Condition
|
||||
</Button>
|
||||
<Button onClick={addGroup} variant="outline" size="sm">
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add Group
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<Label htmlFor="ldapFilter">Generated LDAP Filter</Label>
|
||||
<Textarea
|
||||
id="ldapFilter"
|
||||
value={ldapFilter}
|
||||
onChange={(e) => setLdapFilter(e.target.value)}
|
||||
rows={2}
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex space-x-3">
|
||||
<Button onClick={handleSaveFilter} disabled={!selectedConnection || !filterName}>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
{filterToEdit ? "Update Filter" : "Save Filter"}
|
||||
</Button>
|
||||
<Button onClick={handleTestFilter} variant="secondary" disabled={!selectedConnection}>
|
||||
<Play className="mr-2 h-4 w-4" />
|
||||
Test Filter
|
||||
</Button>
|
||||
{filterToEdit && (
|
||||
<Button
|
||||
onClick={() => setShowHistory(!showHistory)}
|
||||
variant="outline"
|
||||
>
|
||||
<History className="mr-2 h-4 w-4" />
|
||||
{showHistory ? "Hide History" : "Show History"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Saved Filters and History */}
|
||||
<div className="md:col-span-4">
|
||||
<Tabs defaultValue="saved">
|
||||
<TabsList className="w-full">
|
||||
<TabsTrigger value="saved" className="flex-1">Saved Filters</TabsTrigger>
|
||||
<TabsTrigger value="testResults" className="flex-1">Test Results</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="saved">
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle>Saved Filters</CardTitle>
|
||||
<CardDescription>Filters saved for this connection</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
data={savedFilters}
|
||||
columns={filterColumns}
|
||||
isLoading={isLoadingSavedFilters}
|
||||
pagination={{
|
||||
pageIndex,
|
||||
pageSize,
|
||||
pageCount: Math.ceil(savedFilters.length / pageSize),
|
||||
onPageChange: setPageIndex,
|
||||
onPageSizeChange: setPageSize,
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{showHistory && filterToEdit && (
|
||||
<Card className="mt-4">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle>Revision History</CardTitle>
|
||||
<CardDescription>Previous versions of this filter</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
data={filterRevisions}
|
||||
columns={revisionColumns}
|
||||
isLoading={isLoadingRevisions}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="testResults">
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle>Test Results</CardTitle>
|
||||
<CardDescription>
|
||||
{testResults.length > 0
|
||||
? `Found ${testResults.length} matching ${selectedObjectClass}s`
|
||||
: "Run a test to see results"}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<DataTable
|
||||
data={testResults}
|
||||
columns={testResultColumns}
|
||||
isLoading={testFilterMutation.isPending}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Alert Dialogs */}
|
||||
<AlertDialog open={!!filterToDelete} onOpenChange={() => setFilterToDelete(null)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Filter</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to delete the filter "{filterToDelete?.name}"?
|
||||
This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => filterToDelete && deleteFilterMutation.mutate(filterToDelete.id)}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<AlertDialog open={!!revisionToRevert} onOpenChange={() => setRevisionToRevert(null)}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Revert Filter</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to revert to version {revisionToRevert?.version}?
|
||||
This will create a new version with the data from the selected revision.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => {
|
||||
if (filterToEdit && revisionToRevert) {
|
||||
revertToRevisionMutation.mutate({
|
||||
filterId: filterToEdit.id,
|
||||
revisionId: revisionToRevert.id
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Revert
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
@@ -846,6 +846,747 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/connections/{connectionId}/ldap-attributes:
|
||||
* get:
|
||||
* summary: Get LDAP attributes for a specific object class
|
||||
* tags: [LDAP Query Builder]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - name: connectionId
|
||||
* in: path
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* - name: objectClass
|
||||
* in: query
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [user, group, organizationalUnit, computer, domain]
|
||||
* responses:
|
||||
* 200:
|
||||
* description: List of LDAP attributes
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/LdapAttribute'
|
||||
* 401:
|
||||
* $ref: '#/components/responses/UnauthorizedError'
|
||||
* 404:
|
||||
* $ref: '#/components/responses/NotFoundError'
|
||||
*/
|
||||
app.get("/api/connections/:connectionId/ldap-attributes", requireAuth, async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const connectionId = parseInt(req.params.connectionId);
|
||||
const objectClass = req.query.objectClass as string;
|
||||
|
||||
if (!objectClass) {
|
||||
return res.status(400).json({ message: "objectClass parameter is required" });
|
||||
}
|
||||
|
||||
// Verify the connection exists
|
||||
const connection = await storage.getLdapConnection(connectionId);
|
||||
if (!connection) {
|
||||
return res.status(404).json({ message: "LDAP connection not found" });
|
||||
}
|
||||
|
||||
const attributes = await storage.getLdapAttributes(connectionId, objectClass);
|
||||
res.json(attributes);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/connections/{connectionId}/ldap-attributes:
|
||||
* post:
|
||||
* summary: Create a new LDAP attribute
|
||||
* tags: [LDAP Query Builder]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - name: connectionId
|
||||
* in: path
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - name
|
||||
* - objectClass
|
||||
* properties:
|
||||
* name:
|
||||
* type: string
|
||||
* displayName:
|
||||
* type: string
|
||||
* description:
|
||||
* type: string
|
||||
* type:
|
||||
* type: string
|
||||
* multiValued:
|
||||
* type: boolean
|
||||
* objectClass:
|
||||
* type: string
|
||||
* enum: [user, group, organizationalUnit, computer, domain]
|
||||
* isIndexed:
|
||||
* type: boolean
|
||||
* responses:
|
||||
* 201:
|
||||
* description: Attribute created successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/LdapAttribute'
|
||||
* 400:
|
||||
* $ref: '#/components/responses/BadRequestError'
|
||||
* 401:
|
||||
* $ref: '#/components/responses/UnauthorizedError'
|
||||
*/
|
||||
app.post("/api/connections/:connectionId/ldap-attributes", requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
const connectionId = parseInt(req.params.connectionId);
|
||||
|
||||
// Verify the connection exists
|
||||
const connection = await storage.getLdapConnection(connectionId);
|
||||
if (!connection) {
|
||||
return res.status(404).json({ message: "LDAP connection not found" });
|
||||
}
|
||||
|
||||
const attribute = await storage.createLdapAttribute({
|
||||
...req.body,
|
||||
connectionId
|
||||
});
|
||||
|
||||
res.status(201).json(attribute);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/ldap-attributes/{id}:
|
||||
* put:
|
||||
* summary: Update an LDAP attribute
|
||||
* tags: [LDAP Query Builder]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - name: id
|
||||
* in: path
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* displayName:
|
||||
* type: string
|
||||
* description:
|
||||
* type: string
|
||||
* type:
|
||||
* type: string
|
||||
* multiValued:
|
||||
* type: boolean
|
||||
* isIndexed:
|
||||
* type: boolean
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Attribute updated successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/LdapAttribute'
|
||||
* 400:
|
||||
* $ref: '#/components/responses/BadRequestError'
|
||||
* 401:
|
||||
* $ref: '#/components/responses/UnauthorizedError'
|
||||
* 404:
|
||||
* $ref: '#/components/responses/NotFoundError'
|
||||
*/
|
||||
app.put("/api/ldap-attributes/:id", requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const updatedAttribute = await storage.updateLdapAttribute(id, req.body);
|
||||
|
||||
if (!updatedAttribute) {
|
||||
return res.status(404).json({ message: "LDAP attribute not found" });
|
||||
}
|
||||
|
||||
res.json(updatedAttribute);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/ldap-attributes/{id}:
|
||||
* delete:
|
||||
* summary: Delete an LDAP attribute
|
||||
* tags: [LDAP Query Builder]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - name: id
|
||||
* in: path
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Attribute deleted successfully
|
||||
* 401:
|
||||
* $ref: '#/components/responses/UnauthorizedError'
|
||||
* 404:
|
||||
* $ref: '#/components/responses/NotFoundError'
|
||||
*/
|
||||
app.delete("/api/ldap-attributes/:id", requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const deleted = await storage.deleteLdapAttribute(id);
|
||||
|
||||
if (!deleted) {
|
||||
return res.status(404).json({ message: "LDAP attribute not found" });
|
||||
}
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/connections/{connectionId}/ldap-filters:
|
||||
* get:
|
||||
* summary: List LDAP filters for a connection
|
||||
* tags: [LDAP Query Builder]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - name: connectionId
|
||||
* in: path
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* responses:
|
||||
* 200:
|
||||
* description: List of LDAP filters
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/LdapFilter'
|
||||
* 401:
|
||||
* $ref: '#/components/responses/UnauthorizedError'
|
||||
* 404:
|
||||
* $ref: '#/components/responses/NotFoundError'
|
||||
*/
|
||||
/**
|
||||
* @swagger
|
||||
* /api/connections/{connectionId}/test-filter:
|
||||
* post:
|
||||
* summary: Test an LDAP filter against a connection
|
||||
* tags: [LDAP Query Builder]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - name: connectionId
|
||||
* in: path
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - ldapFilter
|
||||
* - objectClass
|
||||
* properties:
|
||||
* ldapFilter:
|
||||
* type: string
|
||||
* objectClass:
|
||||
* type: string
|
||||
* enum: [user, group, organizationalUnit, computer, domain]
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Filter test results
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* 400:
|
||||
* $ref: '#/components/responses/BadRequestError'
|
||||
* 401:
|
||||
* $ref: '#/components/responses/UnauthorizedError'
|
||||
* 404:
|
||||
* $ref: '#/components/responses/NotFoundError'
|
||||
*/
|
||||
app.post("/api/connections/:connectionId/test-filter", requireAuth, async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const connectionId = parseInt(req.params.connectionId);
|
||||
const { ldapFilter, objectClass } = req.body;
|
||||
|
||||
if (!ldapFilter || !objectClass) {
|
||||
return res.status(400).json({ message: "ldapFilter and objectClass are required" });
|
||||
}
|
||||
|
||||
// Verify the connection exists
|
||||
const connection = await storage.getLdapConnection(connectionId);
|
||||
if (!connection) {
|
||||
return res.status(404).json({ message: "LDAP connection not found" });
|
||||
}
|
||||
|
||||
// Test the filter
|
||||
const results = await storage.testLdapFilter(connectionId, ldapFilter, objectClass);
|
||||
res.json(results);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/connections/:connectionId/ldap-filters", requireAuth, async (req, res, next) => {
|
||||
try {
|
||||
const connectionId = parseInt(req.params.connectionId);
|
||||
|
||||
// Verify the connection exists
|
||||
const connection = await storage.getLdapConnection(connectionId);
|
||||
if (!connection) {
|
||||
return res.status(404).json({ message: "LDAP connection not found" });
|
||||
}
|
||||
|
||||
const filters = await storage.listLdapFilters(connectionId);
|
||||
res.json(filters);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/connections/{connectionId}/ldap-filters:
|
||||
* post:
|
||||
* summary: Create a new LDAP filter
|
||||
* tags: [LDAP Query Builder]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - name: connectionId
|
||||
* in: path
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - name
|
||||
* - objectClass
|
||||
* - filter
|
||||
* - ldapFilter
|
||||
* properties:
|
||||
* name:
|
||||
* type: string
|
||||
* description:
|
||||
* type: string
|
||||
* objectClass:
|
||||
* type: string
|
||||
* enum: [user, group, organizationalUnit, computer, domain]
|
||||
* filter:
|
||||
* type: object
|
||||
* ldapFilter:
|
||||
* type: string
|
||||
* responses:
|
||||
* 201:
|
||||
* description: Filter created successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/LdapFilter'
|
||||
* 400:
|
||||
* $ref: '#/components/responses/BadRequestError'
|
||||
* 401:
|
||||
* $ref: '#/components/responses/UnauthorizedError'
|
||||
*/
|
||||
app.post("/api/connections/:connectionId/ldap-filters", requireAuth, async (req, res, next) => {
|
||||
try {
|
||||
const connectionId = parseInt(req.params.connectionId);
|
||||
|
||||
// Verify the connection exists
|
||||
const connection = await storage.getLdapConnection(connectionId);
|
||||
if (!connection) {
|
||||
return res.status(404).json({ message: "LDAP connection not found" });
|
||||
}
|
||||
|
||||
const filter = await storage.createLdapFilter({
|
||||
...req.body,
|
||||
connectionId,
|
||||
createdBy: req.user.id,
|
||||
modifiedBy: req.user.id
|
||||
});
|
||||
|
||||
res.status(201).json(filter);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/ldap-filters/{id}:
|
||||
* get:
|
||||
* summary: Get a specific LDAP filter
|
||||
* tags: [LDAP Query Builder]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - name: id
|
||||
* in: path
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* responses:
|
||||
* 200:
|
||||
* description: LDAP filter details
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/LdapFilter'
|
||||
* 401:
|
||||
* $ref: '#/components/responses/UnauthorizedError'
|
||||
* 404:
|
||||
* $ref: '#/components/responses/NotFoundError'
|
||||
*/
|
||||
app.get("/api/ldap-filters/:id", requireAuth, async (req, res, next) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
const filter = await storage.getLdapFilter(id);
|
||||
|
||||
if (!filter) {
|
||||
return res.status(404).json({ message: "LDAP filter not found" });
|
||||
}
|
||||
|
||||
res.json(filter);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/ldap-filters/{id}:
|
||||
* put:
|
||||
* summary: Update an LDAP filter
|
||||
* tags: [LDAP Query Builder]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - name: id
|
||||
* in: path
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* name:
|
||||
* type: string
|
||||
* description:
|
||||
* type: string
|
||||
* filter:
|
||||
* type: object
|
||||
* ldapFilter:
|
||||
* type: string
|
||||
* isActive:
|
||||
* type: boolean
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Filter updated successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/LdapFilter'
|
||||
* 400:
|
||||
* $ref: '#/components/responses/BadRequestError'
|
||||
* 401:
|
||||
* $ref: '#/components/responses/UnauthorizedError'
|
||||
* 404:
|
||||
* $ref: '#/components/responses/NotFoundError'
|
||||
*/
|
||||
app.put("/api/ldap-filters/:id", requireAuth, async (req, res, next) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
|
||||
// Check if the filter exists
|
||||
const existingFilter = await storage.getLdapFilter(id);
|
||||
if (!existingFilter) {
|
||||
return res.status(404).json({ message: "LDAP filter not found" });
|
||||
}
|
||||
|
||||
// Update with the current user as modifier
|
||||
const updatedFilter = await storage.updateLdapFilter(id, {
|
||||
...req.body,
|
||||
modifiedBy: req.user.id
|
||||
});
|
||||
|
||||
res.json(updatedFilter);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/ldap-filters/{id}:
|
||||
* delete:
|
||||
* summary: Delete an LDAP filter
|
||||
* tags: [LDAP Query Builder]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - name: id
|
||||
* in: path
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Filter deleted successfully
|
||||
* 401:
|
||||
* $ref: '#/components/responses/UnauthorizedError'
|
||||
* 404:
|
||||
* $ref: '#/components/responses/NotFoundError'
|
||||
*/
|
||||
app.delete("/api/ldap-filters/:id", requireAuth, async (req, res, next) => {
|
||||
try {
|
||||
const id = parseInt(req.params.id);
|
||||
|
||||
// Check if the filter exists and if the user is allowed to delete it
|
||||
const filter = await storage.getLdapFilter(id);
|
||||
if (!filter) {
|
||||
return res.status(404).json({ message: "LDAP filter not found" });
|
||||
}
|
||||
|
||||
// Only allow the creator or admins to delete
|
||||
if (filter.createdBy !== req.user.id) {
|
||||
const userRole = await storage.getRole(req.user.roleId!);
|
||||
if (userRole?.name !== "admin") {
|
||||
return res.status(403).json({ message: "You are not authorized to delete this filter" });
|
||||
}
|
||||
}
|
||||
|
||||
const deleted = await storage.deleteLdapFilter(id);
|
||||
res.json({ success: deleted });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/ldap-filters/{filterId}/revisions:
|
||||
* get:
|
||||
* summary: Get the revision history for an LDAP filter
|
||||
* tags: [LDAP Query Builder]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - name: filterId
|
||||
* in: path
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* responses:
|
||||
* 200:
|
||||
* description: List of filter revisions
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/LdapFilterRevision'
|
||||
* 401:
|
||||
* $ref: '#/components/responses/UnauthorizedError'
|
||||
* 404:
|
||||
* $ref: '#/components/responses/NotFoundError'
|
||||
*/
|
||||
app.get("/api/ldap-filters/:filterId/revisions", requireAuth, async (req, res, next) => {
|
||||
try {
|
||||
const filterId = parseInt(req.params.filterId);
|
||||
|
||||
// Check if the filter exists
|
||||
const filter = await storage.getLdapFilter(filterId);
|
||||
if (!filter) {
|
||||
return res.status(404).json({ message: "LDAP filter not found" });
|
||||
}
|
||||
|
||||
const revisions = await storage.getLdapFilterRevisions(filterId);
|
||||
res.json(revisions);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/ldap-filters/{filterId}/revert/{revisionId}:
|
||||
* post:
|
||||
* summary: Revert a filter to a previous revision
|
||||
* tags: [LDAP Query Builder]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - name: filterId
|
||||
* in: path
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* - name: revisionId
|
||||
* in: path
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Filter reverted successfully
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* $ref: '#/components/schemas/LdapFilter'
|
||||
* 401:
|
||||
* $ref: '#/components/responses/UnauthorizedError'
|
||||
* 404:
|
||||
* $ref: '#/components/responses/NotFoundError'
|
||||
*/
|
||||
app.post("/api/ldap-filters/:filterId/revert/:revisionId", requireAuth, async (req, res, next) => {
|
||||
try {
|
||||
const filterId = parseInt(req.params.filterId);
|
||||
const revisionId = parseInt(req.params.revisionId);
|
||||
|
||||
// Check if the filter exists
|
||||
const filter = await storage.getLdapFilter(filterId);
|
||||
if (!filter) {
|
||||
return res.status(404).json({ message: "LDAP filter not found" });
|
||||
}
|
||||
|
||||
// Check if the user has permission to modify this filter
|
||||
if (filter.createdBy !== req.user.id) {
|
||||
const userRole = await storage.getRole(req.user.roleId!);
|
||||
if (userRole?.name !== "admin") {
|
||||
return res.status(403).json({ message: "You are not authorized to modify this filter" });
|
||||
}
|
||||
}
|
||||
|
||||
// First update the modifiedBy to the current user
|
||||
await storage.updateLdapFilter(filterId, { modifiedBy: req.user.id });
|
||||
|
||||
// Then perform the revert
|
||||
const revertedFilter = await storage.revertLdapFilterToRevision(filterId, revisionId);
|
||||
|
||||
if (!revertedFilter) {
|
||||
return res.status(404).json({ message: "Failed to revert filter or revision not found" });
|
||||
}
|
||||
|
||||
res.json(revertedFilter);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /api/connections/{connectionId}/test-ldap-filter:
|
||||
* post:
|
||||
* summary: Test an LDAP filter against a connection
|
||||
* tags: [LDAP Query Builder]
|
||||
* security:
|
||||
* - cookieAuth: []
|
||||
* parameters:
|
||||
* - name: connectionId
|
||||
* in: path
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required:
|
||||
* - ldapFilter
|
||||
* - objectClass
|
||||
* properties:
|
||||
* ldapFilter:
|
||||
* type: string
|
||||
* objectClass:
|
||||
* type: string
|
||||
* enum: [user, group, organizationalUnit, computer, domain]
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Test results
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* type: object
|
||||
* 400:
|
||||
* $ref: '#/components/responses/BadRequestError'
|
||||
* 401:
|
||||
* $ref: '#/components/responses/UnauthorizedError'
|
||||
*/
|
||||
app.post("/api/connections/:connectionId/test-ldap-filter", requireAuth, async (req, res, next) => {
|
||||
try {
|
||||
const connectionId = parseInt(req.params.connectionId);
|
||||
const { ldapFilter, objectClass } = req.body;
|
||||
|
||||
if (!ldapFilter || !objectClass) {
|
||||
return res.status(400).json({ message: "ldapFilter and objectClass are required" });
|
||||
}
|
||||
|
||||
// Verify the connection exists
|
||||
const connection = await storage.getLdapConnection(connectionId);
|
||||
if (!connection) {
|
||||
return res.status(404).json({ message: "LDAP connection not found" });
|
||||
}
|
||||
|
||||
const results = await storage.testLdapFilter(connectionId, ldapFilter, objectClass);
|
||||
res.json(results);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
const httpServer = createServer(app);
|
||||
|
||||
return httpServer;
|
||||
|
||||
+352
-1
@@ -4,8 +4,10 @@ import {
|
||||
AdUser, InsertAdUser, AdGroup, InsertAdGroup,
|
||||
AdOrgUnit, InsertAdOrgUnit, AdComputer, InsertAdComputer,
|
||||
AdDomain, InsertAdDomain, Role, ApiQuery,
|
||||
LdapFilter, InsertLdapFilter, LdapFilterRevision, InsertLdapFilterRevision,
|
||||
LdapAttribute, InsertLdapAttribute,
|
||||
users, apiTokens, ldapConnections, adUsers, adGroups, adOrgUnits, adComputers, adDomains,
|
||||
roles
|
||||
roles, ldapFilters, ldapFilterRevisions, ldapAttributes
|
||||
} from "@shared/schema";
|
||||
import session from "express-session";
|
||||
import createMemoryStore from "memorystore";
|
||||
@@ -57,6 +59,25 @@ export interface IStorage {
|
||||
deleteLdapConnection(id: number): Promise<boolean>;
|
||||
listLdapConnections(): Promise<LdapConnection[]>;
|
||||
|
||||
// LDAP Query Builder
|
||||
getLdapAttributes(connectionId: number, objectClass: string): Promise<LdapAttribute[]>;
|
||||
createLdapAttribute(attribute: InsertLdapAttribute): Promise<LdapAttribute>;
|
||||
updateLdapAttribute(id: number, attribute: Partial<LdapAttribute>): Promise<LdapAttribute | undefined>;
|
||||
deleteLdapAttribute(id: number): Promise<boolean>;
|
||||
|
||||
getLdapFilter(id: number): Promise<LdapFilter | undefined>;
|
||||
createLdapFilter(filter: InsertLdapFilter): Promise<LdapFilter>;
|
||||
updateLdapFilter(id: number, filter: Partial<LdapFilter>): Promise<LdapFilter | undefined>;
|
||||
deleteLdapFilter(id: number): Promise<boolean>;
|
||||
listLdapFilters(connectionId: number): Promise<LdapFilter[]>;
|
||||
|
||||
getLdapFilterRevisions(filterId: number): Promise<LdapFilterRevision[]>;
|
||||
getLdapFilterRevision(id: number): Promise<LdapFilterRevision | undefined>;
|
||||
createLdapFilterRevision(revision: InsertLdapFilterRevision): Promise<LdapFilterRevision>;
|
||||
revertLdapFilterToRevision(filterId: number, revisionId: number): Promise<LdapFilter | undefined>;
|
||||
|
||||
testLdapFilter(connectionId: number, ldapFilter: string, objectClass: string): Promise<any[]>;
|
||||
|
||||
// AD Users
|
||||
getAdUser(id: number): Promise<AdUser | undefined>;
|
||||
createAdUser(user: InsertAdUser): Promise<AdUser>;
|
||||
@@ -197,6 +218,336 @@ export class DatabaseStorage implements IStorage {
|
||||
async listLdapConnections(): Promise<LdapConnection[]> {
|
||||
return db.select().from(ldapConnections);
|
||||
}
|
||||
|
||||
// LDAP Query Builder methods
|
||||
async getLdapAttributes(connectionId: number, objectClass: string): Promise<LdapAttribute[]> {
|
||||
const cacheKey = `ldapAttributes:${connectionId}:${objectClass}`;
|
||||
|
||||
// Try to get from cache first
|
||||
const cachedData = await getCached<LdapAttribute[]>(cacheKey);
|
||||
if (cachedData) {
|
||||
debug(`Cache hit for ${cacheKey}`);
|
||||
return cachedData;
|
||||
}
|
||||
|
||||
const attributes = await db.select()
|
||||
.from(ldapAttributes)
|
||||
.where(
|
||||
and(
|
||||
eq(ldapAttributes.connectionId, connectionId),
|
||||
eq(ldapAttributes.objectClass, objectClass)
|
||||
)
|
||||
);
|
||||
|
||||
// Cache the result
|
||||
await setCached(cacheKey, attributes, CACHE_TTL.LONG);
|
||||
return attributes;
|
||||
}
|
||||
|
||||
async createLdapAttribute(attribute: InsertLdapAttribute): Promise<LdapAttribute> {
|
||||
debug(`Creating LDAP attribute: ${JSON.stringify(attribute)}`);
|
||||
const result = await db.insert(ldapAttributes).values(attribute).returning();
|
||||
|
||||
// Invalidate cache for the specific connection and object class
|
||||
await invalidateCache(`ldapAttributes:${attribute.connectionId}:${attribute.objectClass}`);
|
||||
|
||||
return result[0];
|
||||
}
|
||||
|
||||
async updateLdapAttribute(id: number, attribute: Partial<LdapAttribute>): Promise<LdapAttribute | undefined> {
|
||||
debug(`Updating LDAP attribute ${id}: ${JSON.stringify(attribute)}`);
|
||||
|
||||
// Get the attribute first to get connectionId and objectClass for cache invalidation
|
||||
const existingAttribute = await this.getLdapAttribute(id);
|
||||
if (!existingAttribute) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const result = await db.update(ldapAttributes)
|
||||
.set({
|
||||
...attribute,
|
||||
updatedAt: new Date()
|
||||
})
|
||||
.where(eq(ldapAttributes.id, id))
|
||||
.returning();
|
||||
|
||||
// Invalidate cache for the specific connection and object class
|
||||
await invalidateCache(`ldapAttributes:${existingAttribute.connectionId}:${existingAttribute.objectClass}`);
|
||||
|
||||
return result.length > 0 ? result[0] : undefined;
|
||||
}
|
||||
|
||||
async getLdapAttribute(id: number): Promise<LdapAttribute | undefined> {
|
||||
const result = await db.select().from(ldapAttributes).where(eq(ldapAttributes.id, id));
|
||||
return result.length > 0 ? result[0] : undefined;
|
||||
}
|
||||
|
||||
async deleteLdapAttribute(id: number): Promise<boolean> {
|
||||
debug(`Deleting LDAP attribute ${id}`);
|
||||
|
||||
// Get the attribute first to get connectionId and objectClass for cache invalidation
|
||||
const existingAttribute = await this.getLdapAttribute(id);
|
||||
if (!existingAttribute) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = await db.delete(ldapAttributes)
|
||||
.where(eq(ldapAttributes.id, id))
|
||||
.returning({ id: ldapAttributes.id });
|
||||
|
||||
// Invalidate cache for the specific connection and object class
|
||||
if (result.length > 0) {
|
||||
await invalidateCache(`ldapAttributes:${existingAttribute.connectionId}:${existingAttribute.objectClass}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async getLdapFilter(id: number): Promise<LdapFilter | undefined> {
|
||||
debug(`Getting LDAP filter ${id}`);
|
||||
const result = await db.select().from(ldapFilters).where(eq(ldapFilters.id, id));
|
||||
return result.length > 0 ? result[0] : undefined;
|
||||
}
|
||||
|
||||
async createLdapFilter(filter: InsertLdapFilter): Promise<LdapFilter> {
|
||||
debug(`Creating LDAP filter: ${JSON.stringify(filter)}`);
|
||||
|
||||
// Create the filter
|
||||
const newFilter = await db.insert(ldapFilters).values({
|
||||
...filter,
|
||||
currentVersion: 1,
|
||||
modifiedAt: new Date(),
|
||||
}).returning();
|
||||
|
||||
// Also create the initial revision
|
||||
await this.createLdapFilterRevision({
|
||||
filterId: newFilter[0].id,
|
||||
version: 1,
|
||||
filter: filter.filter,
|
||||
ldapFilter: filter.ldapFilter,
|
||||
createdBy: filter.createdBy,
|
||||
comment: "Initial version"
|
||||
});
|
||||
|
||||
// Invalidate the list cache
|
||||
await invalidateCache(`ldapFilters:${filter.connectionId}`);
|
||||
|
||||
return newFilter[0];
|
||||
}
|
||||
|
||||
async updateLdapFilter(id: number, filter: Partial<LdapFilter>): Promise<LdapFilter | undefined> {
|
||||
debug(`Updating LDAP filter ${id}: ${JSON.stringify(filter)}`);
|
||||
|
||||
// Get the current filter
|
||||
const currentFilter = await this.getLdapFilter(id);
|
||||
if (!currentFilter) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// If the filter structure or LDAP filter is changing, increment the version
|
||||
const currentVersion = currentFilter.currentVersion || 1;
|
||||
const newVersion = (filter.filter || filter.ldapFilter)
|
||||
? currentVersion + 1
|
||||
: currentVersion;
|
||||
|
||||
// Update the filter
|
||||
const result = await db.update(ldapFilters)
|
||||
.set({
|
||||
...filter,
|
||||
currentVersion: newVersion,
|
||||
modifiedAt: new Date()
|
||||
})
|
||||
.where(eq(ldapFilters.id, id))
|
||||
.returning();
|
||||
|
||||
// If version incremented, create a new revision
|
||||
if (newVersion > (currentFilter.currentVersion || 0) && filter.filter && filter.ldapFilter) {
|
||||
await this.createLdapFilterRevision({
|
||||
filterId: id,
|
||||
version: newVersion,
|
||||
filter: filter.filter,
|
||||
ldapFilter: filter.ldapFilter,
|
||||
createdBy: filter.modifiedBy,
|
||||
comment: `Version ${newVersion}`
|
||||
});
|
||||
}
|
||||
|
||||
// Invalidate the cache
|
||||
await invalidateCache(`ldapFilters:${currentFilter.connectionId}`);
|
||||
await invalidateCache(`ldapFilter:${id}`);
|
||||
|
||||
return result.length > 0 ? result[0] : undefined;
|
||||
}
|
||||
|
||||
async deleteLdapFilter(id: number): Promise<boolean> {
|
||||
debug(`Deleting LDAP filter ${id}`);
|
||||
|
||||
// Get the filter first for connection ID
|
||||
const filter = await this.getLdapFilter(id);
|
||||
if (!filter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Delete the filter (related revisions will be cascade deleted)
|
||||
const result = await db.delete(ldapFilters)
|
||||
.where(eq(ldapFilters.id, id))
|
||||
.returning({ id: ldapFilters.id });
|
||||
|
||||
// Invalidate the cache
|
||||
if (result.length > 0) {
|
||||
await invalidateCache(`ldapFilters:${filter.connectionId}`);
|
||||
await invalidateCache(`ldapFilter:${id}`);
|
||||
await invalidateCache(`ldapFilterRevisions:${id}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async listLdapFilters(connectionId: number): Promise<LdapFilter[]> {
|
||||
const cacheKey = `ldapFilters:${connectionId}`;
|
||||
|
||||
// Try to get from cache first
|
||||
const cachedData = await getCached<LdapFilter[]>(cacheKey);
|
||||
if (cachedData) {
|
||||
debug(`Cache hit for ${cacheKey}`);
|
||||
return cachedData;
|
||||
}
|
||||
|
||||
const filters = await db.select()
|
||||
.from(ldapFilters)
|
||||
.where(eq(ldapFilters.connectionId, connectionId));
|
||||
|
||||
// Cache the result
|
||||
await setCached(cacheKey, filters, CACHE_TTL.MEDIUM);
|
||||
return filters;
|
||||
}
|
||||
|
||||
async getLdapFilterRevisions(filterId: number): Promise<LdapFilterRevision[]> {
|
||||
const cacheKey = `ldapFilterRevisions:${filterId}`;
|
||||
|
||||
// Try to get from cache first
|
||||
const cachedData = await getCached<LdapFilterRevision[]>(cacheKey);
|
||||
if (cachedData) {
|
||||
debug(`Cache hit for ${cacheKey}`);
|
||||
return cachedData;
|
||||
}
|
||||
|
||||
const revisions = await db.select()
|
||||
.from(ldapFilterRevisions)
|
||||
.where(eq(ldapFilterRevisions.filterId, filterId))
|
||||
.orderBy(ldapFilterRevisions.version);
|
||||
|
||||
// Cache the result
|
||||
await setCached(cacheKey, revisions, CACHE_TTL.MEDIUM);
|
||||
return revisions;
|
||||
}
|
||||
|
||||
async getLdapFilterRevision(id: number): Promise<LdapFilterRevision | undefined> {
|
||||
debug(`Getting LDAP filter revision ${id}`);
|
||||
const result = await db.select().from(ldapFilterRevisions).where(eq(ldapFilterRevisions.id, id));
|
||||
return result.length > 0 ? result[0] : undefined;
|
||||
}
|
||||
|
||||
async createLdapFilterRevision(revision: InsertLdapFilterRevision): Promise<LdapFilterRevision> {
|
||||
debug(`Creating LDAP filter revision: ${JSON.stringify(revision)}`);
|
||||
const result = await db.insert(ldapFilterRevisions)
|
||||
.values({
|
||||
...revision,
|
||||
createdAt: new Date()
|
||||
})
|
||||
.returning();
|
||||
|
||||
// Invalidate cache for the revisions list
|
||||
await invalidateCache(`ldapFilterRevisions:${revision.filterId}`);
|
||||
|
||||
return result[0];
|
||||
}
|
||||
|
||||
async revertLdapFilterToRevision(filterId: number, revisionId: number): Promise<LdapFilter | undefined> {
|
||||
debug(`Reverting LDAP filter ${filterId} to revision ${revisionId}`);
|
||||
|
||||
// Get the current filter
|
||||
const filter = await this.getLdapFilter(filterId);
|
||||
if (!filter) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Get the target revision
|
||||
const revision = await this.getLdapFilterRevision(revisionId);
|
||||
if (!revision || revision.filterId !== filterId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Create a new revision with the next version number
|
||||
const currentVersion = filter.currentVersion || 1;
|
||||
const newVersion = currentVersion + 1;
|
||||
|
||||
// Update the filter with the revision data
|
||||
const result = await db.update(ldapFilters)
|
||||
.set({
|
||||
filter: revision.filter as any,
|
||||
ldapFilter: revision.ldapFilter,
|
||||
currentVersion: newVersion,
|
||||
modifiedAt: new Date()
|
||||
})
|
||||
.where(eq(ldapFilters.id, filterId))
|
||||
.returning();
|
||||
|
||||
// Create a new revision record
|
||||
await this.createLdapFilterRevision({
|
||||
filterId,
|
||||
version: newVersion,
|
||||
filter: revision.filter as any,
|
||||
ldapFilter: revision.ldapFilter,
|
||||
createdBy: filter.modifiedBy,
|
||||
comment: `Reverted to revision ${revision.version}`
|
||||
});
|
||||
|
||||
// Invalidate caches
|
||||
await invalidateCache(`ldapFilters:${filter.connectionId}`);
|
||||
await invalidateCache(`ldapFilter:${filterId}`);
|
||||
|
||||
return result.length > 0 ? result[0] : undefined;
|
||||
}
|
||||
|
||||
async testLdapFilter(connectionId: number, ldapFilter: string, objectClass: string): Promise<any[]> {
|
||||
debug(`Testing LDAP filter for connection ${connectionId}, object class ${objectClass}: ${ldapFilter}`);
|
||||
|
||||
// This is a placeholder. In a real implementation, this would connect to the LDAP server
|
||||
// and execute the query. For now, we'll simulate it using our existing data.
|
||||
|
||||
let results: any[] = [];
|
||||
|
||||
switch (objectClass) {
|
||||
case 'user':
|
||||
results = await this.listAdUsers(connectionId);
|
||||
break;
|
||||
case 'group':
|
||||
results = await this.listAdGroups(connectionId);
|
||||
break;
|
||||
case 'organizationalUnit':
|
||||
results = await this.listAdOrgUnits(connectionId);
|
||||
break;
|
||||
case 'computer':
|
||||
results = await this.listAdComputers(connectionId);
|
||||
break;
|
||||
case 'domain':
|
||||
results = await this.listAdDomains(connectionId);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported object class: ${objectClass}`);
|
||||
}
|
||||
|
||||
// In a real implementation, we'd use the ldapFilter to filter the results
|
||||
// Here we're just returning all items since we don't have a LDAP filter parser
|
||||
|
||||
// Adding log of the test
|
||||
debug(`LDAP filter test returned ${results.length} results`);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// AD Users
|
||||
async getAdUser(id: number): Promise<AdUser | undefined> {
|
||||
|
||||
+110
-1
@@ -1,4 +1,4 @@
|
||||
import { pgTable, text, serial, integer, boolean, timestamp, jsonb, primaryKey } from "drizzle-orm/pg-core";
|
||||
import { pgTable, text, serial, integer, boolean, timestamp, jsonb, json, primaryKey } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
import { relations } from "drizzle-orm";
|
||||
@@ -288,3 +288,112 @@ export type AdDomain = typeof adDomains.$inferSelect;
|
||||
export type InsertAdDomain = z.infer<typeof insertAdDomainSchema>;
|
||||
export type Login = z.infer<typeof loginSchema>;
|
||||
export type ApiQuery = z.infer<typeof apiQuerySchema>;
|
||||
|
||||
// LDAP Query Builder schemas
|
||||
export const ldapFilterObjectClasses = ["user", "group", "organizationalUnit", "computer", "domain"] as const;
|
||||
|
||||
// LDAP Filter schema
|
||||
export const ldapFilters = pgTable("ldap_filters", {
|
||||
id: serial("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
description: text("description").default(""),
|
||||
connectionId: integer("connection_id").references(() => ldapConnections.id, { onDelete: "cascade" }).notNull(),
|
||||
objectClass: text("object_class").notNull(),
|
||||
currentVersion: integer("current_version").default(1),
|
||||
filter: jsonb("filter").notNull(), // JSON representation of the filter structure
|
||||
ldapFilter: text("ldap_filter").notNull(), // The actual LDAP filter string
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
createdBy: integer("created_by").references(() => users.id),
|
||||
modifiedAt: timestamp("modified_at").defaultNow(),
|
||||
modifiedBy: integer("modified_by").references(() => users.id),
|
||||
isActive: boolean("is_active").default(true),
|
||||
});
|
||||
|
||||
// LDAP Filter Revision schema - stores the history of filter changes
|
||||
export const ldapFilterRevisions = pgTable("ldap_filter_revisions", {
|
||||
id: serial("id").primaryKey(),
|
||||
filterId: integer("filter_id").references(() => ldapFilters.id, { onDelete: "cascade" }).notNull(),
|
||||
version: integer("version").notNull(),
|
||||
filter: jsonb("filter").notNull(), // Stores the filter structure
|
||||
ldapFilter: text("ldap_filter").notNull(), // The actual LDAP filter string
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
createdBy: integer("created_by").references(() => users.id),
|
||||
comment: text("comment"),
|
||||
});
|
||||
|
||||
// LDAP Attribute schema - stores known attributes for connections
|
||||
export const ldapAttributes = pgTable("ldap_attributes", {
|
||||
id: serial("id").primaryKey(),
|
||||
connectionId: integer("connection_id").references(() => ldapConnections.id, { onDelete: "cascade" }).notNull(),
|
||||
name: text("name").notNull(),
|
||||
displayName: text("display_name"),
|
||||
description: text("description"),
|
||||
type: text("type"),
|
||||
multiValued: boolean("multi_valued").default(false),
|
||||
objectClass: text("object_class").notNull(),
|
||||
isIndexed: boolean("is_indexed").default(false),
|
||||
createdAt: timestamp("created_at").defaultNow(),
|
||||
updatedAt: timestamp("updated_at").defaultNow(),
|
||||
});
|
||||
|
||||
// Define relations
|
||||
export const ldapFiltersRelations = relations(ldapFilters, ({ one, many }) => ({
|
||||
connection: one(ldapConnections, {
|
||||
fields: [ldapFilters.connectionId],
|
||||
references: [ldapConnections.id],
|
||||
}),
|
||||
creator: one(users, {
|
||||
fields: [ldapFilters.createdBy],
|
||||
references: [users.id],
|
||||
}),
|
||||
modifier: one(users, {
|
||||
fields: [ldapFilters.modifiedBy],
|
||||
references: [users.id],
|
||||
}),
|
||||
revisions: many(ldapFilterRevisions),
|
||||
}));
|
||||
|
||||
export const ldapFilterRevisionsRelations = relations(ldapFilterRevisions, ({ one }) => ({
|
||||
filter: one(ldapFilters, {
|
||||
fields: [ldapFilterRevisions.filterId],
|
||||
references: [ldapFilters.id],
|
||||
}),
|
||||
creator: one(users, {
|
||||
fields: [ldapFilterRevisions.createdBy],
|
||||
references: [users.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const ldapAttributesRelations = relations(ldapAttributes, ({ one }) => ({
|
||||
connection: one(ldapConnections, {
|
||||
fields: [ldapAttributes.connectionId],
|
||||
references: [ldapConnections.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
// Generate insertion schemas
|
||||
export const insertLdapFilterSchema = createInsertSchema(ldapFilters).omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
modifiedAt: true,
|
||||
currentVersion: true
|
||||
});
|
||||
|
||||
export const insertLdapFilterRevisionSchema = createInsertSchema(ldapFilterRevisions).omit({
|
||||
id: true,
|
||||
createdAt: true
|
||||
});
|
||||
|
||||
export const insertLdapAttributeSchema = createInsertSchema(ldapAttributes).omit({
|
||||
id: true,
|
||||
createdAt: true,
|
||||
updatedAt: true
|
||||
});
|
||||
|
||||
// Export types
|
||||
export type LdapFilter = typeof ldapFilters.$inferSelect;
|
||||
export type InsertLdapFilter = z.infer<typeof insertLdapFilterSchema>;
|
||||
export type LdapFilterRevision = typeof ldapFilterRevisions.$inferSelect;
|
||||
export type InsertLdapFilterRevision = z.infer<typeof insertLdapFilterRevisionSchema>;
|
||||
export type LdapAttribute = typeof ldapAttributes.$inferSelect;
|
||||
export type InsertLdapAttribute = z.infer<typeof insertLdapAttributeSchema>;
|
||||
|
||||
Reference in New Issue
Block a user