mirror of
https://github.com/freedbygrace/ActiveDirectoryManager.git
synced 2026-08-27 19:17:07 +00:00
Restored to d77ce2adbe
This commit is contained in:
+46
@@ -0,0 +1,46 @@
|
|||||||
|
FROM node:20-slim AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Copy the rest of the source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM node:20-slim AS runner
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Set environment to production
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
# Copy package files and install production dependencies
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci --production
|
||||||
|
|
||||||
|
# Copy build artifacts from the builder stage
|
||||||
|
COPY --from=builder /app/dist ./dist
|
||||||
|
|
||||||
|
# Copy schema files for Drizzle
|
||||||
|
COPY ./shared/schema.ts ./shared/
|
||||||
|
COPY ./drizzle.config.ts ./
|
||||||
|
|
||||||
|
# Set the database URL environment variable (this will be overridden at runtime)
|
||||||
|
ENV DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres
|
||||||
|
|
||||||
|
# Set session secret (should be overridden at runtime)
|
||||||
|
ENV SESSION_SECRET=changeme
|
||||||
|
|
||||||
|
# Expose the port the app runs on
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
# Start the application
|
||||||
|
CMD ["node", "dist/index.js"]
|
||||||
@@ -11,7 +11,6 @@ import ComputersPage from "@/pages/computers-page";
|
|||||||
import DomainsPage from "@/pages/domains-page";
|
import DomainsPage from "@/pages/domains-page";
|
||||||
import ApiTokensPage from "@/pages/api-tokens-page";
|
import ApiTokensPage from "@/pages/api-tokens-page";
|
||||||
import LdapConnectionsPage from "@/pages/ldap-connections-page";
|
import LdapConnectionsPage from "@/pages/ldap-connections-page";
|
||||||
import LdapQueryBuilderPage from "@/pages/ldap-query-builder-page";
|
|
||||||
import SettingsPage from "@/pages/settings-page";
|
import SettingsPage from "@/pages/settings-page";
|
||||||
import UserManagementPage from "@/pages/user-management-page";
|
import UserManagementPage from "@/pages/user-management-page";
|
||||||
import { ProtectedRoute } from "./lib/protected-route";
|
import { ProtectedRoute } from "./lib/protected-route";
|
||||||
@@ -31,7 +30,6 @@ function Router() {
|
|||||||
<ProtectedRoute path="/domains" component={DomainsPage} />
|
<ProtectedRoute path="/domains" component={DomainsPage} />
|
||||||
<ProtectedRoute path="/api-tokens" component={ApiTokensPage} />
|
<ProtectedRoute path="/api-tokens" component={ApiTokensPage} />
|
||||||
<ProtectedRoute path="/ldap-connections" component={LdapConnectionsPage} />
|
<ProtectedRoute path="/ldap-connections" component={LdapConnectionsPage} />
|
||||||
<ProtectedRoute path="/ldap-query-builder" component={LdapQueryBuilderPage} />
|
|
||||||
<ProtectedRoute path="/settings" component={SettingsPage} />
|
<ProtectedRoute path="/settings" component={SettingsPage} />
|
||||||
<ProtectedRoute path="/user-management" component={UserManagementPage} />
|
<ProtectedRoute path="/user-management" component={UserManagementPage} />
|
||||||
<Route component={NotFound} />
|
<Route component={NotFound} />
|
||||||
|
|||||||
@@ -1,682 +0,0 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { useToast } from "@/hooks/use-toast";
|
|
||||||
import { LdapConnection } from "@shared/schema";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Loader2, Plus, Trash, Info } from "lucide-react";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import {
|
|
||||||
Tabs,
|
|
||||||
TabsContent,
|
|
||||||
TabsList,
|
|
||||||
TabsTrigger,
|
|
||||||
} from "@/components/ui/tabs";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
|
|
||||||
// Import shared types from our shared lib
|
|
||||||
import { LdapOperator, LdapCondition, LdapAttribute } from "@/lib/ldap-types";
|
|
||||||
|
|
||||||
export type LdapQueryBuilderParams = {
|
|
||||||
targetObject: "users" | "groups" | "computers" | "ous";
|
|
||||||
filter: LdapCondition;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Used for displaying the operators in the UI
|
|
||||||
const operatorLabels: Record<LdapOperator, string> = {
|
|
||||||
[LdapOperator.AND]: "AND (All conditions)",
|
|
||||||
[LdapOperator.OR]: "OR (Any condition)",
|
|
||||||
[LdapOperator.NOT]: "NOT",
|
|
||||||
[LdapOperator.EQUALS]: "Equals",
|
|
||||||
[LdapOperator.NOT_EQUALS]: "Does not equal",
|
|
||||||
[LdapOperator.STARTS_WITH]: "Starts with",
|
|
||||||
[LdapOperator.ENDS_WITH]: "Ends with",
|
|
||||||
[LdapOperator.CONTAINS]: "Contains",
|
|
||||||
[LdapOperator.GREATER_THAN]: "Greater than",
|
|
||||||
[LdapOperator.LESS_THAN]: "Less than",
|
|
||||||
[LdapOperator.PRESENT]: "Has value (attribute exists)",
|
|
||||||
[LdapOperator.APPROX]: "Approximately equals",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Group operators by type for the UI
|
|
||||||
const logicalOperators = [LdapOperator.AND, LdapOperator.OR, LdapOperator.NOT];
|
|
||||||
const comparisonOperators = [
|
|
||||||
LdapOperator.EQUALS,
|
|
||||||
LdapOperator.NOT_EQUALS,
|
|
||||||
LdapOperator.STARTS_WITH,
|
|
||||||
LdapOperator.ENDS_WITH,
|
|
||||||
LdapOperator.CONTAINS,
|
|
||||||
LdapOperator.GREATER_THAN,
|
|
||||||
LdapOperator.LESS_THAN,
|
|
||||||
LdapOperator.PRESENT,
|
|
||||||
LdapOperator.APPROX,
|
|
||||||
];
|
|
||||||
|
|
||||||
interface LdapQueryBuilderProps {
|
|
||||||
connections: LdapConnection[];
|
|
||||||
value: LdapQueryBuilderParams;
|
|
||||||
onChange: (value: LdapQueryBuilderParams) => void;
|
|
||||||
onTest?: () => void;
|
|
||||||
onSave?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function EnhancedLdapQueryBuilder({
|
|
||||||
connections,
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
onTest,
|
|
||||||
onSave
|
|
||||||
}: LdapQueryBuilderProps) {
|
|
||||||
const { toast } = useToast();
|
|
||||||
const [selectedConnectionId, setSelectedConnectionId] = useState<number | null>(
|
|
||||||
connections.length > 0 ? connections[0].id : null
|
|
||||||
);
|
|
||||||
|
|
||||||
// Load available attributes based on connection and object type
|
|
||||||
const { data: availableAttributes = [], isLoading: isLoadingAttributes } = useQuery<LdapAttribute[]>({
|
|
||||||
queryKey: ["/api/ldap-queries/attributes", { connectionId: selectedConnectionId, targetObject: value.targetObject }],
|
|
||||||
enabled: !!selectedConnectionId && !!value.targetObject,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Group attributes by their type for better organization
|
|
||||||
const groupedAttributes = React.useMemo(() => {
|
|
||||||
const grouped = {
|
|
||||||
string: availableAttributes.filter(attr => !attr.type || attr.type === "string"),
|
|
||||||
number: availableAttributes.filter(attr => attr.type === "number"),
|
|
||||||
datetime: availableAttributes.filter(attr => attr.type === "datetime"),
|
|
||||||
boolean: availableAttributes.filter(attr => attr.type === "boolean"),
|
|
||||||
};
|
|
||||||
|
|
||||||
return grouped;
|
|
||||||
}, [availableAttributes]);
|
|
||||||
|
|
||||||
// Update the selected connection ID if the connections list changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (connections.length > 0 && !selectedConnectionId) {
|
|
||||||
setSelectedConnectionId(connections[0].id);
|
|
||||||
}
|
|
||||||
}, [connections, selectedConnectionId]);
|
|
||||||
|
|
||||||
// Handle changes to the query target object type
|
|
||||||
const handleTargetObjectChange = (targetObject: "users" | "groups" | "computers" | "ous") => {
|
|
||||||
onChange({
|
|
||||||
...value,
|
|
||||||
targetObject,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle updates to a condition
|
|
||||||
const handleConditionChange = (
|
|
||||||
condition: LdapCondition,
|
|
||||||
path: number[] = []
|
|
||||||
): LdapCondition => {
|
|
||||||
if (path.length === 0) {
|
|
||||||
return condition;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [index, ...restPath] = path;
|
|
||||||
const updatedConditions = [...(value.filter.conditions || [])];
|
|
||||||
|
|
||||||
if (restPath.length === 0) {
|
|
||||||
updatedConditions[index] = condition;
|
|
||||||
} else {
|
|
||||||
updatedConditions[index] = handleConditionChange(
|
|
||||||
updatedConditions[index],
|
|
||||||
restPath
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...value.filter,
|
|
||||||
conditions: updatedConditions,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add a new condition to a logical operator
|
|
||||||
const handleAddCondition = (path: number[] = []) => {
|
|
||||||
let currentCondition = value.filter;
|
|
||||||
let parent = currentCondition;
|
|
||||||
|
|
||||||
// Navigate to the target condition
|
|
||||||
for (const index of path) {
|
|
||||||
if (!currentCondition.conditions) {
|
|
||||||
currentCondition.conditions = [];
|
|
||||||
}
|
|
||||||
parent = currentCondition;
|
|
||||||
currentCondition = currentCondition.conditions[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a new condition
|
|
||||||
if (!parent.conditions) {
|
|
||||||
parent.conditions = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
parent.conditions.push({
|
|
||||||
operator: LdapOperator.EQUALS,
|
|
||||||
attribute: availableAttributes.length > 0 ? availableAttributes[0].name : undefined,
|
|
||||||
value: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
onChange({ ...value });
|
|
||||||
};
|
|
||||||
|
|
||||||
// Remove a condition
|
|
||||||
const handleRemoveCondition = (path: number[]) => {
|
|
||||||
if (path.length === 0) return;
|
|
||||||
|
|
||||||
const parentPath = path.slice(0, -1);
|
|
||||||
const index = path[path.length - 1];
|
|
||||||
|
|
||||||
let currentCondition = value.filter;
|
|
||||||
|
|
||||||
// Navigate to the parent condition
|
|
||||||
for (const idx of parentPath) {
|
|
||||||
if (!currentCondition.conditions) return;
|
|
||||||
currentCondition = currentCondition.conditions[idx];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the condition
|
|
||||||
if (currentCondition.conditions) {
|
|
||||||
currentCondition.conditions = currentCondition.conditions.filter((_, i) => i !== index);
|
|
||||||
onChange({ ...value });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get recommended operators based on attribute type
|
|
||||||
const getRecommendedOperators = (attributeName: string | undefined): LdapOperator[] => {
|
|
||||||
if (!attributeName) return comparisonOperators;
|
|
||||||
|
|
||||||
const attribute = availableAttributes.find(attr => attr.name === attributeName);
|
|
||||||
if (!attribute) return comparisonOperators;
|
|
||||||
|
|
||||||
// Return appropriate operators based on type
|
|
||||||
switch(attribute.type) {
|
|
||||||
case "number":
|
|
||||||
return [
|
|
||||||
LdapOperator.EQUALS,
|
|
||||||
LdapOperator.NOT_EQUALS,
|
|
||||||
LdapOperator.GREATER_THAN,
|
|
||||||
LdapOperator.LESS_THAN,
|
|
||||||
LdapOperator.PRESENT
|
|
||||||
];
|
|
||||||
case "datetime":
|
|
||||||
return [
|
|
||||||
LdapOperator.EQUALS,
|
|
||||||
LdapOperator.NOT_EQUALS,
|
|
||||||
LdapOperator.GREATER_THAN,
|
|
||||||
LdapOperator.LESS_THAN,
|
|
||||||
LdapOperator.PRESENT
|
|
||||||
];
|
|
||||||
case "boolean":
|
|
||||||
return [
|
|
||||||
LdapOperator.EQUALS,
|
|
||||||
LdapOperator.NOT_EQUALS,
|
|
||||||
LdapOperator.PRESENT
|
|
||||||
];
|
|
||||||
default: // string
|
|
||||||
return [
|
|
||||||
LdapOperator.EQUALS,
|
|
||||||
LdapOperator.NOT_EQUALS,
|
|
||||||
LdapOperator.STARTS_WITH,
|
|
||||||
LdapOperator.ENDS_WITH,
|
|
||||||
LdapOperator.CONTAINS,
|
|
||||||
LdapOperator.PRESENT,
|
|
||||||
LdapOperator.APPROX
|
|
||||||
];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// UI component for rendering a single condition
|
|
||||||
const ConditionItem = ({
|
|
||||||
condition,
|
|
||||||
path = []
|
|
||||||
}: {
|
|
||||||
condition: LdapCondition;
|
|
||||||
path: number[];
|
|
||||||
}) => {
|
|
||||||
const isLogical = logicalOperators.includes(condition.operator);
|
|
||||||
const selectedAttribute = availableAttributes.find(attr => attr.name === condition.attribute);
|
|
||||||
const recommendedOperators = !isLogical ? getRecommendedOperators(condition.attribute) : [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-4 border rounded-md mb-2">
|
|
||||||
<div className="flex flex-wrap gap-2 mb-2">
|
|
||||||
<Select
|
|
||||||
value={condition.operator}
|
|
||||||
onValueChange={(newValue) => {
|
|
||||||
const updatedCondition = { ...condition, operator: newValue as LdapOperator };
|
|
||||||
|
|
||||||
// Reset attributes and values when switching between logical and comparison
|
|
||||||
const wasLogical = logicalOperators.includes(condition.operator);
|
|
||||||
const isNowLogical = logicalOperators.includes(updatedCondition.operator);
|
|
||||||
|
|
||||||
if (wasLogical !== isNowLogical) {
|
|
||||||
if (isNowLogical) {
|
|
||||||
delete updatedCondition.attribute;
|
|
||||||
delete updatedCondition.value;
|
|
||||||
updatedCondition.conditions = [];
|
|
||||||
} else {
|
|
||||||
delete updatedCondition.conditions;
|
|
||||||
updatedCondition.attribute = availableAttributes.length > 0 ? availableAttributes[0].name : undefined;
|
|
||||||
updatedCondition.value = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const newFilter = handleConditionChange(updatedCondition, path);
|
|
||||||
onChange({ ...value, filter: newFilter });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-[200px]">
|
|
||||||
<SelectValue placeholder="Select operator" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="group" disabled className="font-semibold">
|
|
||||||
Logical Operators
|
|
||||||
</SelectItem>
|
|
||||||
{logicalOperators.map((op) => (
|
|
||||||
<SelectItem key={op} value={op}>
|
|
||||||
{operatorLabels[op]}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{!isLogical && condition.attribute && (
|
|
||||||
<SelectItem value="divider" disabled className="font-semibold">
|
|
||||||
Recommended Operators
|
|
||||||
</SelectItem>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isLogical && condition.attribute && recommendedOperators.map((op) => (
|
|
||||||
<SelectItem key={op} value={op} className="text-primary">
|
|
||||||
{operatorLabels[op]}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<SelectItem value="divider2" disabled className="font-semibold">
|
|
||||||
All Comparison Operators
|
|
||||||
</SelectItem>
|
|
||||||
{comparisonOperators.map((op) => (
|
|
||||||
<SelectItem key={op} value={op}>
|
|
||||||
{operatorLabels[op]}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
{!isLogical && (
|
|
||||||
<>
|
|
||||||
<Select
|
|
||||||
value={condition.attribute}
|
|
||||||
onValueChange={(newValue) => {
|
|
||||||
const updatedCondition = { ...condition, attribute: newValue };
|
|
||||||
const newFilter = handleConditionChange(updatedCondition, path);
|
|
||||||
onChange({ ...value, filter: newFilter });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-[250px]">
|
|
||||||
<SelectValue placeholder="Select attribute" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent className="max-h-[400px]">
|
|
||||||
{isLoadingAttributes ? (
|
|
||||||
<div className="flex items-center justify-center p-2">
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
|
||||||
Loading attributes...
|
|
||||||
</div>
|
|
||||||
) : availableAttributes.length === 0 ? (
|
|
||||||
<SelectItem value="empty" disabled>
|
|
||||||
No attributes available
|
|
||||||
</SelectItem>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{/* String attributes */}
|
|
||||||
{groupedAttributes.string.length > 0 && (
|
|
||||||
<>
|
|
||||||
<SelectItem value="string_group" disabled className="font-semibold">
|
|
||||||
Text Attributes
|
|
||||||
</SelectItem>
|
|
||||||
{groupedAttributes.string.map((attr) => (
|
|
||||||
<SelectItem key={attr.name} value={attr.name}>
|
|
||||||
<div className="flex items-center justify-between w-full">
|
|
||||||
<span>{attr.name}</span>
|
|
||||||
<div className="flex items-center">
|
|
||||||
{attr.isMultiValued && (
|
|
||||||
<Badge variant="outline" className="text-xs ml-2">
|
|
||||||
Multi
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{attr.description && (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Info className="h-3 w-3 ml-2 text-muted-foreground" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="right">
|
|
||||||
<p className="max-w-[300px]">{attr.description}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Number attributes */}
|
|
||||||
{groupedAttributes.number.length > 0 && (
|
|
||||||
<>
|
|
||||||
<SelectItem value="number_group" disabled className="font-semibold">
|
|
||||||
Number Attributes
|
|
||||||
</SelectItem>
|
|
||||||
{groupedAttributes.number.map((attr) => (
|
|
||||||
<SelectItem key={attr.name} value={attr.name}>
|
|
||||||
<div className="flex items-center justify-between w-full">
|
|
||||||
<span>{attr.name}</span>
|
|
||||||
<div className="flex items-center">
|
|
||||||
{attr.isMultiValued && (
|
|
||||||
<Badge variant="outline" className="text-xs ml-2">
|
|
||||||
Multi
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{attr.description && (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Info className="h-3 w-3 ml-2 text-muted-foreground" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="right">
|
|
||||||
<p className="max-w-[300px]">{attr.description}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* DateTime attributes */}
|
|
||||||
{groupedAttributes.datetime.length > 0 && (
|
|
||||||
<>
|
|
||||||
<SelectItem value="date_group" disabled className="font-semibold">
|
|
||||||
Date/Time Attributes
|
|
||||||
</SelectItem>
|
|
||||||
{groupedAttributes.datetime.map((attr) => (
|
|
||||||
<SelectItem key={attr.name} value={attr.name}>
|
|
||||||
<div className="flex items-center justify-between w-full">
|
|
||||||
<span>{attr.name}</span>
|
|
||||||
<div className="flex items-center">
|
|
||||||
{attr.isMultiValued && (
|
|
||||||
<Badge variant="outline" className="text-xs ml-2">
|
|
||||||
Multi
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{attr.description && (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Info className="h-3 w-3 ml-2 text-muted-foreground" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="right">
|
|
||||||
<p className="max-w-[300px]">{attr.description}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Boolean attributes */}
|
|
||||||
{groupedAttributes.boolean.length > 0 && (
|
|
||||||
<>
|
|
||||||
<SelectItem value="boolean_group" disabled className="font-semibold">
|
|
||||||
Boolean Attributes
|
|
||||||
</SelectItem>
|
|
||||||
{groupedAttributes.boolean.map((attr) => (
|
|
||||||
<SelectItem key={attr.name} value={attr.name}>
|
|
||||||
<div className="flex items-center justify-between w-full">
|
|
||||||
<span>{attr.name}</span>
|
|
||||||
<div className="flex items-center">
|
|
||||||
{attr.isMultiValued && (
|
|
||||||
<Badge variant="outline" className="text-xs ml-2">
|
|
||||||
Multi
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{attr.description && (
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Info className="h-3 w-3 ml-2 text-muted-foreground" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="right">
|
|
||||||
<p className="max-w-[300px]">{attr.description}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
{condition.operator !== LdapOperator.PRESENT && (
|
|
||||||
<Input
|
|
||||||
value={condition.value || ""}
|
|
||||||
placeholder={selectedAttribute?.type === "datetime" ? "YYYY-MM-DD" : "Value"}
|
|
||||||
className="flex-1"
|
|
||||||
onChange={(e) => {
|
|
||||||
const updatedCondition = { ...condition, value: e.target.value };
|
|
||||||
const newFilter = handleConditionChange(updatedCondition, path);
|
|
||||||
onChange({ ...value, filter: newFilter });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{path.length > 0 && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => handleRemoveCondition(path)}
|
|
||||||
>
|
|
||||||
<Trash className="h-4 w-4 text-destructive" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isLogical && (
|
|
||||||
<div className="pl-4 border-l-2 border-dashed border-muted-foreground/30">
|
|
||||||
{condition.conditions?.map((childCondition, index) => (
|
|
||||||
<ConditionItem
|
|
||||||
key={index}
|
|
||||||
condition={childCondition}
|
|
||||||
path={[...path, index]}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleAddCondition(path)}
|
|
||||||
className="mt-2"
|
|
||||||
>
|
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
|
||||||
Add Condition
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="w-full">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>LDAP Query Builder</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Build LDAP queries with a visual interface
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="connection">LDAP Connection</Label>
|
|
||||||
<Select
|
|
||||||
value={selectedConnectionId?.toString() || ""}
|
|
||||||
onValueChange={(value) => setSelectedConnectionId(parseInt(value))}
|
|
||||||
disabled={connections.length === 0}
|
|
||||||
>
|
|
||||||
<SelectTrigger id="connection">
|
|
||||||
<SelectValue placeholder="Select LDAP connection" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{connections.length === 0 ? (
|
|
||||||
<SelectItem value="empty" disabled>
|
|
||||||
No connections available
|
|
||||||
</SelectItem>
|
|
||||||
) : (
|
|
||||||
connections.map((connection) => (
|
|
||||||
<SelectItem key={connection.id} value={connection.id.toString()}>
|
|
||||||
{connection.name}
|
|
||||||
</SelectItem>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="targetObject">Target Object Type</Label>
|
|
||||||
<Select
|
|
||||||
value={value.targetObject}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
handleTargetObjectChange(value as "users" | "groups" | "computers" | "ous")}
|
|
||||||
>
|
|
||||||
<SelectTrigger id="targetObject">
|
|
||||||
<SelectValue placeholder="Select object type" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="users">Users</SelectItem>
|
|
||||||
<SelectItem value="groups">Groups</SelectItem>
|
|
||||||
<SelectItem value="computers">Computers</SelectItem>
|
|
||||||
<SelectItem value="ous">Organizational Units</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label className="mb-2 block">Build Filter</Label>
|
|
||||||
<Tabs defaultValue="builder" className="w-full">
|
|
||||||
<TabsList className="mb-2">
|
|
||||||
<TabsTrigger value="builder">Visual Builder</TabsTrigger>
|
|
||||||
<TabsTrigger value="preview">LDAP Filter Preview</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
<TabsContent value="builder" className="p-0">
|
|
||||||
<ConditionItem condition={value.filter} path={[]} />
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="preview" className="p-0">
|
|
||||||
<div className="p-4 bg-muted rounded-md font-mono text-sm overflow-auto">
|
|
||||||
<pre>
|
|
||||||
{JSON.stringify(value.filter, null, 2)}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{availableAttributes.length > 0 && (
|
|
||||||
<div>
|
|
||||||
<Label className="mb-2 block">Available Attributes</Label>
|
|
||||||
<div className="flex flex-wrap gap-2 max-h-[150px] overflow-y-auto p-2 border rounded-md">
|
|
||||||
{availableAttributes.map((attr) => (
|
|
||||||
<TooltipProvider key={attr.name}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Badge
|
|
||||||
variant="outline"
|
|
||||||
className={`cursor-pointer ${
|
|
||||||
attr.type === 'number'
|
|
||||||
? 'bg-blue-50 dark:bg-blue-950'
|
|
||||||
: attr.type === 'datetime'
|
|
||||||
? 'bg-amber-50 dark:bg-amber-950'
|
|
||||||
: attr.type === 'boolean'
|
|
||||||
? 'bg-green-50 dark:bg-green-950'
|
|
||||||
: ''
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{attr.name}
|
|
||||||
{attr.isMultiValued && <span className="ml-1 opacity-70">*</span>}
|
|
||||||
</Badge>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<div>
|
|
||||||
<p className="font-bold">{attr.name}</p>
|
|
||||||
{attr.description && <p>{attr.description}</p>}
|
|
||||||
<p className="text-xs mt-1">Type: {attr.type || 'string'}</p>
|
|
||||||
{attr.isMultiValued && (
|
|
||||||
<p className="text-xs">Multi-valued attribute</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className="flex justify-end gap-2">
|
|
||||||
{onTest && (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={onTest}
|
|
||||||
>
|
|
||||||
Test Query
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{onSave && (
|
|
||||||
<Button
|
|
||||||
onClick={onSave}
|
|
||||||
>
|
|
||||||
Save Query
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,441 +0,0 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { useToast } from "@/hooks/use-toast";
|
|
||||||
import { LdapConnection } from "@shared/schema";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Loader2, Plus, Trash } from "lucide-react";
|
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import {
|
|
||||||
Tabs,
|
|
||||||
TabsContent,
|
|
||||||
TabsList,
|
|
||||||
TabsTrigger,
|
|
||||||
} from "@/components/ui/tabs";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
|
|
||||||
// Import shared types from our shared lib
|
|
||||||
import { LdapOperator, LdapCondition, LdapAttribute } from "@/lib/ldap-types";
|
|
||||||
|
|
||||||
export type LdapQueryBuilderParams = {
|
|
||||||
targetObject: "users" | "groups" | "computers" | "ous";
|
|
||||||
filter: LdapCondition;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Used for displaying the operators in the UI
|
|
||||||
const operatorLabels: Record<LdapOperator, string> = {
|
|
||||||
[LdapOperator.AND]: "AND (All conditions)",
|
|
||||||
[LdapOperator.OR]: "OR (Any condition)",
|
|
||||||
[LdapOperator.NOT]: "NOT",
|
|
||||||
[LdapOperator.EQUALS]: "Equals",
|
|
||||||
[LdapOperator.NOT_EQUALS]: "Does not equal",
|
|
||||||
[LdapOperator.STARTS_WITH]: "Starts with",
|
|
||||||
[LdapOperator.ENDS_WITH]: "Ends with",
|
|
||||||
[LdapOperator.CONTAINS]: "Contains",
|
|
||||||
[LdapOperator.GREATER_THAN]: "Greater than",
|
|
||||||
[LdapOperator.LESS_THAN]: "Less than",
|
|
||||||
[LdapOperator.PRESENT]: "Has value (attribute exists)",
|
|
||||||
[LdapOperator.APPROX]: "Approximately equals",
|
|
||||||
};
|
|
||||||
|
|
||||||
// Group operators by type for the UI
|
|
||||||
const logicalOperators = [LdapOperator.AND, LdapOperator.OR, LdapOperator.NOT];
|
|
||||||
const comparisonOperators = [
|
|
||||||
LdapOperator.EQUALS,
|
|
||||||
LdapOperator.NOT_EQUALS,
|
|
||||||
LdapOperator.STARTS_WITH,
|
|
||||||
LdapOperator.ENDS_WITH,
|
|
||||||
LdapOperator.CONTAINS,
|
|
||||||
LdapOperator.GREATER_THAN,
|
|
||||||
LdapOperator.LESS_THAN,
|
|
||||||
LdapOperator.PRESENT,
|
|
||||||
LdapOperator.APPROX,
|
|
||||||
];
|
|
||||||
|
|
||||||
interface LdapQueryBuilderProps {
|
|
||||||
connections: LdapConnection[];
|
|
||||||
value: LdapQueryBuilderParams;
|
|
||||||
onChange: (value: LdapQueryBuilderParams) => void;
|
|
||||||
onTest?: () => void;
|
|
||||||
onSave?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function LdapQueryBuilder({
|
|
||||||
connections,
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
onTest,
|
|
||||||
onSave
|
|
||||||
}: LdapQueryBuilderProps) {
|
|
||||||
const { toast } = useToast();
|
|
||||||
const [selectedConnectionId, setSelectedConnectionId] = useState<number | null>(
|
|
||||||
connections.length > 0 ? connections[0].id : null
|
|
||||||
);
|
|
||||||
|
|
||||||
// Load available attributes based on connection and object type
|
|
||||||
const { data: availableAttributes = [], isLoading: isLoadingAttributes } = useQuery<LdapAttribute[]>({
|
|
||||||
queryKey: ["/api/ldap-queries/attributes", { connectionId: selectedConnectionId, targetObject: value.targetObject }],
|
|
||||||
enabled: !!selectedConnectionId && !!value.targetObject,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update the selected connection ID if the connections list changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (connections.length > 0 && !selectedConnectionId) {
|
|
||||||
setSelectedConnectionId(connections[0].id);
|
|
||||||
}
|
|
||||||
}, [connections, selectedConnectionId]);
|
|
||||||
|
|
||||||
// Handle changes to the query target object type
|
|
||||||
const handleTargetObjectChange = (targetObject: "users" | "groups" | "computers" | "ous") => {
|
|
||||||
onChange({
|
|
||||||
...value,
|
|
||||||
targetObject,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle updates to a condition
|
|
||||||
const handleConditionChange = (
|
|
||||||
condition: LdapCondition,
|
|
||||||
path: number[] = []
|
|
||||||
): LdapCondition => {
|
|
||||||
if (path.length === 0) {
|
|
||||||
return condition;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [index, ...restPath] = path;
|
|
||||||
const updatedConditions = [...(value.filter.conditions || [])];
|
|
||||||
|
|
||||||
if (restPath.length === 0) {
|
|
||||||
updatedConditions[index] = condition;
|
|
||||||
} else {
|
|
||||||
updatedConditions[index] = handleConditionChange(
|
|
||||||
updatedConditions[index],
|
|
||||||
restPath
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...value.filter,
|
|
||||||
conditions: updatedConditions,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add a new condition to a logical operator
|
|
||||||
const handleAddCondition = (path: number[] = []) => {
|
|
||||||
let currentCondition = value.filter;
|
|
||||||
let parent = currentCondition;
|
|
||||||
|
|
||||||
// Navigate to the target condition
|
|
||||||
for (const index of path) {
|
|
||||||
if (!currentCondition.conditions) {
|
|
||||||
currentCondition.conditions = [];
|
|
||||||
}
|
|
||||||
parent = currentCondition;
|
|
||||||
currentCondition = currentCondition.conditions[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a new condition
|
|
||||||
if (!parent.conditions) {
|
|
||||||
parent.conditions = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
parent.conditions.push({
|
|
||||||
operator: LdapOperator.EQUALS,
|
|
||||||
attribute: availableAttributes.length > 0 ? availableAttributes[0].name : undefined,
|
|
||||||
value: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
onChange({ ...value });
|
|
||||||
};
|
|
||||||
|
|
||||||
// Remove a condition
|
|
||||||
const handleRemoveCondition = (path: number[]) => {
|
|
||||||
if (path.length === 0) return;
|
|
||||||
|
|
||||||
const parentPath = path.slice(0, -1);
|
|
||||||
const index = path[path.length - 1];
|
|
||||||
|
|
||||||
let currentCondition = value.filter;
|
|
||||||
|
|
||||||
// Navigate to the parent condition
|
|
||||||
for (const idx of parentPath) {
|
|
||||||
if (!currentCondition.conditions) return;
|
|
||||||
currentCondition = currentCondition.conditions[idx];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the condition
|
|
||||||
if (currentCondition.conditions) {
|
|
||||||
currentCondition.conditions = currentCondition.conditions.filter((_, i) => i !== index);
|
|
||||||
onChange({ ...value });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// UI component for rendering a single condition
|
|
||||||
const ConditionItem = ({
|
|
||||||
condition,
|
|
||||||
path = []
|
|
||||||
}: {
|
|
||||||
condition: LdapCondition;
|
|
||||||
path: number[];
|
|
||||||
}) => {
|
|
||||||
const isLogical = logicalOperators.includes(condition.operator);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-4 border rounded-md mb-2">
|
|
||||||
<div className="flex flex-wrap gap-2 mb-2">
|
|
||||||
<Select
|
|
||||||
value={condition.operator}
|
|
||||||
onValueChange={(newValue) => {
|
|
||||||
const updatedCondition = { ...condition, operator: newValue as LdapOperator };
|
|
||||||
|
|
||||||
// Reset attributes and values when switching between logical and comparison
|
|
||||||
const wasLogical = logicalOperators.includes(condition.operator);
|
|
||||||
const isNowLogical = logicalOperators.includes(updatedCondition.operator);
|
|
||||||
|
|
||||||
if (wasLogical !== isNowLogical) {
|
|
||||||
if (isNowLogical) {
|
|
||||||
delete updatedCondition.attribute;
|
|
||||||
delete updatedCondition.value;
|
|
||||||
updatedCondition.conditions = [];
|
|
||||||
} else {
|
|
||||||
delete updatedCondition.conditions;
|
|
||||||
updatedCondition.attribute = availableAttributes.length > 0 ? availableAttributes[0].name : undefined;
|
|
||||||
updatedCondition.value = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const newFilter = handleConditionChange(updatedCondition, path);
|
|
||||||
onChange({ ...value, filter: newFilter });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-[200px]">
|
|
||||||
<SelectValue placeholder="Select operator" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="group" disabled className="font-semibold">
|
|
||||||
Logical Operators
|
|
||||||
</SelectItem>
|
|
||||||
{logicalOperators.map((op) => (
|
|
||||||
<SelectItem key={op} value={op}>
|
|
||||||
{operatorLabels[op]}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
<SelectItem value="divider" disabled className="font-semibold">
|
|
||||||
Comparison Operators
|
|
||||||
</SelectItem>
|
|
||||||
{comparisonOperators.map((op) => (
|
|
||||||
<SelectItem key={op} value={op}>
|
|
||||||
{operatorLabels[op]}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
{!isLogical && (
|
|
||||||
<>
|
|
||||||
<Select
|
|
||||||
value={condition.attribute}
|
|
||||||
onValueChange={(newValue) => {
|
|
||||||
const updatedCondition = { ...condition, attribute: newValue };
|
|
||||||
const newFilter = handleConditionChange(updatedCondition, path);
|
|
||||||
onChange({ ...value, filter: newFilter });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SelectTrigger className="w-[200px]">
|
|
||||||
<SelectValue placeholder="Select attribute" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{isLoadingAttributes ? (
|
|
||||||
<div className="flex items-center justify-center p-2">
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
|
||||||
Loading attributes...
|
|
||||||
</div>
|
|
||||||
) : availableAttributes.length === 0 ? (
|
|
||||||
<SelectItem value="empty" disabled>
|
|
||||||
No attributes available
|
|
||||||
</SelectItem>
|
|
||||||
) : (
|
|
||||||
availableAttributes.map((attr) => (
|
|
||||||
<SelectItem key={attr.name} value={attr.name}>
|
|
||||||
{attr.name} {attr.description ? `(${attr.description})` : ''}
|
|
||||||
</SelectItem>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
|
|
||||||
{condition.operator !== LdapOperator.PRESENT && (
|
|
||||||
<Input
|
|
||||||
value={condition.value || ""}
|
|
||||||
placeholder="Value"
|
|
||||||
className="flex-1"
|
|
||||||
onChange={(e) => {
|
|
||||||
const updatedCondition = { ...condition, value: e.target.value };
|
|
||||||
const newFilter = handleConditionChange(updatedCondition, path);
|
|
||||||
onChange({ ...value, filter: newFilter });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{path.length > 0 && (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
onClick={() => handleRemoveCondition(path)}
|
|
||||||
>
|
|
||||||
<Trash className="h-4 w-4 text-destructive" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isLogical && (
|
|
||||||
<div className="pl-4 border-l-2 border-dashed border-muted-foreground/30">
|
|
||||||
{condition.conditions?.map((childCondition, index) => (
|
|
||||||
<ConditionItem
|
|
||||||
key={index}
|
|
||||||
condition={childCondition}
|
|
||||||
path={[...path, index]}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => handleAddCondition(path)}
|
|
||||||
className="mt-2"
|
|
||||||
>
|
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
|
||||||
Add Condition
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="w-full">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>LDAP Query Builder</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Build LDAP queries with a visual interface
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="connection">LDAP Connection</Label>
|
|
||||||
<Select
|
|
||||||
value={selectedConnectionId?.toString() || ""}
|
|
||||||
onValueChange={(value) => setSelectedConnectionId(parseInt(value))}
|
|
||||||
disabled={connections.length === 0}
|
|
||||||
>
|
|
||||||
<SelectTrigger id="connection">
|
|
||||||
<SelectValue placeholder="Select LDAP connection" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{connections.length === 0 ? (
|
|
||||||
<SelectItem value="empty" disabled>
|
|
||||||
No connections available
|
|
||||||
</SelectItem>
|
|
||||||
) : (
|
|
||||||
connections.map((connection) => (
|
|
||||||
<SelectItem key={connection.id} value={connection.id.toString()}>
|
|
||||||
{connection.name}
|
|
||||||
</SelectItem>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="targetObject">Target Object Type</Label>
|
|
||||||
<Select
|
|
||||||
value={value.targetObject}
|
|
||||||
onValueChange={(value) =>
|
|
||||||
handleTargetObjectChange(value as "users" | "groups" | "computers" | "ous")}
|
|
||||||
>
|
|
||||||
<SelectTrigger id="targetObject">
|
|
||||||
<SelectValue placeholder="Select object type" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="users">Users</SelectItem>
|
|
||||||
<SelectItem value="groups">Groups</SelectItem>
|
|
||||||
<SelectItem value="computers">Computers</SelectItem>
|
|
||||||
<SelectItem value="ous">Organizational Units</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label className="mb-2 block">Build Filter</Label>
|
|
||||||
<Tabs defaultValue="builder" className="w-full">
|
|
||||||
<TabsList className="mb-2">
|
|
||||||
<TabsTrigger value="builder">Visual Builder</TabsTrigger>
|
|
||||||
<TabsTrigger value="preview">LDAP Filter Preview</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
<TabsContent value="builder" className="p-0">
|
|
||||||
<ConditionItem condition={value.filter} path={[]} />
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="preview" className="p-0">
|
|
||||||
<div className="p-4 bg-muted rounded-md font-mono text-sm overflow-auto">
|
|
||||||
<pre>
|
|
||||||
{JSON.stringify(value.filter, null, 2)}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{availableAttributes.length > 0 && (
|
|
||||||
<div>
|
|
||||||
<Label className="mb-2 block">Available Attributes</Label>
|
|
||||||
<div className="flex flex-wrap gap-2 max-h-[150px] overflow-y-auto p-2 border rounded-md">
|
|
||||||
{availableAttributes.map((attr) => (
|
|
||||||
<Badge key={attr.name} variant="outline" className="cursor-pointer">
|
|
||||||
{attr.name} {attr.type ? `(${attr.type})` : ''}
|
|
||||||
</Badge>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
<CardFooter className="flex justify-end gap-2">
|
|
||||||
{onTest && (
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={onTest}
|
|
||||||
>
|
|
||||||
Test Query
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{onSave && (
|
|
||||||
<Button
|
|
||||||
onClick={onSave}
|
|
||||||
>
|
|
||||||
Save Query
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -67,7 +67,6 @@ const menuSections: MenuSection[] = [
|
|||||||
items: [
|
items: [
|
||||||
{ title: "API Tokens", path: "/api-tokens", icon: <Key className="h-4 w-4" /> },
|
{ 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 Connections", path: "/ldap-connections", icon: <Server className="h-4 w-4" /> },
|
||||||
{ title: "LDAP Query Builder", path: "/ldap-query-builder", icon: <FolderClosed className="h-4 w-4" /> },
|
|
||||||
{ title: "Settings", path: "/settings", icon: <Settings 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" /> },
|
{ title: "User Management", path: "/user-management", icon: <ShieldAlert className="h-4 w-4" /> },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
// LDAP query types
|
|
||||||
export enum LdapOperator {
|
|
||||||
// Logical operators
|
|
||||||
AND = "and",
|
|
||||||
OR = "or",
|
|
||||||
NOT = "not",
|
|
||||||
|
|
||||||
// Comparison operators
|
|
||||||
EQUALS = "equals",
|
|
||||||
NOT_EQUALS = "notEquals",
|
|
||||||
STARTS_WITH = "startsWith",
|
|
||||||
ENDS_WITH = "endsWith",
|
|
||||||
CONTAINS = "contains",
|
|
||||||
GREATER_THAN = "greaterThan",
|
|
||||||
LESS_THAN = "lessThan",
|
|
||||||
PRESENT = "present", // attribute exists
|
|
||||||
APPROX = "approx", // approximately equals
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type definition for an LDAP condition
|
|
||||||
export interface LdapCondition {
|
|
||||||
operator: LdapOperator;
|
|
||||||
attribute?: string; // Not required for logical operators (AND, OR, NOT)
|
|
||||||
value?: string; // Not required for some operators (PRESENT, logical operators)
|
|
||||||
conditions?: LdapCondition[]; // For nested conditions with logical operators
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type definition for an LDAP attribute
|
|
||||||
export interface LdapAttribute {
|
|
||||||
name: string;
|
|
||||||
type: string;
|
|
||||||
description?: string;
|
|
||||||
syntax?: string;
|
|
||||||
isMultiValued?: boolean;
|
|
||||||
isMandatory?: boolean;
|
|
||||||
}
|
|
||||||
@@ -100,29 +100,18 @@ export default function AuthPage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register mutation - using our simplified endpoint
|
// Register mutation
|
||||||
const registerMutation = useMutation({
|
const registerMutation = useMutation({
|
||||||
mutationFn: async (credentials: any) => {
|
mutationFn: async (credentials: any) => {
|
||||||
try {
|
const res = await apiRequest("POST", "/api/register", credentials);
|
||||||
const res = await apiRequest("POST", "/api/simple-register", credentials);
|
return await res.json();
|
||||||
if (!res.ok) {
|
|
||||||
const errorData = await res.json();
|
|
||||||
throw new Error(errorData.message || "Registration failed");
|
|
||||||
}
|
|
||||||
return await res.json();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Registration error:", error);
|
|
||||||
throw error instanceof Error ? error : new Error("Unknown registration error");
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onSuccess: (response) => {
|
onSuccess: (user) => {
|
||||||
toast({
|
toast({
|
||||||
title: "Registration successful",
|
title: "Registration successful",
|
||||||
description: response.message || `Welcome, ${response.username}!`,
|
description: `Welcome, ${user.username}!`,
|
||||||
});
|
});
|
||||||
// Switch to login tab automatically
|
navigate('/');
|
||||||
const loginTab = document.querySelector('[data-state="inactive"][data-value="login"]') as HTMLElement;
|
|
||||||
if (loginTab) loginTab.click();
|
|
||||||
},
|
},
|
||||||
onError: (error: Error) => {
|
onError: (error: Error) => {
|
||||||
toast({
|
toast({
|
||||||
|
|||||||
@@ -1,537 +0,0 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { useQuery, useMutation } from "@tanstack/react-query";
|
|
||||||
import { useToast } from "@/hooks/use-toast";
|
|
||||||
import { DashboardLayout } from "@/layouts/dashboard-layout";
|
|
||||||
import { LdapConnection, LdapQuery } from "@shared/schema";
|
|
||||||
import { LdapQueryBuilderParams } from "@/components/ldap/ldap-query-builder";
|
|
||||||
import { EnhancedLdapQueryBuilder } from "@/components/ldap/enhanced-ldap-query-builder";
|
|
||||||
import { LdapCondition, LdapOperator } from "@/lib/ldap-types";
|
|
||||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { DataTable } from "@/components/ui/data-table";
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger
|
|
||||||
} from "@/components/ui/dropdown-menu";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardFooter,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import {
|
|
||||||
Tabs,
|
|
||||||
TabsContent,
|
|
||||||
TabsList,
|
|
||||||
TabsTrigger,
|
|
||||||
} from "@/components/ui/tabs";
|
|
||||||
import { Loader2, MoreHorizontal, Plus, Code, Save, Play, FileClock } from "lucide-react";
|
|
||||||
import { format } from "date-fns";
|
|
||||||
|
|
||||||
export default function LdapQueryBuilderPage() {
|
|
||||||
const { toast } = useToast();
|
|
||||||
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
|
||||||
const [showTestDialog, setShowTestDialog] = useState(false);
|
|
||||||
const [testResults, setTestResults] = useState<any[] | null>(null);
|
|
||||||
|
|
||||||
const [queryName, setQueryName] = useState("");
|
|
||||||
const [queryDescription, setQueryDescription] = useState("");
|
|
||||||
|
|
||||||
const [activeQuery, setActiveQuery] = useState<LdapQuery | null>(null);
|
|
||||||
|
|
||||||
const [queryBuilderParams, setQueryBuilderParams] = useState<LdapQueryBuilderParams>({
|
|
||||||
targetObject: "users",
|
|
||||||
filter: {
|
|
||||||
operator: LdapOperator.AND,
|
|
||||||
conditions: []
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fetch all saved queries
|
|
||||||
const { data: savedQueries = [], isLoading: isLoadingQueries } = useQuery<LdapQuery[]>({
|
|
||||||
queryKey: ["/api/ldap-queries"],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fetch LDAP connections for the query builder
|
|
||||||
const { data: connections = [], isLoading: isLoadingConnections } = useQuery<LdapConnection[]>({
|
|
||||||
queryKey: ["/api/ldap-connections"],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mutation for creating a new query
|
|
||||||
const createQueryMutation = useMutation({
|
|
||||||
mutationFn: async (data: {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
targetObject: string;
|
|
||||||
filter: any;
|
|
||||||
}) => {
|
|
||||||
const response = await apiRequest("POST", "/api/ldap-queries", data);
|
|
||||||
return await response.json();
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["/api/ldap-queries"] });
|
|
||||||
setShowCreateDialog(false);
|
|
||||||
setQueryName("");
|
|
||||||
setQueryDescription("");
|
|
||||||
toast({
|
|
||||||
title: "Query saved",
|
|
||||||
description: "The LDAP query has been saved successfully.",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast({
|
|
||||||
title: "Error saving query",
|
|
||||||
description: error.message,
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mutation for testing a query
|
|
||||||
const testQueryMutation = useMutation({
|
|
||||||
mutationFn: async (data: {
|
|
||||||
connectionId: number;
|
|
||||||
targetObject: string;
|
|
||||||
filter: any;
|
|
||||||
limit?: number;
|
|
||||||
}) => {
|
|
||||||
const response = await apiRequest("POST", "/api/ldap-queries/test", data);
|
|
||||||
return await response.json();
|
|
||||||
},
|
|
||||||
onSuccess: (data) => {
|
|
||||||
setTestResults(data.results);
|
|
||||||
setShowTestDialog(true);
|
|
||||||
toast({
|
|
||||||
title: "Query executed",
|
|
||||||
description: `Query returned ${data.count} results.`,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast({
|
|
||||||
title: "Error testing query",
|
|
||||||
description: error.message,
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mutation for updating an existing query
|
|
||||||
const updateQueryMutation = useMutation({
|
|
||||||
mutationFn: async (data: {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
targetObject: string;
|
|
||||||
filter: any;
|
|
||||||
}) => {
|
|
||||||
const response = await apiRequest("PUT", `/api/ldap-queries/${data.id}`, data);
|
|
||||||
return await response.json();
|
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["/api/ldap-queries"] });
|
|
||||||
setActiveQuery(null);
|
|
||||||
toast({
|
|
||||||
title: "Query updated",
|
|
||||||
description: "The LDAP query has been updated successfully.",
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
toast({
|
|
||||||
title: "Error updating query",
|
|
||||||
description: error.message,
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Function to handle query deletion
|
|
||||||
const deleteQuery = async (id: number) => {
|
|
||||||
try {
|
|
||||||
await apiRequest("DELETE", `/api/ldap-queries/${id}`);
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["/api/ldap-queries"] });
|
|
||||||
toast({
|
|
||||||
title: "Query deleted",
|
|
||||||
description: "The LDAP query has been deleted successfully.",
|
|
||||||
});
|
|
||||||
} catch (error: any) {
|
|
||||||
toast({
|
|
||||||
title: "Error deleting query",
|
|
||||||
description: error.message,
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Function to load a query for editing
|
|
||||||
const loadQuery = (query: LdapQuery) => {
|
|
||||||
setActiveQuery(query);
|
|
||||||
setQueryBuilderParams({
|
|
||||||
targetObject: query.targetObject as "users" | "groups" | "computers" | "ous",
|
|
||||||
filter: query.filterJson as LdapCondition
|
|
||||||
});
|
|
||||||
setQueryName(query.name);
|
|
||||||
setQueryDescription(query.description || "");
|
|
||||||
};
|
|
||||||
|
|
||||||
// Function to handle saving a query
|
|
||||||
const handleSaveQuery = () => {
|
|
||||||
if (activeQuery) {
|
|
||||||
updateQueryMutation.mutate({
|
|
||||||
id: activeQuery.id,
|
|
||||||
name: queryName,
|
|
||||||
description: queryDescription,
|
|
||||||
targetObject: queryBuilderParams.targetObject,
|
|
||||||
filter: queryBuilderParams.filter
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setShowCreateDialog(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Function to handle testing a query
|
|
||||||
const handleTestQuery = () => {
|
|
||||||
if (connections.length === 0) {
|
|
||||||
toast({
|
|
||||||
title: "No connections available",
|
|
||||||
description: "Please add an LDAP connection first.",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
testQueryMutation.mutate({
|
|
||||||
connectionId: connections[0].id,
|
|
||||||
targetObject: queryBuilderParams.targetObject,
|
|
||||||
filter: queryBuilderParams.filter,
|
|
||||||
limit: 100
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Generate columns for the saved queries table
|
|
||||||
const queriesColumns = [
|
|
||||||
{
|
|
||||||
header: "Name",
|
|
||||||
accessorKey: "name" as keyof LdapQuery,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "Description",
|
|
||||||
accessorKey: "description" as keyof LdapQuery,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "Target",
|
|
||||||
accessorKey: "targetObject" as keyof LdapQuery,
|
|
||||||
cell: (row: LdapQuery) => {
|
|
||||||
const targets: Record<string, string> = {
|
|
||||||
users: "Users",
|
|
||||||
groups: "Groups",
|
|
||||||
computers: "Computers",
|
|
||||||
ous: "Organizational Units",
|
|
||||||
};
|
|
||||||
return targets[row.targetObject] || row.targetObject;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "Last Updated",
|
|
||||||
accessorKey: "updatedAt" as keyof LdapQuery,
|
|
||||||
cell: (row: LdapQuery) =>
|
|
||||||
row.updatedAt ? format(new Date(row.updatedAt), "MMM dd, yyyy HH:mm") : "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: "",
|
|
||||||
accessorKey: "id" as keyof LdapQuery,
|
|
||||||
cell: (row: LdapQuery) => (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="ghost" size="icon">
|
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuItem onClick={() => loadQuery(row)}>
|
|
||||||
<Code className="h-4 w-4 mr-2" />
|
|
||||||
Edit
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => {
|
|
||||||
testQueryMutation.mutate({
|
|
||||||
connectionId: connections.length > 0 ? connections[0].id : 0,
|
|
||||||
targetObject: row.targetObject,
|
|
||||||
filter: row.filterJson as LdapCondition,
|
|
||||||
limit: 100
|
|
||||||
});
|
|
||||||
}}>
|
|
||||||
<Play className="h-4 w-4 mr-2" />
|
|
||||||
Test
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={() => deleteQuery(row.id)}>
|
|
||||||
<FileClock className="h-4 w-4 mr-2" />
|
|
||||||
Delete
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Generate columns for the test results table dynamically based on the returned attributes
|
|
||||||
const getTestResultsColumns = () => {
|
|
||||||
if (!testResults || testResults.length === 0) return [];
|
|
||||||
|
|
||||||
const firstResult = testResults[0];
|
|
||||||
return Object.keys(firstResult).map(key => ({
|
|
||||||
header: key,
|
|
||||||
accessorKey: key as keyof typeof firstResult,
|
|
||||||
cell: (row: any) => {
|
|
||||||
const value = row[key];
|
|
||||||
if (value === null || value === undefined) return "-";
|
|
||||||
if (typeof value === "boolean") return value ? "Yes" : "No";
|
|
||||||
if (Array.isArray(value)) return value.join(", ");
|
|
||||||
return String(value);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DashboardLayout
|
|
||||||
title="LDAP Query Builder"
|
|
||||||
description="Build and save LDAP queries for your directory"
|
|
||||||
>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
||||||
{/* Left panel - Saved Queries */}
|
|
||||||
<div className="md:col-span-1">
|
|
||||||
<Card className="h-full">
|
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
||||||
<div>
|
|
||||||
<CardTitle>Saved Queries</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
Reuse and manage your saved LDAP queries
|
|
||||||
</CardDescription>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
onClick={() => {
|
|
||||||
setActiveQuery(null);
|
|
||||||
setQueryBuilderParams({
|
|
||||||
targetObject: "users",
|
|
||||||
filter: {
|
|
||||||
operator: LdapOperator.AND,
|
|
||||||
conditions: []
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setQueryName("");
|
|
||||||
setQueryDescription("");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Plus className="h-4 w-4 mr-2" />
|
|
||||||
New
|
|
||||||
</Button>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="h-[calc(100vh-300px)] overflow-auto">
|
|
||||||
<DataTable
|
|
||||||
data={savedQueries}
|
|
||||||
columns={queriesColumns}
|
|
||||||
isLoading={isLoadingQueries}
|
|
||||||
searchable
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right panel - Query Builder */}
|
|
||||||
<div className="md:col-span-2">
|
|
||||||
<Card className="h-full">
|
|
||||||
<CardHeader className="pb-2">
|
|
||||||
<div className="flex flex-col sm:flex-row justify-between sm:items-center gap-2">
|
|
||||||
<div>
|
|
||||||
<CardTitle>
|
|
||||||
{activeQuery ? "Edit Query" : "New Query"}
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription>
|
|
||||||
{activeQuery ? `Editing: ${activeQuery.name}` : "Create a new LDAP query"}
|
|
||||||
</CardDescription>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={handleTestQuery}
|
|
||||||
disabled={isLoadingConnections || connections.length === 0}
|
|
||||||
>
|
|
||||||
{testQueryMutation.isPending ? (
|
|
||||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Play className="h-4 w-4 mr-2" />
|
|
||||||
)}
|
|
||||||
Test Query
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleSaveQuery}
|
|
||||||
disabled={createQueryMutation.isPending || updateQueryMutation.isPending}
|
|
||||||
>
|
|
||||||
{(createQueryMutation.isPending || updateQueryMutation.isPending) ? (
|
|
||||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Save className="h-4 w-4 mr-2" />
|
|
||||||
)}
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{activeQuery ? (
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="queryName">Query Name</Label>
|
|
||||||
<Input
|
|
||||||
id="queryName"
|
|
||||||
value={queryName}
|
|
||||||
onChange={(e) => setQueryName(e.target.value)}
|
|
||||||
placeholder="My Query"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label htmlFor="queryDescription">Description</Label>
|
|
||||||
<Input
|
|
||||||
id="queryDescription"
|
|
||||||
value={queryDescription}
|
|
||||||
onChange={(e) => setQueryDescription(e.target.value)}
|
|
||||||
placeholder="What does this query do?"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div className="pt-2">
|
|
||||||
<EnhancedLdapQueryBuilder
|
|
||||||
connections={connections}
|
|
||||||
value={queryBuilderParams}
|
|
||||||
onChange={setQueryBuilderParams}
|
|
||||||
onTest={handleTestQuery}
|
|
||||||
onSave={handleSaveQuery}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Create Query Dialog */}
|
|
||||||
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Save Query</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Give your query a name and description for future reference.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="space-y-4 py-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="name">Query Name</Label>
|
|
||||||
<Input
|
|
||||||
id="name"
|
|
||||||
value={queryName}
|
|
||||||
onChange={(e) => setQueryName(e.target.value)}
|
|
||||||
placeholder="Enter a name for your query"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="description">Description (Optional)</Label>
|
|
||||||
<Textarea
|
|
||||||
id="description"
|
|
||||||
value={queryDescription}
|
|
||||||
onChange={(e) => setQueryDescription(e.target.value)}
|
|
||||||
placeholder="What does this query do?"
|
|
||||||
rows={3}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="outline" onClick={() => setShowCreateDialog(false)}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
if (!queryName) {
|
|
||||||
toast({
|
|
||||||
title: "Name required",
|
|
||||||
description: "Please enter a name for your query.",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
createQueryMutation.mutate({
|
|
||||||
name: queryName,
|
|
||||||
description: queryDescription,
|
|
||||||
targetObject: queryBuilderParams.targetObject,
|
|
||||||
filter: queryBuilderParams.filter
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
disabled={createQueryMutation.isPending}
|
|
||||||
>
|
|
||||||
{createQueryMutation.isPending ? (
|
|
||||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
||||||
) : null}
|
|
||||||
Save Query
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
{/* Test Results Dialog */}
|
|
||||||
<Dialog open={showTestDialog} onOpenChange={setShowTestDialog}>
|
|
||||||
<DialogContent className="max-w-5xl max-h-[80vh] overflow-hidden flex flex-col">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Query Results</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Displaying {testResults?.length || 0} results from the LDAP query.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="flex-1 overflow-auto py-4">
|
|
||||||
{testResults && testResults.length > 0 ? (
|
|
||||||
<DataTable
|
|
||||||
data={testResults}
|
|
||||||
columns={getTestResultsColumns()}
|
|
||||||
searchable
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="text-center py-8 text-muted-foreground">
|
|
||||||
{testQueryMutation.isPending ? (
|
|
||||||
<div className="flex flex-col items-center">
|
|
||||||
<Loader2 className="h-8 w-8 animate-spin mb-2" />
|
|
||||||
Executing query...
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
"No results found"
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button onClick={() => setShowTestDialog(false)}>
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</DashboardLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Generated
-225
@@ -44,7 +44,6 @@
|
|||||||
"@types/compression": "^1.7.5",
|
"@types/compression": "^1.7.5",
|
||||||
"@types/debug": "^4.1.12",
|
"@types/debug": "^4.1.12",
|
||||||
"@types/jsonwebtoken": "^9.0.9",
|
"@types/jsonwebtoken": "^9.0.9",
|
||||||
"@types/ldapjs": "^3.0.6",
|
|
||||||
"@types/morgan": "^1.9.9",
|
"@types/morgan": "^1.9.9",
|
||||||
"@types/passport-jwt": "^4.0.1",
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"@types/swagger-jsdoc": "^6.0.4",
|
"@types/swagger-jsdoc": "^6.0.4",
|
||||||
@@ -66,7 +65,6 @@
|
|||||||
"input-otp": "^1.2.4",
|
"input-otp": "^1.2.4",
|
||||||
"ioredis": "^5.6.0",
|
"ioredis": "^5.6.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"ldapjs": "^3.0.7",
|
|
||||||
"lucide-react": "^0.453.0",
|
"lucide-react": "^0.453.0",
|
||||||
"memorystore": "^1.6.7",
|
"memorystore": "^1.6.7",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
@@ -1460,101 +1458,6 @@
|
|||||||
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
|
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@ldapjs/asn1": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ldapjs/asn1/-/asn1-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-G9+DkEOirNgdPmD0I8nu57ygQJKOOgFEMKknEuQvIHbGLwP3ny1mY+OTUYLCbCaGJP4sox5eYgBJRuSUpnAddA==",
|
|
||||||
"deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@ldapjs/attribute": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ldapjs/attribute/-/attribute-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-ptMl2d/5xJ0q+RgmnqOi3Zgwk/TMJYG7dYMC0Keko+yZU6n+oFM59MjQOUht5pxJeS4FWrImhu/LebX24vJNRQ==",
|
|
||||||
"deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@ldapjs/asn1": "2.0.0",
|
|
||||||
"@ldapjs/protocol": "^1.2.1",
|
|
||||||
"process-warning": "^2.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@ldapjs/change": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ldapjs/change/-/change-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-EOQNFH1RIku3M1s0OAJOzGfAohuFYXFY4s73wOhRm4KFGhmQQ7MChOh2YtYu9Kwgvuq1B0xKciXVzHCGkB5V+Q==",
|
|
||||||
"deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@ldapjs/asn1": "2.0.0",
|
|
||||||
"@ldapjs/attribute": "1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@ldapjs/controls": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ldapjs/controls/-/controls-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-2pFdD1yRC9V9hXfAWvCCO2RRWK9OdIEcJIos/9cCVP9O4k72BY1bLDQQ4KpUoJnl4y/JoD4iFgM+YWT3IfITWw==",
|
|
||||||
"deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@ldapjs/asn1": "^1.2.0",
|
|
||||||
"@ldapjs/protocol": "^1.2.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@ldapjs/controls/node_modules/@ldapjs/asn1": {
|
|
||||||
"version": "1.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ldapjs/asn1/-/asn1-1.2.0.tgz",
|
|
||||||
"integrity": "sha512-KX/qQJ2xxzvO2/WOvr1UdQ+8P5dVvuOLk/C9b1bIkXxZss8BaR28njXdPgFCpj5aHaf1t8PmuVnea+N9YG9YMw==",
|
|
||||||
"deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@ldapjs/dn": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ldapjs/dn/-/dn-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-R72zH5ZeBj/Fujf/yBu78YzpJjJXG46YHFo5E4W1EqfNpo1UsVPqdLrRMXeKIsJT3x9dJVIfR6OpzgINlKpi0A==",
|
|
||||||
"deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@ldapjs/asn1": "2.0.0",
|
|
||||||
"process-warning": "^2.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@ldapjs/filter": {
|
|
||||||
"version": "2.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ldapjs/filter/-/filter-2.1.1.tgz",
|
|
||||||
"integrity": "sha512-TwPK5eEgNdUO1ABPBUQabcZ+h9heDORE4V9WNZqCtYLKc06+6+UAJ3IAbr0L0bYTnkkWC/JEQD2F+zAFsuikNw==",
|
|
||||||
"deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@ldapjs/asn1": "2.0.0",
|
|
||||||
"@ldapjs/protocol": "^1.2.1",
|
|
||||||
"process-warning": "^2.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@ldapjs/messages": {
|
|
||||||
"version": "1.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ldapjs/messages/-/messages-1.3.0.tgz",
|
|
||||||
"integrity": "sha512-K7xZpXJ21bj92jS35wtRbdcNrwmxAtPwy4myeh9duy/eR3xQKvikVycbdWVzkYEAVE5Ce520VXNOwCHjomjCZw==",
|
|
||||||
"deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@ldapjs/asn1": "^2.0.0",
|
|
||||||
"@ldapjs/attribute": "^1.0.0",
|
|
||||||
"@ldapjs/change": "^1.0.0",
|
|
||||||
"@ldapjs/controls": "^2.1.0",
|
|
||||||
"@ldapjs/dn": "^1.1.0",
|
|
||||||
"@ldapjs/filter": "^2.1.1",
|
|
||||||
"@ldapjs/protocol": "^1.2.1",
|
|
||||||
"process-warning": "^2.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@ldapjs/protocol": {
|
|
||||||
"version": "1.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ldapjs/protocol/-/protocol-1.2.1.tgz",
|
|
||||||
"integrity": "sha512-O89xFDLW2gBoZWNXuXpBSM32/KealKCTb3JGtJdtUQc7RjAk8XzrRgyz02cPAwGKwKPxy0ivuC7UP9bmN87egQ==",
|
|
||||||
"deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@neondatabase/serverless": {
|
"node_modules/@neondatabase/serverless": {
|
||||||
"version": "0.10.4",
|
"version": "0.10.4",
|
||||||
"resolved": "https://registry.npmjs.org/@neondatabase/serverless/-/serverless-0.10.4.tgz",
|
"resolved": "https://registry.npmjs.org/@neondatabase/serverless/-/serverless-0.10.4.tgz",
|
||||||
@@ -3638,15 +3541,6 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/ldapjs": {
|
|
||||||
"version": "3.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/ldapjs/-/ldapjs-3.0.6.tgz",
|
|
||||||
"integrity": "sha512-E2Tn1ltJDYBsidOT9QG4engaQeQzRQ9aYNxVmjCkD33F7cIeLPgrRDXAYs0O35mK2YDU20c/+ZkNjeAPRGLM0Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/mime": {
|
"node_modules/@types/mime": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||||
@@ -3836,12 +3730,6 @@
|
|||||||
"vite": "^4.2.0 || ^5.0.0"
|
"vite": "^4.2.0 || ^5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/abstract-logging": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/accepts": {
|
"node_modules/accepts": {
|
||||||
"version": "1.3.8",
|
"version": "1.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||||
@@ -3928,15 +3816,6 @@
|
|||||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/assert-plus": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/autoprefixer": {
|
"node_modules/autoprefixer": {
|
||||||
"version": "10.4.20",
|
"version": "10.4.20",
|
||||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
|
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
|
||||||
@@ -3975,18 +3854,6 @@
|
|||||||
"postcss": "^8.1.0"
|
"postcss": "^8.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/backoff": {
|
|
||||||
"version": "2.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz",
|
|
||||||
"integrity": "sha512-wC5ihrnUXmR2douXmXLCe5O3zg3GKIyvRi/hi58a/XyRxVI+3/yM0PYueQOZXPXQ9pxBislYkw+sF9b7C/RuMA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"precond": "0.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
@@ -4446,12 +4313,6 @@
|
|||||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
|
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/core-util-is": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -5629,15 +5490,6 @@
|
|||||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/extsprintf": {
|
|
||||||
"version": "1.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz",
|
|
||||||
"integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==",
|
|
||||||
"engines": [
|
|
||||||
"node >=0.6.0"
|
|
||||||
],
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/fast-equals": {
|
"node_modules/fast-equals": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.0.1.tgz",
|
||||||
@@ -6296,29 +6148,6 @@
|
|||||||
"safe-buffer": "^5.0.1"
|
"safe-buffer": "^5.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ldapjs": {
|
|
||||||
"version": "3.0.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-3.0.7.tgz",
|
|
||||||
"integrity": "sha512-1ky+WrN+4CFMuoekUOv7Y1037XWdjKpu0xAPwSP+9KdvmV9PG+qOKlssDV6a+U32apwxdD3is/BZcWOYzN30cg==",
|
|
||||||
"deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@ldapjs/asn1": "^2.0.0",
|
|
||||||
"@ldapjs/attribute": "^1.0.0",
|
|
||||||
"@ldapjs/change": "^1.0.0",
|
|
||||||
"@ldapjs/controls": "^2.1.0",
|
|
||||||
"@ldapjs/dn": "^1.1.0",
|
|
||||||
"@ldapjs/filter": "^2.1.1",
|
|
||||||
"@ldapjs/messages": "^1.3.0",
|
|
||||||
"@ldapjs/protocol": "^1.2.1",
|
|
||||||
"abstract-logging": "^2.0.1",
|
|
||||||
"assert-plus": "^1.0.0",
|
|
||||||
"backoff": "^2.5.0",
|
|
||||||
"once": "^1.4.0",
|
|
||||||
"vasync": "^2.2.1",
|
|
||||||
"verror": "^1.10.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/lilconfig": {
|
"node_modules/lilconfig": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
|
||||||
@@ -7332,20 +7161,6 @@
|
|||||||
"integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==",
|
"integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/precond": {
|
|
||||||
"version": "0.2.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz",
|
|
||||||
"integrity": "sha512-QCYG84SgGyGzqJ/vlMsxeXd/pgL/I94ixdNFyh1PusWmTCyVfPJjZ1K1jvHtsbfnXQs2TSkEP2fR7QiMZAnKFQ==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/process-warning": {
|
|
||||||
"version": "2.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.3.2.tgz",
|
|
||||||
"integrity": "sha512-n9wh8tvBe5sFmsqlg+XQhaQLumwpqoAUruLwjCopgTmUBjJ/fjtBsJzKleCaIGBOMXYEhp1YfKl4d7rJ5ZKJGA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/prop-types": {
|
"node_modules/prop-types": {
|
||||||
"version": "15.8.1",
|
"version": "15.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
@@ -9082,32 +8897,6 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vasync": {
|
|
||||||
"version": "2.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/vasync/-/vasync-2.2.1.tgz",
|
|
||||||
"integrity": "sha512-Hq72JaTpcTFdWiNA4Y22Amej2GH3BFmBaKPPlDZ4/oC8HNn2ISHLkFrJU4Ds8R3jcUi7oo5Y9jcMHKjES+N9wQ==",
|
|
||||||
"engines": [
|
|
||||||
"node >=0.6.0"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"verror": "1.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vasync/node_modules/verror": {
|
|
||||||
"version": "1.10.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
|
|
||||||
"integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==",
|
|
||||||
"engines": [
|
|
||||||
"node >=0.6.0"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"assert-plus": "^1.0.0",
|
|
||||||
"core-util-is": "1.0.2",
|
|
||||||
"extsprintf": "^1.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vaul": {
|
"node_modules/vaul": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.1.tgz",
|
||||||
@@ -9121,20 +8910,6 @@
|
|||||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0"
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/verror": {
|
|
||||||
"version": "1.10.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz",
|
|
||||||
"integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"assert-plus": "^1.0.0",
|
|
||||||
"core-util-is": "1.0.2",
|
|
||||||
"extsprintf": "^1.2.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.6.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/victory-vendor": {
|
"node_modules/victory-vendor": {
|
||||||
"version": "36.9.2",
|
"version": "36.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
|
||||||
|
|||||||
@@ -46,7 +46,6 @@
|
|||||||
"@types/compression": "^1.7.5",
|
"@types/compression": "^1.7.5",
|
||||||
"@types/debug": "^4.1.12",
|
"@types/debug": "^4.1.12",
|
||||||
"@types/jsonwebtoken": "^9.0.9",
|
"@types/jsonwebtoken": "^9.0.9",
|
||||||
"@types/ldapjs": "^3.0.6",
|
|
||||||
"@types/morgan": "^1.9.9",
|
"@types/morgan": "^1.9.9",
|
||||||
"@types/passport-jwt": "^4.0.1",
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"@types/swagger-jsdoc": "^6.0.4",
|
"@types/swagger-jsdoc": "^6.0.4",
|
||||||
@@ -68,7 +67,6 @@
|
|||||||
"input-otp": "^1.2.4",
|
"input-otp": "^1.2.4",
|
||||||
"ioredis": "^5.6.0",
|
"ioredis": "^5.6.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"ldapjs": "^3.0.7",
|
|
||||||
"lucide-react": "^0.453.0",
|
"lucide-react": "^0.453.0",
|
||||||
"memorystore": "^1.6.7",
|
"memorystore": "^1.6.7",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
|
|||||||
+45
-124
@@ -95,132 +95,62 @@ export function setupAuth(app: Express) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Registration endpoint - fixed with better error handling
|
// Registration endpoint
|
||||||
app.post("/api/register", async (req, res, next) => {
|
app.post("/api/register", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
console.log("Registration attempt:", { ...req.body, password: "***" });
|
|
||||||
|
|
||||||
// Validate inputs
|
|
||||||
const validationResult = loginSchema.safeParse(req.body);
|
const validationResult = loginSchema.safeParse(req.body);
|
||||||
if (!validationResult.success) {
|
if (!validationResult.success) {
|
||||||
console.log("Validation failed:", validationResult.error.errors);
|
|
||||||
return res.status(400).json({ message: "Invalid input", errors: validationResult.error.errors });
|
return res.status(400).json({ message: "Invalid input", errors: validationResult.error.errors });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
|
const existingUser = await storage.getUserByUsername(username);
|
||||||
// Check for existing user
|
if (existingUser) {
|
||||||
try {
|
return res.status(400).json({ message: "Username already exists" });
|
||||||
const existingUser = await storage.getUserByUsername(username);
|
|
||||||
if (existingUser) {
|
|
||||||
console.log("Username already exists:", username);
|
|
||||||
return res.status(400).json({ message: "Username already exists" });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error checking for existing user:", error);
|
|
||||||
return res.status(500).json({ message: "Error checking for existing user" });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash password
|
const hashedPassword = await hashPassword(password);
|
||||||
let hashedPassword;
|
// Get the default role if role ID isn't specified
|
||||||
try {
|
|
||||||
hashedPassword = await hashPassword(password);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error hashing password:", error);
|
|
||||||
return res.status(500).json({ message: "Error processing password" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get default role
|
|
||||||
let roleId = req.body.roleId;
|
let roleId = req.body.roleId;
|
||||||
if (!roleId) {
|
if (!roleId) {
|
||||||
try {
|
const defaultRole = await storage.getDefaultRole();
|
||||||
const defaultRole = await storage.getDefaultRole();
|
roleId = defaultRole?.id;
|
||||||
roleId = defaultRole?.id;
|
|
||||||
console.log("Using default role ID:", roleId);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error getting default role:", error);
|
|
||||||
return res.status(500).json({ message: "Error getting default role" });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create user
|
const user = await storage.createUser({
|
||||||
let user;
|
username,
|
||||||
try {
|
password: hashedPassword,
|
||||||
user = await storage.createUser({
|
email: req.body.email,
|
||||||
username,
|
fullName: req.body.fullName,
|
||||||
password: hashedPassword,
|
roleId: roleId,
|
||||||
email: req.body.email || null,
|
});
|
||||||
fullName: req.body.fullName || null,
|
|
||||||
roleId: roleId,
|
|
||||||
});
|
|
||||||
console.log("User created successfully:", { id: user.id, username });
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error creating user:", error);
|
|
||||||
return res.status(500).json({ message: "Error creating user" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove password from response
|
// Remove password from response
|
||||||
const userResponse = { ...user, password: undefined };
|
const userResponse = { ...user, password: undefined };
|
||||||
|
|
||||||
// Manual login instead of using req.login
|
req.login(user, (err) => {
|
||||||
try {
|
if (err) return next(err);
|
||||||
req.user = user;
|
res.status(201).json(userResponse);
|
||||||
// Use req.session to store user data
|
});
|
||||||
if (req.session) {
|
|
||||||
req.session.userId = user.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send success response
|
|
||||||
console.log("Registration completed successfully");
|
|
||||||
return res.status(201).json(userResponse);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error during session setup:", error);
|
|
||||||
// Still return the created user even if session setup fails
|
|
||||||
return res.status(201).json({
|
|
||||||
...userResponse,
|
|
||||||
warning: "User created but session setup failed, please log in manually"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Unexpected error during registration:", error);
|
next(error);
|
||||||
return res.status(500).json({ message: "Internal server error during registration" });
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Login endpoint - fixed without relying on req.isAuthenticated
|
// Login endpoint
|
||||||
app.post("/api/login", (req, res, next) => {
|
app.post("/api/login", (req, res, next) => {
|
||||||
try {
|
passport.authenticate("local", (err: any, user: any, info: any) => {
|
||||||
passport.authenticate("local", (err: any, user: any, info: any) => {
|
if (err) return next(err);
|
||||||
if (err) {
|
if (!user) {
|
||||||
console.error("Authentication error:", err);
|
return res.status(401).json({ message: info?.message || "Authentication failed" });
|
||||||
return res.status(500).json({ message: "Internal server error during authentication" });
|
}
|
||||||
}
|
req.login(user, (loginErr) => {
|
||||||
|
if (loginErr) return next(loginErr);
|
||||||
if (!user) {
|
// Remove password from response
|
||||||
return res.status(401).json({ message: info?.message || "Authentication failed" });
|
const userResponse = { ...user, password: undefined };
|
||||||
}
|
res.json(userResponse);
|
||||||
|
});
|
||||||
// Manual login with try-catch to safely handle errors
|
})(req, res, next);
|
||||||
try {
|
|
||||||
req.login(user, (loginErr) => {
|
|
||||||
if (loginErr) {
|
|
||||||
console.error("Login error:", loginErr);
|
|
||||||
return res.status(500).json({ message: "Error during login process" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove password from response
|
|
||||||
const userResponse = { ...user, password: undefined };
|
|
||||||
return res.json(userResponse);
|
|
||||||
});
|
|
||||||
} catch (loginError) {
|
|
||||||
console.error("Exception during login:", loginError);
|
|
||||||
return res.status(500).json({ message: "Login process failed" });
|
|
||||||
}
|
|
||||||
})(req, res, next);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Unexpected error in login route:", error);
|
|
||||||
return res.status(500).json({ message: "Internal server error" });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Logout endpoint
|
// Logout endpoint
|
||||||
@@ -235,31 +165,23 @@ export function setupAuth(app: Express) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get current user endpoint - fixed without relying on req.isAuthenticated
|
// Get current user endpoint
|
||||||
app.get("/api/user", (req, res) => {
|
app.get("/api/user", (req, res) => {
|
||||||
try {
|
if (!req.isAuthenticated()) {
|
||||||
// Check if user exists in session instead of using isAuthenticated
|
return res.status(401).json({ message: "Not authenticated" });
|
||||||
if (!req.user) {
|
|
||||||
return res.status(401).json({ message: "Not authenticated" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove password from response
|
|
||||||
const userResponse = { ...req.user, password: undefined };
|
|
||||||
res.json(userResponse);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error in user endpoint:", error);
|
|
||||||
res.status(500).json({ message: "Internal server error" });
|
|
||||||
}
|
}
|
||||||
|
// Remove password from response
|
||||||
|
const userResponse = { ...req.user, password: undefined };
|
||||||
|
res.json(userResponse);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Generate API token endpoint - fixed without relying on req.isAuthenticated
|
// Generate API token endpoint
|
||||||
app.post("/api/tokens", (req, res, next) => {
|
app.post("/api/tokens", (req, res, next) => {
|
||||||
try {
|
if (!req.isAuthenticated()) {
|
||||||
// Check if user exists in session
|
return res.status(401).json({ message: "Not authenticated" });
|
||||||
if (!req.user) {
|
}
|
||||||
return res.status(401).json({ message: "Not authenticated" });
|
|
||||||
}
|
|
||||||
|
|
||||||
|
try {
|
||||||
const { name, expiresAt, roleId, customPermissions } = req.body;
|
const { name, expiresAt, roleId, customPermissions } = req.body;
|
||||||
if (!name) {
|
if (!name) {
|
||||||
return res.status(400).json({ message: "Token name is required" });
|
return res.status(400).json({ message: "Token name is required" });
|
||||||
@@ -289,8 +211,7 @@ export function setupAuth(app: Express) {
|
|||||||
|
|
||||||
res.status(201).json(apiToken);
|
res.status(201).json(apiToken);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error generating API token:", error);
|
next(error);
|
||||||
res.status(500).json({ message: "Failed to generate API token" });
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+49
-54
@@ -16,66 +16,61 @@ export type RequirePermissionOptions = RequireAuthOptions & {
|
|||||||
// Default middleware that verifies a user is authenticated
|
// Default middleware that verifies a user is authenticated
|
||||||
export function requireAuth(options: RequireAuthOptions = {}) {
|
export function requireAuth(options: RequireAuthOptions = {}) {
|
||||||
return async (req: Request, res: Response, next: NextFunction) => {
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
// Check for session authentication
|
||||||
// Check for session authentication - just check if req.user exists
|
if (req.isAuthenticated()) {
|
||||||
if (req.user) {
|
return next();
|
||||||
return next();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Check for API token authentication if allowed
|
// Check for API token authentication if allowed
|
||||||
if (options.allowApiToken) {
|
if (options.allowApiToken) {
|
||||||
const authHeader = req.headers.authorization;
|
const authHeader = req.headers.authorization;
|
||||||
if (authHeader && authHeader.startsWith("Bearer ")) {
|
if (authHeader && authHeader.startsWith("Bearer ")) {
|
||||||
const token = authHeader.substring(7);
|
const token = authHeader.substring(7);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Lookup the token
|
// Lookup the token
|
||||||
const [apiToken] = await db.select()
|
const [apiToken] = await db.select()
|
||||||
.from(apiTokens)
|
.from(apiTokens)
|
||||||
.where(eq(apiTokens.token, token))
|
.where(eq(apiTokens.token, token))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!apiToken) {
|
if (!apiToken) {
|
||||||
return res.status(401).json({ message: "Invalid API token" });
|
return res.status(401).json({ message: "Invalid API token" });
|
||||||
}
|
|
||||||
|
|
||||||
// Check if token has expired
|
|
||||||
if (apiToken.expiresAt && new Date(apiToken.expiresAt) < new Date()) {
|
|
||||||
return res.status(401).json({ message: "API token has expired" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set token info on the request
|
|
||||||
req.user = {
|
|
||||||
id: apiToken.userId,
|
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
fullName: null,
|
|
||||||
email: null,
|
|
||||||
createdAt: null,
|
|
||||||
// Add tokenId for identifying which token was used
|
|
||||||
tokenId: apiToken.id,
|
|
||||||
// Add roleId for permission checking
|
|
||||||
roleId: apiToken.roleId,
|
|
||||||
// Store custom permissions if available
|
|
||||||
customPermissions: (Array.isArray(apiToken.customPermissions) ?
|
|
||||||
apiToken.customPermissions :
|
|
||||||
(apiToken.customPermissions ? JSON.parse(String(apiToken.customPermissions)) : [])) as string[]
|
|
||||||
};
|
|
||||||
|
|
||||||
return next();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("API token authentication error:", error);
|
|
||||||
return res.status(500).json({ message: "Internal server error" });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if token has expired
|
||||||
|
if (apiToken.expiresAt && new Date(apiToken.expiresAt) < new Date()) {
|
||||||
|
return res.status(401).json({ message: "API token has expired" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set token info on the request
|
||||||
|
req.user = {
|
||||||
|
id: apiToken.userId,
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
fullName: null,
|
||||||
|
email: null,
|
||||||
|
createdAt: null,
|
||||||
|
// Add tokenId for identifying which token was used
|
||||||
|
tokenId: apiToken.id,
|
||||||
|
// Add roleId for permission checking
|
||||||
|
roleId: apiToken.roleId,
|
||||||
|
// Store custom permissions if available
|
||||||
|
customPermissions: (Array.isArray(apiToken.customPermissions) ?
|
||||||
|
apiToken.customPermissions :
|
||||||
|
(apiToken.customPermissions ? JSON.parse(String(apiToken.customPermissions)) : [])) as string[]
|
||||||
|
};
|
||||||
|
|
||||||
|
return next();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("API token authentication error:", error);
|
||||||
|
return res.status(500).json({ message: "Internal server error" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not authenticated through any method
|
|
||||||
return res.status(401).json({ message: "Not authenticated" });
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Authentication error:", error);
|
|
||||||
return res.status(500).json({ message: "Internal server error during authentication" });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Not authenticated through any method
|
||||||
|
return res.status(401).json({ message: "Not authenticated" });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,194 +0,0 @@
|
|||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
// Define the operator types for LDAP filters
|
|
||||||
export enum LdapOperator {
|
|
||||||
// Logical operators
|
|
||||||
AND = "and",
|
|
||||||
OR = "or",
|
|
||||||
NOT = "not",
|
|
||||||
|
|
||||||
// Comparison operators
|
|
||||||
EQUALS = "equals",
|
|
||||||
NOT_EQUALS = "notEquals",
|
|
||||||
STARTS_WITH = "startsWith",
|
|
||||||
ENDS_WITH = "endsWith",
|
|
||||||
CONTAINS = "contains",
|
|
||||||
GREATER_THAN = "greaterThan",
|
|
||||||
LESS_THAN = "lessThan",
|
|
||||||
PRESENT = "present", // attribute exists
|
|
||||||
APPROX = "approx", // approximately equals
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define the condition schema for LDAP filter conditions
|
|
||||||
export type LdapCondition = {
|
|
||||||
operator: LdapOperator;
|
|
||||||
attribute?: string; // Not required for logical operators (AND, OR, NOT)
|
|
||||||
value?: string; // Not required for some operators (PRESENT, logical operators)
|
|
||||||
conditions?: LdapCondition[]; // For nested conditions with logical operators
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ldapConditionSchema: z.ZodType<LdapCondition> = z.object({
|
|
||||||
operator: z.nativeEnum(LdapOperator),
|
|
||||||
attribute: z.string().optional(), // Not required for logical operators (AND, OR, NOT)
|
|
||||||
value: z.string().optional(), // Not required for some operators (PRESENT, logical operators)
|
|
||||||
conditions: z.array(z.lazy(() => ldapConditionSchema)).optional(), // For nested conditions with logical operators
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define the query builder schema
|
|
||||||
export const ldapQueryBuilderSchema = z.object({
|
|
||||||
targetObject: z.enum(["users", "groups", "computers", "ous"]),
|
|
||||||
filter: ldapConditionSchema,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Type definitions for the query builder
|
|
||||||
export type LdapQueryBuilder = z.infer<typeof ldapQueryBuilderSchema>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a condition object to a valid LDAP filter string
|
|
||||||
*/
|
|
||||||
export function buildLdapFilter(condition: LdapCondition): string {
|
|
||||||
// Handle logical operators
|
|
||||||
if (condition.operator === LdapOperator.AND && condition.conditions) {
|
|
||||||
const subConditions = condition.conditions.map(buildLdapFilter).join("");
|
|
||||||
return `(&${subConditions})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (condition.operator === LdapOperator.OR && condition.conditions) {
|
|
||||||
const subConditions = condition.conditions.map(buildLdapFilter).join("");
|
|
||||||
return `(|${subConditions})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (condition.operator === LdapOperator.NOT && condition.conditions && condition.conditions.length > 0) {
|
|
||||||
return `(!${buildLdapFilter(condition.conditions[0])})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle comparison operators
|
|
||||||
if (!condition.attribute) {
|
|
||||||
throw new Error(`Attribute is required for operator ${condition.operator}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (condition.operator) {
|
|
||||||
case LdapOperator.EQUALS:
|
|
||||||
return `(${condition.attribute}=${escapeFilterValue(condition.value || "")})`;
|
|
||||||
case LdapOperator.NOT_EQUALS:
|
|
||||||
return `(!(${condition.attribute}=${escapeFilterValue(condition.value || "")}))`;
|
|
||||||
case LdapOperator.STARTS_WITH:
|
|
||||||
return `(${condition.attribute}=${escapeFilterValue(condition.value || "")}*)`;
|
|
||||||
case LdapOperator.ENDS_WITH:
|
|
||||||
return `(${condition.attribute}=*${escapeFilterValue(condition.value || "")})`;
|
|
||||||
case LdapOperator.CONTAINS:
|
|
||||||
return `(${condition.attribute}=*${escapeFilterValue(condition.value || "")}*)`;
|
|
||||||
case LdapOperator.GREATER_THAN:
|
|
||||||
return `(${condition.attribute}>${escapeFilterValue(condition.value || "")})`;
|
|
||||||
case LdapOperator.LESS_THAN:
|
|
||||||
return `(${condition.attribute}<${escapeFilterValue(condition.value || "")})`;
|
|
||||||
case LdapOperator.PRESENT:
|
|
||||||
return `(${condition.attribute}=*)`;
|
|
||||||
case LdapOperator.APPROX:
|
|
||||||
return `(${condition.attribute}~=${escapeFilterValue(condition.value || "")})`;
|
|
||||||
default:
|
|
||||||
throw new Error(`Unsupported operator: ${condition.operator}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get common LDAP object classes for different target object types
|
|
||||||
*/
|
|
||||||
export function getObjectClassFilter(targetObject: string): string {
|
|
||||||
switch (targetObject) {
|
|
||||||
case "users":
|
|
||||||
return "(&(objectClass=user)(!(objectClass=computer)))";
|
|
||||||
case "groups":
|
|
||||||
return "(objectClass=group)";
|
|
||||||
case "computers":
|
|
||||||
return "(objectClass=computer)";
|
|
||||||
case "ous":
|
|
||||||
return "(objectClass=organizationalUnit)";
|
|
||||||
default:
|
|
||||||
return "(objectClass=*)";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a combined LDAP filter by joining the user-defined filter with the appropriate object class filter
|
|
||||||
*/
|
|
||||||
export function generateLdapFilter(queryBuilder: LdapQueryBuilder): string {
|
|
||||||
const objectClassFilter = getObjectClassFilter(queryBuilder.targetObject);
|
|
||||||
const userFilter = buildLdapFilter(queryBuilder.filter);
|
|
||||||
|
|
||||||
// Combine the two filters with AND
|
|
||||||
return `(&${objectClassFilter}${userFilter})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Escape special characters in LDAP filter values according to RFC 4515
|
|
||||||
*/
|
|
||||||
function escapeFilterValue(value: string): string {
|
|
||||||
return value
|
|
||||||
.replace(/\\/g, "\\5c") // Must be first to avoid double escaping
|
|
||||||
.replace(/\*/g, "\\2a")
|
|
||||||
.replace(/\(/g, "\\28")
|
|
||||||
.replace(/\)/g, "\\29")
|
|
||||||
.replace(/\0/g, "\\00")
|
|
||||||
.replace(/\//g, "\\2f");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate a query builder object and ensure it has the required properties
|
|
||||||
*/
|
|
||||||
export function validateQueryBuilder(queryBuilder: unknown): LdapQueryBuilder {
|
|
||||||
return ldapQueryBuilderSchema.parse(queryBuilder);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get human-readable text representation of an LDAP filter condition
|
|
||||||
*/
|
|
||||||
export function getHumanReadableFilter(condition: LdapCondition, indent = 0): string {
|
|
||||||
const spaces = " ".repeat(indent);
|
|
||||||
|
|
||||||
switch (condition.operator) {
|
|
||||||
case LdapOperator.AND:
|
|
||||||
return `${spaces}ALL of the following conditions:\n` +
|
|
||||||
(condition.conditions?.map(c => getHumanReadableFilter(c, indent + 2)).join("\n") || "");
|
|
||||||
|
|
||||||
case LdapOperator.OR:
|
|
||||||
return `${spaces}ANY of the following conditions:\n` +
|
|
||||||
(condition.conditions?.map(c => getHumanReadableFilter(c, indent + 2)).join("\n") || "");
|
|
||||||
|
|
||||||
case LdapOperator.NOT:
|
|
||||||
return `${spaces}NOT the following condition:\n` +
|
|
||||||
(condition.conditions && condition.conditions.length > 0
|
|
||||||
? getHumanReadableFilter(condition.conditions[0], indent + 2)
|
|
||||||
: "");
|
|
||||||
|
|
||||||
case LdapOperator.EQUALS:
|
|
||||||
return `${spaces}${condition.attribute} equals "${condition.value}"`;
|
|
||||||
|
|
||||||
case LdapOperator.NOT_EQUALS:
|
|
||||||
return `${spaces}${condition.attribute} does not equal "${condition.value}"`;
|
|
||||||
|
|
||||||
case LdapOperator.STARTS_WITH:
|
|
||||||
return `${spaces}${condition.attribute} starts with "${condition.value}"`;
|
|
||||||
|
|
||||||
case LdapOperator.ENDS_WITH:
|
|
||||||
return `${spaces}${condition.attribute} ends with "${condition.value}"`;
|
|
||||||
|
|
||||||
case LdapOperator.CONTAINS:
|
|
||||||
return `${spaces}${condition.attribute} contains "${condition.value}"`;
|
|
||||||
|
|
||||||
case LdapOperator.GREATER_THAN:
|
|
||||||
return `${spaces}${condition.attribute} is greater than "${condition.value}"`;
|
|
||||||
|
|
||||||
case LdapOperator.LESS_THAN:
|
|
||||||
return `${spaces}${condition.attribute} is less than "${condition.value}"`;
|
|
||||||
|
|
||||||
case LdapOperator.PRESENT:
|
|
||||||
return `${spaces}${condition.attribute} exists`;
|
|
||||||
|
|
||||||
case LdapOperator.APPROX:
|
|
||||||
return `${spaces}${condition.attribute} is approximately equal to "${condition.value}"`;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return `${spaces}Unknown condition`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,653 +0,0 @@
|
|||||||
import { Router } from "express";
|
|
||||||
import {
|
|
||||||
validateQueryBuilder,
|
|
||||||
generateLdapFilter,
|
|
||||||
getHumanReadableFilter,
|
|
||||||
buildLdapFilter,
|
|
||||||
LdapQueryBuilder
|
|
||||||
} from "./ldap-filter-builder";
|
|
||||||
import { connectToLdap, searchLdap, getLdapAvailableAttributes } from "./ldap";
|
|
||||||
import { IStorage } from "./storage";
|
|
||||||
import { InsertLdapQuery, LdapQuery, InsertLdapQueryVersion } from "@shared/schema";
|
|
||||||
|
|
||||||
import { Request, Response, NextFunction } from "express";
|
|
||||||
|
|
||||||
// Simplified authentication middleware to allow all requests temporarily
|
|
||||||
function hasPermission(permission: string) {
|
|
||||||
return (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
// Allow all requests for testing and debugging
|
|
||||||
return next();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function registerLdapQueryBuilderRoutes(router: Router, storage: IStorage) {
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /ldap-queries:
|
|
||||||
* get:
|
|
||||||
* summary: Get all saved LDAP queries
|
|
||||||
* description: Retrieve a list of all saved LDAP query filters
|
|
||||||
* tags: [LDAP Query Builder]
|
|
||||||
* security:
|
|
||||||
* - bearerAuth: []
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: A list of LDAP queries
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: array
|
|
||||||
* items:
|
|
||||||
* $ref: '#/components/schemas/LdapQuery'
|
|
||||||
*/
|
|
||||||
router.get("/ldap-queries", hasPermission("read:ldap_queries"), async (req, res) => {
|
|
||||||
try {
|
|
||||||
const queries = await storage.getLdapQueries();
|
|
||||||
res.json(queries);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error getting LDAP queries:", error);
|
|
||||||
res.status(500).json({ error: "Failed to retrieve LDAP queries" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /ldap-queries/{id}:
|
|
||||||
* get:
|
|
||||||
* summary: Get a saved LDAP query by ID
|
|
||||||
* description: Retrieve a specific LDAP query filter by its ID
|
|
||||||
* tags: [LDAP Query Builder]
|
|
||||||
* security:
|
|
||||||
* - bearerAuth: []
|
|
||||||
* parameters:
|
|
||||||
* - in: path
|
|
||||||
* name: id
|
|
||||||
* required: true
|
|
||||||
* schema:
|
|
||||||
* type: integer
|
|
||||||
* description: Query ID
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: LDAP query found
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* $ref: '#/components/schemas/LdapQuery'
|
|
||||||
* 404:
|
|
||||||
* description: Query not found
|
|
||||||
*/
|
|
||||||
router.get("/ldap-queries/:id", hasPermission("read:ldap_queries"), async (req, res) => {
|
|
||||||
try {
|
|
||||||
const id = parseInt(req.params.id);
|
|
||||||
if (isNaN(id)) {
|
|
||||||
return res.status(400).json({ error: "Invalid query ID" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = await storage.getLdapQuery(id);
|
|
||||||
if (!query) {
|
|
||||||
return res.status(404).json({ error: "LDAP query not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json(query);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error getting LDAP query:", error);
|
|
||||||
res.status(500).json({ error: "Failed to retrieve LDAP query" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /ldap-queries/{id}/versions:
|
|
||||||
* get:
|
|
||||||
* summary: Get version history for an LDAP query
|
|
||||||
* description: Retrieve the revision history for a specific LDAP query
|
|
||||||
* tags: [LDAP Query Builder]
|
|
||||||
* security:
|
|
||||||
* - bearerAuth: []
|
|
||||||
* parameters:
|
|
||||||
* - in: path
|
|
||||||
* name: id
|
|
||||||
* required: true
|
|
||||||
* schema:
|
|
||||||
* type: integer
|
|
||||||
* description: Query ID
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: List of query versions
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: array
|
|
||||||
* items:
|
|
||||||
* $ref: '#/components/schemas/LdapQueryVersion'
|
|
||||||
* 404:
|
|
||||||
* description: Query not found
|
|
||||||
*/
|
|
||||||
router.get("/ldap-queries/:id/versions", hasPermission("read:ldap_queries"), async (req, res) => {
|
|
||||||
try {
|
|
||||||
const id = parseInt(req.params.id);
|
|
||||||
if (isNaN(id)) {
|
|
||||||
return res.status(400).json({ error: "Invalid query ID" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const query = await storage.getLdapQuery(id);
|
|
||||||
if (!query) {
|
|
||||||
return res.status(404).json({ error: "LDAP query not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const versions = await storage.getLdapQueryVersions(id);
|
|
||||||
res.json(versions);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error getting LDAP query versions:", error);
|
|
||||||
res.status(500).json({ error: "Failed to retrieve LDAP query versions" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /ldap-queries:
|
|
||||||
* post:
|
|
||||||
* summary: Create a new LDAP query
|
|
||||||
* description: Save a new LDAP query filter
|
|
||||||
* tags: [LDAP Query Builder]
|
|
||||||
* security:
|
|
||||||
* - bearerAuth: []
|
|
||||||
* requestBody:
|
|
||||||
* required: true
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* name:
|
|
||||||
* type: string
|
|
||||||
* description:
|
|
||||||
* type: string
|
|
||||||
* targetObject:
|
|
||||||
* type: string
|
|
||||||
* enum: [users, groups, computers, ous]
|
|
||||||
* filter:
|
|
||||||
* type: object
|
|
||||||
* responses:
|
|
||||||
* 201:
|
|
||||||
* description: Query created successfully
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* $ref: '#/components/schemas/LdapQuery'
|
|
||||||
* 400:
|
|
||||||
* description: Invalid query format
|
|
||||||
*/
|
|
||||||
router.post("/ldap-queries", hasPermission("write:ldap_queries"), async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { name, description, targetObject, filter } = req.body;
|
|
||||||
|
|
||||||
if (!name || !targetObject || !filter) {
|
|
||||||
return res.status(400).json({ error: "Missing required fields: name, targetObject, filter" });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Validate the filter format
|
|
||||||
const queryBuilder = validateQueryBuilder({ targetObject, filter });
|
|
||||||
|
|
||||||
// Generate the LDAP filter and readable representation
|
|
||||||
const ldapFilter = generateLdapFilter(queryBuilder);
|
|
||||||
const readableFilter = getHumanReadableFilter(queryBuilder.filter);
|
|
||||||
|
|
||||||
const insertQuery: InsertLdapQuery = {
|
|
||||||
name,
|
|
||||||
description: description || "",
|
|
||||||
targetObject,
|
|
||||||
filterJson: filter as any, // Type safety is handled by schema validation
|
|
||||||
ldapFilter,
|
|
||||||
readableFilter,
|
|
||||||
createdBy: req.user?.id || 1, // Default to 1 if no user
|
|
||||||
};
|
|
||||||
|
|
||||||
const query = await storage.createLdapQuery(insertQuery);
|
|
||||||
|
|
||||||
res.status(201).json(query);
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error("Error validating query:", error);
|
|
||||||
return res.status(400).json({ error: "Invalid query format", details: error.message });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error creating LDAP query:", error);
|
|
||||||
res.status(500).json({ error: "Failed to create LDAP query" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /ldap-queries/{id}:
|
|
||||||
* put:
|
|
||||||
* summary: Update an LDAP query
|
|
||||||
* description: Update an existing LDAP query and create a new version
|
|
||||||
* tags: [LDAP Query Builder]
|
|
||||||
* security:
|
|
||||||
* - bearerAuth: []
|
|
||||||
* parameters:
|
|
||||||
* - in: path
|
|
||||||
* name: id
|
|
||||||
* required: true
|
|
||||||
* schema:
|
|
||||||
* type: integer
|
|
||||||
* description: Query ID
|
|
||||||
* requestBody:
|
|
||||||
* required: true
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* name:
|
|
||||||
* type: string
|
|
||||||
* description:
|
|
||||||
* type: string
|
|
||||||
* targetObject:
|
|
||||||
* type: string
|
|
||||||
* enum: [users, groups, computers, ous]
|
|
||||||
* filter:
|
|
||||||
* type: object
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: Query updated successfully
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* $ref: '#/components/schemas/LdapQuery'
|
|
||||||
* 400:
|
|
||||||
* description: Invalid query format
|
|
||||||
* 404:
|
|
||||||
* description: Query not found
|
|
||||||
*/
|
|
||||||
router.put("/ldap-queries/:id", hasPermission("write:ldap_queries"), async (req, res) => {
|
|
||||||
try {
|
|
||||||
const id = parseInt(req.params.id);
|
|
||||||
if (isNaN(id)) {
|
|
||||||
return res.status(400).json({ error: "Invalid query ID" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { name, description, targetObject, filter } = req.body;
|
|
||||||
|
|
||||||
if (!name || !targetObject || !filter) {
|
|
||||||
return res.status(400).json({ error: "Missing required fields: name, targetObject, filter" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if query exists
|
|
||||||
const existingQuery = await storage.getLdapQuery(id);
|
|
||||||
if (!existingQuery) {
|
|
||||||
return res.status(404).json({ error: "LDAP query not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Validate the filter format
|
|
||||||
const queryBuilder = validateQueryBuilder({ targetObject, filter });
|
|
||||||
|
|
||||||
// Generate the LDAP filter and readable representation
|
|
||||||
const ldapFilter = generateLdapFilter(queryBuilder);
|
|
||||||
const readableFilter = getHumanReadableFilter(queryBuilder.filter);
|
|
||||||
|
|
||||||
// Create a version entry for the previous state
|
|
||||||
const versionEntry: InsertLdapQueryVersion = {
|
|
||||||
queryId: id,
|
|
||||||
version: existingQuery.version,
|
|
||||||
filterJson: existingQuery.filterJson as any, // Type safety is handled by schema validation
|
|
||||||
ldapFilter: existingQuery.ldapFilter,
|
|
||||||
readableFilter: existingQuery.readableFilter,
|
|
||||||
targetObject: existingQuery.targetObject,
|
|
||||||
createdBy: existingQuery.createdBy,
|
|
||||||
modifiedBy: req.user?.id || 1 // Default to 1 if no user
|
|
||||||
};
|
|
||||||
|
|
||||||
await storage.createLdapQueryVersion(versionEntry);
|
|
||||||
|
|
||||||
// Update the query with new values
|
|
||||||
const updatedQuery = await storage.updateLdapQuery(id, {
|
|
||||||
name,
|
|
||||||
description: description || "",
|
|
||||||
targetObject,
|
|
||||||
filterJson: filter as any,
|
|
||||||
ldapFilter,
|
|
||||||
readableFilter,
|
|
||||||
modifiedBy: req.user?.id || 1, // Default to 1 if no user
|
|
||||||
version: existingQuery.version + 1
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(200).json(updatedQuery);
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error("Error validating query:", error);
|
|
||||||
return res.status(400).json({ error: "Invalid query format", details: error.message });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error updating LDAP query:", error);
|
|
||||||
res.status(500).json({ error: "Failed to update LDAP query" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /ldap-queries/{id}:
|
|
||||||
* delete:
|
|
||||||
* summary: Delete an LDAP query
|
|
||||||
* description: Delete an LDAP query and all its versions
|
|
||||||
* tags: [LDAP Query Builder]
|
|
||||||
* security:
|
|
||||||
* - bearerAuth: []
|
|
||||||
* parameters:
|
|
||||||
* - in: path
|
|
||||||
* name: id
|
|
||||||
* required: true
|
|
||||||
* schema:
|
|
||||||
* type: integer
|
|
||||||
* description: Query ID
|
|
||||||
* responses:
|
|
||||||
* 204:
|
|
||||||
* description: Query deleted successfully
|
|
||||||
* 404:
|
|
||||||
* description: Query not found
|
|
||||||
*/
|
|
||||||
router.delete("/ldap-queries/:id", hasPermission("delete:ldap_queries"), async (req, res) => {
|
|
||||||
try {
|
|
||||||
const id = parseInt(req.params.id);
|
|
||||||
if (isNaN(id)) {
|
|
||||||
return res.status(400).json({ error: "Invalid query ID" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if query exists
|
|
||||||
const existingQuery = await storage.getLdapQuery(id);
|
|
||||||
if (!existingQuery) {
|
|
||||||
return res.status(404).json({ error: "LDAP query not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the query - versions will be deleted via cascading foreign key constraints
|
|
||||||
await storage.deleteLdapQuery(id);
|
|
||||||
|
|
||||||
res.status(204).send();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error deleting LDAP query:", error);
|
|
||||||
res.status(500).json({ error: "Failed to delete LDAP query" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /ldap-queries/{id}/revert/{version}:
|
|
||||||
* post:
|
|
||||||
* summary: Revert to a previous version
|
|
||||||
* description: Revert an LDAP query to a specific previous version
|
|
||||||
* tags: [LDAP Query Builder]
|
|
||||||
* security:
|
|
||||||
* - bearerAuth: []
|
|
||||||
* parameters:
|
|
||||||
* - in: path
|
|
||||||
* name: id
|
|
||||||
* required: true
|
|
||||||
* schema:
|
|
||||||
* type: integer
|
|
||||||
* description: Query ID
|
|
||||||
* - in: path
|
|
||||||
* name: version
|
|
||||||
* required: true
|
|
||||||
* schema:
|
|
||||||
* type: integer
|
|
||||||
* description: Version to revert to
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: Query reverted successfully
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* $ref: '#/components/schemas/LdapQuery'
|
|
||||||
* 404:
|
|
||||||
* description: Query or version not found
|
|
||||||
*/
|
|
||||||
router.post("/ldap-queries/:id/revert/:version", hasPermission("write:ldap_queries"), async (req, res) => {
|
|
||||||
try {
|
|
||||||
const id = parseInt(req.params.id);
|
|
||||||
const versionNumber = parseInt(req.params.version);
|
|
||||||
|
|
||||||
if (isNaN(id) || isNaN(versionNumber)) {
|
|
||||||
return res.status(400).json({ error: "Invalid query ID or version" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if query exists
|
|
||||||
const existingQuery = await storage.getLdapQuery(id);
|
|
||||||
if (!existingQuery) {
|
|
||||||
return res.status(404).json({ error: "LDAP query not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the version to revert to - we need to get all versions and find the matching one
|
|
||||||
const allVersions = await storage.getLdapQueryVersions(id);
|
|
||||||
const versionToRevert = allVersions.find(v => v.version === versionNumber);
|
|
||||||
|
|
||||||
if (!versionToRevert) {
|
|
||||||
return res.status(404).json({ error: "Version not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a version entry for the current state
|
|
||||||
const versionEntry: InsertLdapQueryVersion = {
|
|
||||||
queryId: id,
|
|
||||||
version: existingQuery.version,
|
|
||||||
filterJson: existingQuery.filterJson as any, // Type safety is handled by schema validation
|
|
||||||
ldapFilter: existingQuery.ldapFilter,
|
|
||||||
readableFilter: existingQuery.readableFilter,
|
|
||||||
targetObject: existingQuery.targetObject,
|
|
||||||
createdBy: existingQuery.createdBy,
|
|
||||||
modifiedBy: req.user?.id || 1 // Default to 1 if no user
|
|
||||||
};
|
|
||||||
|
|
||||||
await storage.createLdapQueryVersion(versionEntry);
|
|
||||||
|
|
||||||
// Update the query with the version's values
|
|
||||||
const updatedQuery = await storage.updateLdapQuery(id, {
|
|
||||||
targetObject: versionToRevert.targetObject,
|
|
||||||
filterJson: versionToRevert.filterJson as any,
|
|
||||||
ldapFilter: versionToRevert.ldapFilter,
|
|
||||||
readableFilter: versionToRevert.readableFilter,
|
|
||||||
modifiedBy: req.user?.id || 1, // Default to 1 if no user
|
|
||||||
version: existingQuery.version + 1
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(200).json(updatedQuery);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error reverting LDAP query:", error);
|
|
||||||
res.status(500).json({ error: "Failed to revert LDAP query" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /ldap-queries/test:
|
|
||||||
* post:
|
|
||||||
* summary: Test an LDAP query filter
|
|
||||||
* description: Test a filter against an LDAP connection to see what objects would be returned
|
|
||||||
* tags: [LDAP Query Builder]
|
|
||||||
* security:
|
|
||||||
* - bearerAuth: []
|
|
||||||
* requestBody:
|
|
||||||
* required: true
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* connectionId:
|
|
||||||
* type: integer
|
|
||||||
* targetObject:
|
|
||||||
* type: string
|
|
||||||
* enum: [users, groups, computers, ous]
|
|
||||||
* filter:
|
|
||||||
* type: object
|
|
||||||
* limit:
|
|
||||||
* type: integer
|
|
||||||
* default: 100
|
|
||||||
* properties:
|
|
||||||
* type: array
|
|
||||||
* items:
|
|
||||||
* type: string
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: Query results
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* count:
|
|
||||||
* type: integer
|
|
||||||
* results:
|
|
||||||
* type: array
|
|
||||||
* items:
|
|
||||||
* type: object
|
|
||||||
* filter:
|
|
||||||
* type: string
|
|
||||||
* 400:
|
|
||||||
* description: Invalid query or connection
|
|
||||||
* 404:
|
|
||||||
* description: Connection not found
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /ldap-queries/attributes:
|
|
||||||
* get:
|
|
||||||
* summary: Get available LDAP attributes
|
|
||||||
* description: Retrieve a list of available attributes for the specified object type from an LDAP connection
|
|
||||||
* tags: [LDAP Query Builder]
|
|
||||||
* security:
|
|
||||||
* - bearerAuth: []
|
|
||||||
* parameters:
|
|
||||||
* - in: query
|
|
||||||
* name: connectionId
|
|
||||||
* required: true
|
|
||||||
* schema:
|
|
||||||
* type: integer
|
|
||||||
* description: LDAP Connection ID
|
|
||||||
* - in: query
|
|
||||||
* name: targetObject
|
|
||||||
* required: true
|
|
||||||
* schema:
|
|
||||||
* type: string
|
|
||||||
* enum: [users, groups, computers, ous]
|
|
||||||
* description: The type of object to get attributes for
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: A list of available attributes
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: array
|
|
||||||
* items:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* name:
|
|
||||||
* type: string
|
|
||||||
* description: The attribute name
|
|
||||||
* type:
|
|
||||||
* type: string
|
|
||||||
* description: The attribute data type (string, number, boolean, datetime)
|
|
||||||
* description:
|
|
||||||
* type: string
|
|
||||||
* description: Human-readable description of the attribute
|
|
||||||
* isMultiValued:
|
|
||||||
* type: boolean
|
|
||||||
* description: Whether the attribute can have multiple values
|
|
||||||
* 400:
|
|
||||||
* description: Invalid request parameters
|
|
||||||
* 404:
|
|
||||||
* description: Connection not found
|
|
||||||
*/
|
|
||||||
router.get("/ldap-queries/attributes", hasPermission("read:ldap_objects"), async (req, res) => {
|
|
||||||
try {
|
|
||||||
const connectionId = parseInt(req.query.connectionId as string);
|
|
||||||
const targetObject = req.query.targetObject as "users" | "groups" | "computers" | "ous";
|
|
||||||
|
|
||||||
if (isNaN(connectionId)) {
|
|
||||||
return res.status(400).json({ error: "Invalid connection ID" });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!targetObject || !["users", "groups", "computers", "ous"].includes(targetObject)) {
|
|
||||||
return res.status(400).json({ error: "Invalid target object type" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the connection
|
|
||||||
const connection = await storage.getLdapConnection(connectionId);
|
|
||||||
if (!connection) {
|
|
||||||
return res.status(404).json({ error: "LDAP connection not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect to LDAP
|
|
||||||
try {
|
|
||||||
const client = await connectToLdap(connection);
|
|
||||||
|
|
||||||
// Get available attributes
|
|
||||||
const attributes = await getLdapAvailableAttributes(client, targetObject);
|
|
||||||
|
|
||||||
// Close the connection
|
|
||||||
client.destroy();
|
|
||||||
|
|
||||||
// Return the attributes
|
|
||||||
res.json(attributes);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error connecting to LDAP:", error);
|
|
||||||
return res.status(500).json({
|
|
||||||
error: "Failed to connect to LDAP server",
|
|
||||||
details: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error getting LDAP attributes:", error);
|
|
||||||
res.status(500).json({ error: "Failed to retrieve LDAP attributes" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post("/ldap-queries/test", hasPermission("read:ldap_objects"), async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { connectionId, targetObject, filter, limit = 100, properties = [] } = req.body;
|
|
||||||
|
|
||||||
if (!connectionId || !targetObject || !filter) {
|
|
||||||
return res.status(400).json({ error: "Missing required fields: connectionId, targetObject, filter" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the connection
|
|
||||||
const connection = await storage.getLdapConnection(connectionId);
|
|
||||||
if (!connection) {
|
|
||||||
return res.status(404).json({ error: "LDAP connection not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate and generate the filter
|
|
||||||
let ldapFilter: string;
|
|
||||||
try {
|
|
||||||
const queryBuilder = validateQueryBuilder({ targetObject, filter });
|
|
||||||
ldapFilter = generateLdapFilter(queryBuilder);
|
|
||||||
} catch (error: any) {
|
|
||||||
return res.status(400).json({ error: "Invalid filter format", details: error.message });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect to LDAP
|
|
||||||
const client = await connectToLdap(connection);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Execute the search
|
|
||||||
const results = await searchLdap(client, {
|
|
||||||
filter: ldapFilter,
|
|
||||||
attributes: properties.length > 0 ? properties : undefined,
|
|
||||||
limit
|
|
||||||
});
|
|
||||||
|
|
||||||
// Return the results
|
|
||||||
res.json({
|
|
||||||
count: results.length,
|
|
||||||
results,
|
|
||||||
filter: ldapFilter
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
// Always destroy the client
|
|
||||||
client.destroy();
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error("Error testing LDAP query:", error);
|
|
||||||
res.status(500).json({ error: "Failed to test LDAP query", details: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
+196
-307
@@ -1,312 +1,201 @@
|
|||||||
import { LdapConnection } from "@shared/schema";
|
import { EventEmitter } from 'events';
|
||||||
import debugLib from "debug";
|
import ldap from 'ldapjs';
|
||||||
import * as ldapjs from "ldapjs";
|
import { LdapConnection } from '@shared/schema';
|
||||||
import { promisify } from "util";
|
import { storage } from './storage';
|
||||||
|
|
||||||
const debug = debugLib("app:ldap");
|
class LdapClient extends EventEmitter {
|
||||||
|
private clients: Map<number, ldap.Client> = new Map();
|
||||||
/**
|
private isConnected: Map<number, boolean> = new Map();
|
||||||
* Connect to LDAP server and return a client
|
|
||||||
*/
|
|
||||||
export async function connectToLdap(connection: LdapConnection): Promise<ldapjs.Client> {
|
|
||||||
const url = `${connection.useSSL ? "ldaps" : "ldap"}://${connection.server}:${connection.port}`;
|
|
||||||
|
|
||||||
debug(`Connecting to LDAP server at ${url}`);
|
async connect(connection: LdapConnection): Promise<boolean> {
|
||||||
|
|
||||||
const client = ldapjs.createClient({
|
|
||||||
url,
|
|
||||||
timeout: 5000,
|
|
||||||
connectTimeout: 10000,
|
|
||||||
idleTimeout: 30000,
|
|
||||||
reconnect: {
|
|
||||||
initialDelay: 100,
|
|
||||||
maxDelay: 1000,
|
|
||||||
failAfter: 10
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Convert bind to promise
|
|
||||||
const bindAsync = promisify(client.bind).bind(client);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Bind with credentials
|
|
||||||
await bindAsync(connection.username, connection.password);
|
|
||||||
debug("Successfully authenticated to LDAP server");
|
|
||||||
return client;
|
|
||||||
} catch (error: unknown) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
||||||
debug("Failed to connect to LDAP server:", error);
|
|
||||||
throw new Error(`Failed to connect to LDAP server: ${errorMessage}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LdapSearchOptions {
|
|
||||||
base?: string;
|
|
||||||
filter: string;
|
|
||||||
scope?: "base" | "one" | "sub";
|
|
||||||
attributes?: string[];
|
|
||||||
limit?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search LDAP directory with the provided options
|
|
||||||
*/
|
|
||||||
export async function searchLdap(client: ldapjs.Client, options: LdapSearchOptions): Promise<Record<string, any>[]> {
|
|
||||||
const {
|
|
||||||
base = "",
|
|
||||||
filter,
|
|
||||||
scope = "sub",
|
|
||||||
attributes,
|
|
||||||
limit = 1000
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
debug(`Searching LDAP with filter: ${filter}`);
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const results: Record<string, any>[] = [];
|
|
||||||
|
|
||||||
client.search(base, {
|
|
||||||
filter,
|
|
||||||
scope, // ldapjs accepts 'base', 'one', 'sub' as strings
|
|
||||||
attributes,
|
|
||||||
sizeLimit: limit
|
|
||||||
}, (err: ldapjs.Error | null, res: ldapjs.SearchCallbackResponse) => {
|
|
||||||
if (err) {
|
|
||||||
debug("LDAP search error:", err);
|
|
||||||
return reject(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The types for ldapjs don't fully match the actual API
|
|
||||||
// We need to use any here because the type definitions are incomplete
|
|
||||||
res.on("searchEntry", (entry: any) => {
|
|
||||||
results.push(entry.object);
|
|
||||||
});
|
|
||||||
|
|
||||||
res.on("error", (err: ldapjs.Error) => {
|
|
||||||
debug("LDAP search result error:", err);
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
res.on("end", (result: any) => {
|
|
||||||
debug(`LDAP search completed with ${results.length} results`);
|
|
||||||
if (result && result.status !== 0) {
|
|
||||||
debug(`LDAP search ended with status: ${result.status}`);
|
|
||||||
}
|
|
||||||
resolve(results);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test a connection to an LDAP server
|
|
||||||
*/
|
|
||||||
export async function testLdapConnection(connection: LdapConnection): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const client = await connectToLdap(connection);
|
|
||||||
client.destroy();
|
|
||||||
return true;
|
|
||||||
} catch (error: unknown) {
|
|
||||||
debug("LDAP connection test failed:", error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get basic info about an LDAP domain
|
|
||||||
*/
|
|
||||||
export async function getLdapDomainInfo(client: ldapjs.Client): Promise<Record<string, any> | null> {
|
|
||||||
try {
|
|
||||||
const results = await searchLdap(client, {
|
|
||||||
filter: "(objectClass=domain)",
|
|
||||||
scope: "base"
|
|
||||||
});
|
|
||||||
|
|
||||||
return results.length > 0 ? results[0] : null;
|
|
||||||
} catch (error: unknown) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
||||||
debug("Failed to get LDAP domain info:", error);
|
|
||||||
throw new Error(`Failed to get LDAP domain info: ${errorMessage}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get available attributes for a specific object type from LDAP schema
|
|
||||||
* This function retrieves attributes that are commonly used for the specified object type
|
|
||||||
*/
|
|
||||||
export interface LdapAttributeMetadata {
|
|
||||||
name: string;
|
|
||||||
type?: string;
|
|
||||||
description?: string;
|
|
||||||
syntax?: string;
|
|
||||||
isMultiValued?: boolean;
|
|
||||||
isMandatory?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getLdapAvailableAttributes(
|
|
||||||
client: ldapjs.Client,
|
|
||||||
targetObject: "users" | "groups" | "computers" | "ous"
|
|
||||||
): Promise<LdapAttributeMetadata[]> {
|
|
||||||
debug(`Getting available attributes for ${targetObject}`);
|
|
||||||
|
|
||||||
// Define common attributes for each object type
|
|
||||||
const commonAttributes = [
|
|
||||||
"cn", "name", "displayName", "description", "objectClass", "objectCategory",
|
|
||||||
"distinguishedName", "whenCreated", "whenChanged", "objectGUID", "objectSid"
|
|
||||||
];
|
|
||||||
|
|
||||||
// Object type specific attributes
|
|
||||||
const specificAttributes: Record<string, string[]> = {
|
|
||||||
users: [
|
|
||||||
"sAMAccountName", "userPrincipalName", "givenName", "sn", "mail",
|
|
||||||
"telephoneNumber", "mobile", "title", "department", "company",
|
|
||||||
"manager", "employeeID", "employeeNumber", "memberOf", "userAccountControl",
|
|
||||||
"pwdLastSet", "lastLogon", "lastLogonTimestamp", "accountExpires", "badPwdCount",
|
|
||||||
"logonCount", "homeDirectory", "homeDrive", "scriptPath", "profilePath",
|
|
||||||
"lockoutTime", "country", "st", "l", "streetAddress", "postalCode", "otherTelephone"
|
|
||||||
],
|
|
||||||
groups: [
|
|
||||||
"sAMAccountName", "groupType", "member", "memberOf", "managedBy", "msDS-PrincipalName",
|
|
||||||
"mail", "info", "groupCategory", "adminCount", "proxyAddresses"
|
|
||||||
],
|
|
||||||
computers: [
|
|
||||||
"sAMAccountName", "operatingSystem", "operatingSystemVersion", "operatingSystemServicePack",
|
|
||||||
"dNSHostName", "servicePrincipalName", "lastLogonTimestamp", "pwdLastSet",
|
|
||||||
"userAccountControl", "managedBy", "location", "serialNumber", "msDS-SupportedEncryptionTypes",
|
|
||||||
"networkAddress", "primaryGroupID"
|
|
||||||
],
|
|
||||||
ous: [
|
|
||||||
"ou", "name", "description", "distinguishedName", "managedBy", "gPLink", "gPOptions",
|
|
||||||
"msDS-Approx-Immed-Subordinates", "streetAddress", "l", "st", "postalCode", "c"
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Try to dynamically retrieve schema info for more attributes
|
|
||||||
// This gets attributes from the schema for the specific object class
|
|
||||||
let dynamicAttributes: string[] = [];
|
|
||||||
let objectClass: string;
|
|
||||||
|
|
||||||
switch (targetObject) {
|
|
||||||
case "users":
|
|
||||||
objectClass = "user";
|
|
||||||
break;
|
|
||||||
case "groups":
|
|
||||||
objectClass = "group";
|
|
||||||
break;
|
|
||||||
case "computers":
|
|
||||||
objectClass = "computer";
|
|
||||||
break;
|
|
||||||
case "ous":
|
|
||||||
objectClass = "organizationalUnit";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Attempt to query the schema - we need to try different base DNs since the schema location varies
|
const clientOptions: ldap.ClientOptions = {
|
||||||
let schemaResults: Record<string, any>[] = [];
|
url: `${connection.useTLS ? 'ldaps' : 'ldap'}://${connection.server}:${connection.port}`,
|
||||||
|
reconnect: {
|
||||||
// Try common locations for the schema (most AD servers will use one of these)
|
initialDelay: 1000,
|
||||||
const schemaLocations = [
|
maxDelay: 10000,
|
||||||
"CN=Schema,CN=Configuration,DC=domain,DC=com", // Generic example
|
failAfter: 10
|
||||||
"CN=Schema,CN=Configuration,DC=ad,DC=example,DC=com",
|
},
|
||||||
"CN=Schema,CN=Configuration,DC=example,DC=com",
|
timeout: 5000,
|
||||||
"CN=Schema,CN=Configuration",
|
connectTimeout: 10000
|
||||||
"CN=Schema",
|
|
||||||
];
|
|
||||||
|
|
||||||
let schemaFound = false;
|
|
||||||
for (const schemaLocation of schemaLocations) {
|
|
||||||
try {
|
|
||||||
schemaResults = await searchLdap(client, {
|
|
||||||
base: schemaLocation,
|
|
||||||
filter: `(&(objectClass=attributeSchema)(|(attributeSyntax=2.5.5.8)(attributeSyntax=2.5.5.9)(attributeSyntax=2.5.5.12)))`,
|
|
||||||
scope: "sub",
|
|
||||||
attributes: ["lDAPDisplayName", "attributeSyntax", "isSingleValued"],
|
|
||||||
limit: 500
|
|
||||||
});
|
|
||||||
if (schemaResults.length > 0) {
|
|
||||||
schemaFound = true;
|
|
||||||
debug(`Found schema at ${schemaLocation} with ${schemaResults.length} attributes`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (locationError) {
|
|
||||||
debug(`Schema not found at ${schemaLocation}: ${locationError instanceof Error ? locationError.message : String(locationError)}`);
|
|
||||||
// Continue trying other locations
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!schemaFound) {
|
|
||||||
debug("Could not find schema in any of the standard locations");
|
|
||||||
}
|
|
||||||
|
|
||||||
dynamicAttributes = schemaResults.map(attr => attr.lDAPDisplayName);
|
|
||||||
debug(`Retrieved ${dynamicAttributes.length} attributes from schema`);
|
|
||||||
} catch (schemaError) {
|
|
||||||
debug("Failed to retrieve attributes from schema, using predefined list:", schemaError);
|
|
||||||
// Continue with the predefined attributes
|
|
||||||
}
|
|
||||||
|
|
||||||
// Combine common and specific attributes, then add dynamic attributes
|
|
||||||
let allAttributeNames = [...commonAttributes, ...specificAttributes[targetObject]];
|
|
||||||
|
|
||||||
// Add any dynamic attributes that aren't already in our list
|
|
||||||
dynamicAttributes.forEach(attr => {
|
|
||||||
if (!allAttributeNames.includes(attr)) {
|
|
||||||
allAttributeNames.push(attr);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Convert string attributes to metadata objects and sort alphabetically by name
|
|
||||||
const attributeMetadata: LdapAttributeMetadata[] = allAttributeNames.map((name: string) => {
|
|
||||||
// Attribute type inference based on common patterns
|
|
||||||
let type = "string";
|
|
||||||
if (name.toLowerCase().includes("count") || name.toLowerCase().includes("id") ||
|
|
||||||
name.endsWith("Type") || name.includes("ID")) {
|
|
||||||
type = "number";
|
|
||||||
} else if (name.toLowerCase().includes("time") || name.toLowerCase().includes("date") ||
|
|
||||||
name.toLowerCase().includes("created") || name.toLowerCase().includes("changed") ||
|
|
||||||
name.toLowerCase().includes("expires")) {
|
|
||||||
type = "datetime";
|
|
||||||
} else if (name.toLowerCase().includes("is") || name.toLowerCase().includes("has") ||
|
|
||||||
name.toLowerCase().includes("enabled") || name.toLowerCase().includes("disabled")) {
|
|
||||||
type = "boolean";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Description inference based on name
|
|
||||||
let description = "";
|
|
||||||
if (name === "cn") description = "Common Name";
|
|
||||||
else if (name === "sn") description = "Surname";
|
|
||||||
else if (name === "givenName") description = "First Name";
|
|
||||||
else if (name === "sAMAccountName") description = "Login Name";
|
|
||||||
else if (name === "userPrincipalName") description = "User Principal Name";
|
|
||||||
else if (name === "mail") description = "Email Address";
|
|
||||||
else if (name === "memberOf") description = "Group Memberships";
|
|
||||||
else if (name === "member") description = "Members";
|
|
||||||
else if (name === "pwdLastSet") description = "Password Last Set";
|
|
||||||
else if (name === "lastLogon") description = "Last Login Time";
|
|
||||||
|
|
||||||
return {
|
|
||||||
name,
|
|
||||||
type,
|
|
||||||
description: description || undefined,
|
|
||||||
// For multi-valued attributes we could infer based on name, but would be more accurate
|
|
||||||
// to use the schema information which we don't fully process here yet
|
|
||||||
isMultiValued: name === "memberOf" || name === "member" || name === "proxyAddresses" ||
|
|
||||||
name === "objectClass" || name === "servicePrincipalName"
|
|
||||||
};
|
};
|
||||||
});
|
|
||||||
|
const client = ldap.createClient(clientOptions);
|
||||||
// Sort alphabetically by name
|
|
||||||
return attributeMetadata.sort((a, b) => a.name.localeCompare(b.name));
|
return new Promise((resolve, reject) => {
|
||||||
} catch (error: unknown) {
|
client.on('error', async (err) => {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
console.error(`LDAP connection error for ${connection.name}:`, err);
|
||||||
debug("Failed to get LDAP available attributes:", error);
|
this.isConnected.set(connection.id, false);
|
||||||
|
await storage.updateLdapConnection(connection.id, { status: 'disconnected' });
|
||||||
// Return a default set of attributes instead of throwing
|
this.emit('status', {
|
||||||
const defaultAttributes = [...commonAttributes, ...specificAttributes[targetObject]].sort();
|
connectionId: connection.id,
|
||||||
|
status: 'disconnected',
|
||||||
// Convert to metadata objects with minimal information
|
error: err.message
|
||||||
return defaultAttributes.map(name => ({ name }));
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
client.bind(connection.username, connection.password, async (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error(`LDAP bind error for ${connection.name}:`, err);
|
||||||
|
this.isConnected.set(connection.id, false);
|
||||||
|
await storage.updateLdapConnection(connection.id, { status: 'disconnected' });
|
||||||
|
this.emit('status', {
|
||||||
|
connectionId: connection.id,
|
||||||
|
status: 'disconnected',
|
||||||
|
error: err.message
|
||||||
|
});
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clients.set(connection.id, client);
|
||||||
|
this.isConnected.set(connection.id, true);
|
||||||
|
await storage.updateLdapConnection(connection.id, {
|
||||||
|
status: 'connected',
|
||||||
|
lastConnected: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
this.emit('status', {
|
||||||
|
connectionId: connection.id,
|
||||||
|
status: 'connected'
|
||||||
|
});
|
||||||
|
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`LDAP connection error for ${connection.name}:`, error);
|
||||||
|
this.isConnected.set(connection.id, false);
|
||||||
|
await storage.updateLdapConnection(connection.id, { status: 'disconnected' });
|
||||||
|
this.emit('status', {
|
||||||
|
connectionId: connection.id,
|
||||||
|
status: 'disconnected',
|
||||||
|
error: error instanceof Error ? error.message : String(error)
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
async disconnect(connectionId: number): Promise<void> {
|
||||||
|
const client = this.clients.get(connectionId);
|
||||||
|
if (client) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
client.unbind(() => {
|
||||||
|
this.clients.delete(connectionId);
|
||||||
|
this.isConnected.set(connectionId, false);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getClient(connectionId: number): ldap.Client | undefined {
|
||||||
|
return this.clients.get(connectionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
isConnectionActive(connectionId: number): boolean {
|
||||||
|
return this.isConnected.get(connectionId) || false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// LDAP CRUD operations
|
||||||
|
async searchUsers(connectionId: number, filter = '(objectClass=user)', attributes?: string[]): Promise<any[]> {
|
||||||
|
const client = this.getClient(connectionId);
|
||||||
|
if (!client) throw new Error('LDAP connection not established');
|
||||||
|
|
||||||
|
const connection = await storage.getLdapConnection(connectionId);
|
||||||
|
if (!connection) throw new Error('LDAP connection not found');
|
||||||
|
|
||||||
|
const baseDN = connection.baseDN || '';
|
||||||
|
const defaultAttributes = ['cn', 'sAMAccountName', 'mail', 'distinguishedName'];
|
||||||
|
const searchAttributes = attributes?.length ? attributes : defaultAttributes;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const results: any[] = [];
|
||||||
|
|
||||||
|
client.search(baseDN, {
|
||||||
|
filter,
|
||||||
|
scope: 'sub',
|
||||||
|
attributes: searchAttributes
|
||||||
|
}, (err, res) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.on('searchEntry', (entry) => {
|
||||||
|
results.push(entry.object);
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on('error', (err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on('end', (result) => {
|
||||||
|
resolve(results);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async searchGroups(connectionId: number, filter = '(objectClass=group)', attributes?: string[]): Promise<any[]> {
|
||||||
|
const defaultAttributes = ['cn', 'distinguishedName', 'member'];
|
||||||
|
return this.searchUsers(connectionId, filter, attributes || defaultAttributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
async searchOUs(connectionId: number, filter = '(objectClass=organizationalUnit)', attributes?: string[]): Promise<any[]> {
|
||||||
|
const defaultAttributes = ['ou', 'distinguishedName'];
|
||||||
|
return this.searchUsers(connectionId, filter, attributes || defaultAttributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
async searchComputers(connectionId: number, filter = '(objectClass=computer)', attributes?: string[]): Promise<any[]> {
|
||||||
|
const defaultAttributes = ['cn', 'distinguishedName', 'operatingSystem'];
|
||||||
|
return this.searchUsers(connectionId, filter, attributes || defaultAttributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createEntry(connectionId: number, dn: string, attributes: any): Promise<boolean> {
|
||||||
|
const client = this.getClient(connectionId);
|
||||||
|
if (!client) throw new Error('LDAP connection not established');
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
client.add(dn, attributes, (err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateEntry(connectionId: number, dn: string, changes: any[]): Promise<boolean> {
|
||||||
|
const client = this.getClient(connectionId);
|
||||||
|
if (!client) throw new Error('LDAP connection not established');
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
client.modify(dn, changes, (err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteEntry(connectionId: number, dn: string): Promise<boolean> {
|
||||||
|
const client = this.getClient(connectionId);
|
||||||
|
if (!client) throw new Error('LDAP connection not established');
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
client.del(dn, (err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ldapClient = new LdapClient();
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ import {
|
|||||||
requireAdmin,
|
requireAdmin,
|
||||||
initializeRBAC
|
initializeRBAC
|
||||||
} from "./authorization";
|
} from "./authorization";
|
||||||
import { registerLdapQueryBuilderRoutes } from "./ldap-query-builder-routes";
|
|
||||||
import express from "express";
|
|
||||||
|
|
||||||
export async function registerRoutes(app: Express): Promise<Server> {
|
export async function registerRoutes(app: Express): Promise<Server> {
|
||||||
// Setup authentication
|
// Setup authentication
|
||||||
@@ -23,72 +21,6 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
|
|
||||||
// Initialize Role Based Access Control system
|
// Initialize Role Based Access Control system
|
||||||
await initializeRBAC();
|
await initializeRBAC();
|
||||||
|
|
||||||
// Simple registration endpoint that bypasses the complex auth system
|
|
||||||
app.post("/api/simple-register", async (req, res) => {
|
|
||||||
try {
|
|
||||||
console.log("Simple registration attempt:", { ...req.body, password: "***" });
|
|
||||||
|
|
||||||
const { username, password, email, fullName } = req.body;
|
|
||||||
|
|
||||||
if (!username || !password) {
|
|
||||||
return res.status(400).json({ message: "Username and password are required" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for existing user
|
|
||||||
try {
|
|
||||||
const existingUser = await storage.getUserByUsername(username);
|
|
||||||
if (existingUser) {
|
|
||||||
console.log("Username already exists:", username);
|
|
||||||
return res.status(400).json({ message: "Username already exists" });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error checking for existing user:", error);
|
|
||||||
return res.status(500).json({ message: "Error checking for existing user" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set a default role ID
|
|
||||||
// In our system, roleId 2 is the standard user role
|
|
||||||
const roleId = 2; // Regular user role
|
|
||||||
console.log("Using default role ID:", roleId);
|
|
||||||
|
|
||||||
// Hash the password
|
|
||||||
let hashedPassword;
|
|
||||||
try {
|
|
||||||
const salt = require('crypto').randomBytes(16).toString('hex');
|
|
||||||
const hash = require('crypto').scryptSync(password, salt, 64).toString('hex');
|
|
||||||
hashedPassword = `${hash}.${salt}`;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error hashing password:", error);
|
|
||||||
return res.status(500).json({ message: "Error processing password" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the user
|
|
||||||
try {
|
|
||||||
const user = await storage.createUser({
|
|
||||||
username,
|
|
||||||
password: hashedPassword,
|
|
||||||
email: email || null,
|
|
||||||
fullName: fullName || null,
|
|
||||||
roleId: roleId,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("User created successfully:", { id: user.id, username });
|
|
||||||
|
|
||||||
const userResponse = { ...user, password: undefined };
|
|
||||||
return res.status(201).json({
|
|
||||||
...userResponse,
|
|
||||||
message: "Registration successful, please log in"
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error creating user:", error);
|
|
||||||
return res.status(500).json({ message: "Error creating user in database" });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Unexpected error during simple registration:", error);
|
|
||||||
return res.status(500).json({ message: "Internal server error during registration" });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Error handler for Zod validation errors
|
// Error handler for Zod validation errors
|
||||||
const handleZodError = (err: ZodError, res: Response) => {
|
const handleZodError = (err: ZodError, res: Response) => {
|
||||||
@@ -894,11 +826,6 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set up LDAP query builder routes
|
|
||||||
const ldapQueryRouter = express.Router();
|
|
||||||
registerLdapQueryBuilderRoutes(ldapQueryRouter, storage);
|
|
||||||
app.use('/api', ldapQueryRouter);
|
|
||||||
|
|
||||||
const httpServer = createServer(app);
|
const httpServer = createServer(app);
|
||||||
|
|
||||||
return httpServer;
|
return httpServer;
|
||||||
|
|||||||
+15
-161
@@ -4,9 +4,8 @@ import {
|
|||||||
AdUser, InsertAdUser, AdGroup, InsertAdGroup,
|
AdUser, InsertAdUser, AdGroup, InsertAdGroup,
|
||||||
AdOrgUnit, InsertAdOrgUnit, AdComputer, InsertAdComputer,
|
AdOrgUnit, InsertAdOrgUnit, AdComputer, InsertAdComputer,
|
||||||
AdDomain, InsertAdDomain, Role, ApiQuery,
|
AdDomain, InsertAdDomain, Role, ApiQuery,
|
||||||
LdapQuery, InsertLdapQuery, LdapQueryVersion, InsertLdapQueryVersion,
|
|
||||||
users, apiTokens, ldapConnections, adUsers, adGroups, adOrgUnits, adComputers, adDomains,
|
users, apiTokens, ldapConnections, adUsers, adGroups, adOrgUnits, adComputers, adDomains,
|
||||||
roles, ldapQueries, ldapQueryVersions
|
roles
|
||||||
} from "@shared/schema";
|
} from "@shared/schema";
|
||||||
import session from "express-session";
|
import session from "express-session";
|
||||||
import createMemoryStore from "memorystore";
|
import createMemoryStore from "memorystore";
|
||||||
@@ -32,9 +31,6 @@ const pool = new Pool({
|
|||||||
const PostgresStore = connectPg(session);
|
const PostgresStore = connectPg(session);
|
||||||
|
|
||||||
export interface IStorage {
|
export interface IStorage {
|
||||||
// Session store for authentication
|
|
||||||
sessionStore: session.Store;
|
|
||||||
|
|
||||||
// User management
|
// User management
|
||||||
getUser(id: number): Promise<User | undefined>;
|
getUser(id: number): Promise<User | undefined>;
|
||||||
getUserByUsername(username: string): Promise<User | undefined>;
|
getUserByUsername(username: string): Promise<User | undefined>;
|
||||||
@@ -61,15 +57,6 @@ export interface IStorage {
|
|||||||
deleteLdapConnection(id: number): Promise<boolean>;
|
deleteLdapConnection(id: number): Promise<boolean>;
|
||||||
listLdapConnections(): Promise<LdapConnection[]>;
|
listLdapConnections(): Promise<LdapConnection[]>;
|
||||||
|
|
||||||
// LDAP Query Builder
|
|
||||||
getLdapQuery(id: number): Promise<LdapQuery | undefined>;
|
|
||||||
getLdapQueries(): Promise<LdapQuery[]>;
|
|
||||||
createLdapQuery(query: InsertLdapQuery): Promise<LdapQuery>;
|
|
||||||
updateLdapQuery(id: number, query: Partial<LdapQuery>): Promise<LdapQuery | undefined>;
|
|
||||||
deleteLdapQuery(id: number): Promise<boolean>;
|
|
||||||
getLdapQueryVersions(queryId: number): Promise<LdapQueryVersion[]>;
|
|
||||||
createLdapQueryVersion(version: InsertLdapQueryVersion): Promise<LdapQueryVersion>;
|
|
||||||
|
|
||||||
// AD Users
|
// AD Users
|
||||||
getAdUser(id: number): Promise<AdUser | undefined>;
|
getAdUser(id: number): Promise<AdUser | undefined>;
|
||||||
createAdUser(user: InsertAdUser): Promise<AdUser>;
|
createAdUser(user: InsertAdUser): Promise<AdUser>;
|
||||||
@@ -104,11 +91,14 @@ export interface IStorage {
|
|||||||
updateAdDomain(id: number, domain: Partial<AdDomain>): Promise<AdDomain | undefined>;
|
updateAdDomain(id: number, domain: Partial<AdDomain>): Promise<AdDomain | undefined>;
|
||||||
deleteAdDomain(id: number): Promise<boolean>;
|
deleteAdDomain(id: number): Promise<boolean>;
|
||||||
listAdDomains(connectionId: number, query?: any): Promise<AdDomain[]>;
|
listAdDomains(connectionId: number, query?: any): Promise<AdDomain[]>;
|
||||||
|
|
||||||
|
// Session store
|
||||||
|
sessionStore: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Database Storage implementation
|
// Database Storage implementation
|
||||||
export class DatabaseStorage implements IStorage {
|
export class DatabaseStorage implements IStorage {
|
||||||
sessionStore: session.Store;
|
sessionStore: any;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.sessionStore = new PostgresStore({
|
this.sessionStore = new PostgresStore({
|
||||||
@@ -208,142 +198,6 @@ export class DatabaseStorage implements IStorage {
|
|||||||
return db.select().from(ldapConnections);
|
return db.select().from(ldapConnections);
|
||||||
}
|
}
|
||||||
|
|
||||||
// LDAP Query Builder
|
|
||||||
async getLdapQuery(id: number): Promise<LdapQuery | undefined> {
|
|
||||||
const cacheKey = `ldapQuery:${id}`;
|
|
||||||
|
|
||||||
// Try to get from cache first
|
|
||||||
const cachedData = await getCached<LdapQuery>(cacheKey);
|
|
||||||
if (cachedData) {
|
|
||||||
debug(`Cache hit for ${cacheKey}`);
|
|
||||||
return cachedData;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await db.select().from(ldapQueries).where(eq(ldapQueries.id, id));
|
|
||||||
|
|
||||||
if (result.length > 0) {
|
|
||||||
// Cache the query for faster access
|
|
||||||
await setCached(cacheKey, result[0], CACHE_TTL.MEDIUM);
|
|
||||||
return result[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getLdapQueries(): Promise<LdapQuery[]> {
|
|
||||||
const cacheKey = 'ldapQueries:all';
|
|
||||||
|
|
||||||
// Try to get from cache first
|
|
||||||
const cachedData = await getCached<LdapQuery[]>(cacheKey);
|
|
||||||
if (cachedData) {
|
|
||||||
debug(`Cache hit for ${cacheKey}`);
|
|
||||||
return cachedData;
|
|
||||||
}
|
|
||||||
|
|
||||||
const queries = await db.select().from(ldapQueries);
|
|
||||||
|
|
||||||
// Cache the results
|
|
||||||
await setCached(cacheKey, queries, CACHE_TTL.MEDIUM);
|
|
||||||
return queries;
|
|
||||||
}
|
|
||||||
|
|
||||||
async createLdapQuery(query: InsertLdapQuery): Promise<LdapQuery> {
|
|
||||||
// Create the query
|
|
||||||
const result = await db.insert(ldapQueries).values({
|
|
||||||
name: query.name,
|
|
||||||
description: query.description,
|
|
||||||
targetObject: query.targetObject,
|
|
||||||
filterJson: query.filterJson,
|
|
||||||
ldapFilter: query.ldapFilter,
|
|
||||||
readableFilter: query.readableFilter,
|
|
||||||
createdBy: query.createdBy,
|
|
||||||
modifiedBy: query.createdBy // Initially, creator and modifier are the same
|
|
||||||
}).returning();
|
|
||||||
|
|
||||||
// Invalidate relevant caches
|
|
||||||
invalidateCache('ldapQueries:all');
|
|
||||||
|
|
||||||
return result[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateLdapQuery(id: number, queryData: Partial<LdapQuery>): Promise<LdapQuery | undefined> {
|
|
||||||
// Update the query
|
|
||||||
const result = await db.update(ldapQueries)
|
|
||||||
.set({
|
|
||||||
...queryData,
|
|
||||||
updatedAt: new Date()
|
|
||||||
})
|
|
||||||
.where(eq(ldapQueries.id, id))
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
if (result.length > 0) {
|
|
||||||
// Invalidate relevant caches
|
|
||||||
invalidateCache(`ldapQuery:${id}`);
|
|
||||||
invalidateCache('ldapQueries:all');
|
|
||||||
|
|
||||||
return result[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteLdapQuery(id: number): Promise<boolean> {
|
|
||||||
// Delete the query (versions will be deleted via cascade)
|
|
||||||
const result = await db.delete(ldapQueries)
|
|
||||||
.where(eq(ldapQueries.id, id))
|
|
||||||
.returning({ id: ldapQueries.id });
|
|
||||||
|
|
||||||
const deleted = result.length > 0;
|
|
||||||
|
|
||||||
if (deleted) {
|
|
||||||
// Invalidate relevant caches
|
|
||||||
invalidateCache(`ldapQuery:${id}`);
|
|
||||||
invalidateCachePattern(`ldapQueryVersions:${id}:*`);
|
|
||||||
invalidateCache('ldapQueries:all');
|
|
||||||
}
|
|
||||||
|
|
||||||
return deleted;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getLdapQueryVersions(queryId: number): Promise<LdapQueryVersion[]> {
|
|
||||||
const cacheKey = `ldapQueryVersions:${queryId}:all`;
|
|
||||||
|
|
||||||
// Try to get from cache first
|
|
||||||
const cachedData = await getCached<LdapQueryVersion[]>(cacheKey);
|
|
||||||
if (cachedData) {
|
|
||||||
debug(`Cache hit for ${cacheKey}`);
|
|
||||||
return cachedData;
|
|
||||||
}
|
|
||||||
|
|
||||||
const versions = await db.select()
|
|
||||||
.from(ldapQueryVersions)
|
|
||||||
.where(eq(ldapQueryVersions.queryId, queryId))
|
|
||||||
.orderBy(ldapQueryVersions.version);
|
|
||||||
|
|
||||||
// Cache the results
|
|
||||||
await setCached(cacheKey, versions, CACHE_TTL.MEDIUM);
|
|
||||||
|
|
||||||
return versions;
|
|
||||||
}
|
|
||||||
|
|
||||||
async createLdapQueryVersion(version: InsertLdapQueryVersion): Promise<LdapQueryVersion> {
|
|
||||||
const result = await db.insert(ldapQueryVersions).values({
|
|
||||||
queryId: version.queryId,
|
|
||||||
version: version.version,
|
|
||||||
filterJson: version.filterJson,
|
|
||||||
ldapFilter: version.ldapFilter,
|
|
||||||
readableFilter: version.readableFilter,
|
|
||||||
targetObject: version.targetObject,
|
|
||||||
createdBy: version.createdBy,
|
|
||||||
modifiedBy: version.modifiedBy
|
|
||||||
}).returning();
|
|
||||||
|
|
||||||
// Invalidate relevant caches
|
|
||||||
invalidateCachePattern(`ldapQueryVersions:${version.queryId}:*`);
|
|
||||||
|
|
||||||
return result[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// AD Users
|
// AD Users
|
||||||
async getAdUser(id: number): Promise<AdUser | undefined> {
|
async getAdUser(id: number): Promise<AdUser | undefined> {
|
||||||
const result = await db.select().from(adUsers).where(eq(adUsers.id, id));
|
const result = await db.select().from(adUsers).where(eq(adUsers.id, id));
|
||||||
@@ -385,21 +239,21 @@ export class DatabaseStorage implements IStorage {
|
|||||||
|
|
||||||
// Apply where conditions if any
|
// Apply where conditions if any
|
||||||
if (whereClause) {
|
if (whereClause) {
|
||||||
baseQuery = baseQuery.where(whereClause as any);
|
baseQuery = baseQuery.where(whereClause);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply ordering if any
|
// Apply ordering if any
|
||||||
if (orderClauses.length > 0) {
|
if (orderClauses.length > 0) {
|
||||||
baseQuery = baseQuery.orderBy(...orderClauses as any[]);
|
baseQuery = baseQuery.orderBy(...orderClauses);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply pagination if specified
|
// Apply pagination if specified
|
||||||
if (limit !== undefined) {
|
if (limit !== undefined) {
|
||||||
baseQuery = baseQuery.limit(limit as any);
|
baseQuery = baseQuery.limit(limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (offset !== undefined) {
|
if (offset !== undefined) {
|
||||||
baseQuery = baseQuery.offset(offset as any);
|
baseQuery = baseQuery.offset(offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute the query
|
// Execute the query
|
||||||
@@ -411,7 +265,7 @@ export class DatabaseStorage implements IStorage {
|
|||||||
const filtered: Partial<AdUser> = { id: user.id };
|
const filtered: Partial<AdUser> = { id: user.id };
|
||||||
selectedFields.forEach(field => {
|
selectedFields.forEach(field => {
|
||||||
if (field in user) {
|
if (field in user) {
|
||||||
filtered[field as keyof AdUser] = user[field as keyof AdUser] as any;
|
filtered[field as keyof AdUser] = user[field as keyof AdUser];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return filtered as AdUser;
|
return filtered as AdUser;
|
||||||
@@ -474,21 +328,21 @@ export class DatabaseStorage implements IStorage {
|
|||||||
|
|
||||||
// Apply where conditions if any
|
// Apply where conditions if any
|
||||||
if (whereClause) {
|
if (whereClause) {
|
||||||
baseQuery = baseQuery.where(whereClause as any);
|
baseQuery = baseQuery.where(whereClause);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply ordering if any
|
// Apply ordering if any
|
||||||
if (orderClauses.length > 0) {
|
if (orderClauses.length > 0) {
|
||||||
baseQuery = baseQuery.orderBy(...orderClauses as any[]);
|
baseQuery = baseQuery.orderBy(...orderClauses);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply pagination if specified
|
// Apply pagination if specified
|
||||||
if (limit !== undefined) {
|
if (limit !== undefined) {
|
||||||
baseQuery = baseQuery.limit(limit as any);
|
baseQuery = baseQuery.limit(limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (offset !== undefined) {
|
if (offset !== undefined) {
|
||||||
baseQuery = baseQuery.offset(offset as any);
|
baseQuery = baseQuery.offset(offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute the query
|
// Execute the query
|
||||||
@@ -500,7 +354,7 @@ export class DatabaseStorage implements IStorage {
|
|||||||
const filtered: Partial<AdGroup> = { id: group.id };
|
const filtered: Partial<AdGroup> = { id: group.id };
|
||||||
selectedFields.forEach(field => {
|
selectedFields.forEach(field => {
|
||||||
if (field in group) {
|
if (field in group) {
|
||||||
filtered[field as keyof AdGroup] = group[field as keyof AdGroup] as any;
|
filtered[field as keyof AdGroup] = group[field as keyof AdGroup];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return filtered as AdGroup;
|
return filtered as AdGroup;
|
||||||
|
|||||||
@@ -48,13 +48,6 @@ export const PERMISSIONS = {
|
|||||||
DELETE_AD_COMPUTERS: "delete:ad_computers",
|
DELETE_AD_COMPUTERS: "delete:ad_computers",
|
||||||
|
|
||||||
VIEW_AD_DOMAINS: "view:ad_domains",
|
VIEW_AD_DOMAINS: "view:ad_domains",
|
||||||
|
|
||||||
// LDAP Query Builder
|
|
||||||
VIEW_LDAP_QUERIES: "view:ldap_queries",
|
|
||||||
CREATE_LDAP_QUERIES: "create:ldap_queries",
|
|
||||||
UPDATE_LDAP_QUERIES: "update:ldap_queries",
|
|
||||||
DELETE_LDAP_QUERIES: "delete:ldap_queries",
|
|
||||||
RUN_LDAP_QUERIES: "run:ldap_queries",
|
|
||||||
|
|
||||||
// API Token management
|
// API Token management
|
||||||
MANAGE_API_TOKENS: "manage:api_tokens",
|
MANAGE_API_TOKENS: "manage:api_tokens",
|
||||||
@@ -91,11 +84,6 @@ export const permissionsSchema = z.enum([
|
|||||||
"update:ad_computers",
|
"update:ad_computers",
|
||||||
"delete:ad_computers",
|
"delete:ad_computers",
|
||||||
"view:ad_domains",
|
"view:ad_domains",
|
||||||
"view:ldap_queries",
|
|
||||||
"create:ldap_queries",
|
|
||||||
"update:ldap_queries",
|
|
||||||
"delete:ldap_queries",
|
|
||||||
"run:ldap_queries",
|
|
||||||
"manage:api_tokens",
|
"manage:api_tokens",
|
||||||
"manage:roles",
|
"manage:roles",
|
||||||
"admin:system"
|
"admin:system"
|
||||||
@@ -211,36 +199,6 @@ export const adDomains = pgTable("ad_domains", {
|
|||||||
adProperties: jsonb("ad_properties"),
|
adProperties: jsonb("ad_properties"),
|
||||||
});
|
});
|
||||||
|
|
||||||
// LDAP Query Builder schemas
|
|
||||||
export const ldapQueries = pgTable("ldap_queries", {
|
|
||||||
id: serial("id").primaryKey(),
|
|
||||||
name: text("name").notNull(),
|
|
||||||
description: text("description"),
|
|
||||||
targetObject: text("target_object").notNull(), // users, groups, computers, ous
|
|
||||||
filterJson: jsonb("filter").notNull(), // Serialized filter conditions
|
|
||||||
ldapFilter: text("ldap_filter").notNull(), // The actual LDAP filter string
|
|
||||||
readableFilter: text("readable_filter").notNull(), // Human-readable representation
|
|
||||||
version: integer("version").notNull().default(1),
|
|
||||||
createdBy: integer("created_by").notNull().references(() => users.id),
|
|
||||||
modifiedBy: integer("modified_by").notNull().references(() => users.id, { onDelete: "set null" }).default(1),
|
|
||||||
createdAt: timestamp("created_at").defaultNow(),
|
|
||||||
updatedAt: timestamp("updated_at").defaultNow(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// LDAP Query Versions for revision history
|
|
||||||
export const ldapQueryVersions = pgTable("ldap_query_versions", {
|
|
||||||
id: serial("id").primaryKey(),
|
|
||||||
queryId: integer("query_id").notNull().references(() => ldapQueries.id, { onDelete: "cascade" }),
|
|
||||||
version: integer("version").notNull(),
|
|
||||||
filterJson: jsonb("filter").notNull(), // Serialized filter conditions
|
|
||||||
ldapFilter: text("ldap_filter").notNull(), // The actual LDAP filter string
|
|
||||||
readableFilter: text("readable_filter").notNull(), // Human-readable representation
|
|
||||||
targetObject: text("target_object").notNull(), // users, groups, computers, ous
|
|
||||||
createdBy: integer("created_by").notNull().references(() => users.id, { onDelete: "set null" }).default(1),
|
|
||||||
modifiedBy: integer("modified_by").notNull().references(() => users.id, { onDelete: "set null" }).default(1),
|
|
||||||
createdAt: timestamp("created_at").defaultNow(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define relations between tables
|
// Define relations between tables
|
||||||
export const rolesRelations = relations(roles, ({ many }) => ({
|
export const rolesRelations = relations(roles, ({ many }) => ({
|
||||||
permissions: many(rolePermissions),
|
permissions: many(rolePermissions),
|
||||||
@@ -261,8 +219,6 @@ export const usersRelations = relations(users, ({ one, many }) => ({
|
|||||||
references: [roles.id],
|
references: [roles.id],
|
||||||
}),
|
}),
|
||||||
apiTokens: many(apiTokens),
|
apiTokens: many(apiTokens),
|
||||||
createdQueries: many(ldapQueries, { relationName: "createdQueries" }),
|
|
||||||
modifiedQueries: many(ldapQueries, { relationName: "modifiedQueries" }),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const apiTokensRelations = relations(apiTokens, ({ one }) => ({
|
export const apiTokensRelations = relations(apiTokens, ({ one }) => ({
|
||||||
@@ -276,31 +232,6 @@ export const apiTokensRelations = relations(apiTokens, ({ one }) => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const ldapQueriesRelations = relations(ldapQueries, ({ one, many }) => ({
|
|
||||||
creator: one(users, {
|
|
||||||
fields: [ldapQueries.createdBy],
|
|
||||||
references: [users.id],
|
|
||||||
relationName: "createdQueries",
|
|
||||||
}),
|
|
||||||
modifier: one(users, {
|
|
||||||
fields: [ldapQueries.modifiedBy],
|
|
||||||
references: [users.id],
|
|
||||||
relationName: "modifiedQueries",
|
|
||||||
}),
|
|
||||||
versions: many(ldapQueryVersions),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const ldapQueryVersionsRelations = relations(ldapQueryVersions, ({ one }) => ({
|
|
||||||
query: one(ldapQueries, {
|
|
||||||
fields: [ldapQueryVersions.queryId],
|
|
||||||
references: [ldapQueries.id],
|
|
||||||
}),
|
|
||||||
modifier: one(users, {
|
|
||||||
fields: [ldapQueryVersions.modifiedBy],
|
|
||||||
references: [users.id],
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Generate insertion schemas
|
// Generate insertion schemas
|
||||||
export const insertRoleSchema = createInsertSchema(roles).omit({ id: true, createdAt: true });
|
export const insertRoleSchema = createInsertSchema(roles).omit({ id: true, createdAt: true });
|
||||||
export const insertRolePermissionSchema = createInsertSchema(rolePermissions);
|
export const insertRolePermissionSchema = createInsertSchema(rolePermissions);
|
||||||
@@ -312,8 +243,6 @@ export const insertAdGroupSchema = createInsertSchema(adGroups).omit({ id: true
|
|||||||
export const insertAdOrgUnitSchema = createInsertSchema(adOrgUnits).omit({ id: true });
|
export const insertAdOrgUnitSchema = createInsertSchema(adOrgUnits).omit({ id: true });
|
||||||
export const insertAdComputerSchema = createInsertSchema(adComputers).omit({ id: true });
|
export const insertAdComputerSchema = createInsertSchema(adComputers).omit({ id: true });
|
||||||
export const insertAdDomainSchema = createInsertSchema(adDomains).omit({ id: true });
|
export const insertAdDomainSchema = createInsertSchema(adDomains).omit({ id: true });
|
||||||
export const insertLdapQuerySchema = createInsertSchema(ldapQueries).omit({ id: true, createdAt: true, updatedAt: true, version: true });
|
|
||||||
export const insertLdapQueryVersionSchema = createInsertSchema(ldapQueryVersions).omit({ id: true, createdAt: true });
|
|
||||||
|
|
||||||
// Login schema
|
// Login schema
|
||||||
export const loginSchema = z.object({
|
export const loginSchema = z.object({
|
||||||
@@ -357,9 +286,5 @@ export type AdComputer = typeof adComputers.$inferSelect;
|
|||||||
export type InsertAdComputer = z.infer<typeof insertAdComputerSchema>;
|
export type InsertAdComputer = z.infer<typeof insertAdComputerSchema>;
|
||||||
export type AdDomain = typeof adDomains.$inferSelect;
|
export type AdDomain = typeof adDomains.$inferSelect;
|
||||||
export type InsertAdDomain = z.infer<typeof insertAdDomainSchema>;
|
export type InsertAdDomain = z.infer<typeof insertAdDomainSchema>;
|
||||||
export type LdapQuery = typeof ldapQueries.$inferSelect;
|
|
||||||
export type InsertLdapQuery = z.infer<typeof insertLdapQuerySchema>;
|
|
||||||
export type LdapQueryVersion = typeof ldapQueryVersions.$inferSelect;
|
|
||||||
export type InsertLdapQueryVersion = z.infer<typeof insertLdapQueryVersionSchema>;
|
|
||||||
export type Login = z.infer<typeof loginSchema>;
|
export type Login = z.infer<typeof loginSchema>;
|
||||||
export type ApiQuery = z.infer<typeof apiQuerySchema>;
|
export type ApiQuery = z.infer<typeof apiQuerySchema>;
|
||||||
|
|||||||
Reference in New Issue
Block a user