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:
alphaeusmote
2025-04-08 21:43:51 +00:00
parent a0b0c23f18
commit 98159c0d30
6 changed files with 2177 additions and 2 deletions
+2
View File
@@ -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} />
+2
View File
@@ -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>
);
}
+741
View File
@@ -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
View File
@@ -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
View File
@@ -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>;