mirror of
https://github.com/freedbygrace/ActiveDirectoryManager.git
synced 2026-08-06 17:17:42 +00:00
Restored to 'd77ce2adbe81c897769f79623edb2fdd7c5cdbc7'
Replit-Restored-To: d77ce2adbe
This commit is contained in:
+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) => {
|
||||
try {
|
||||
console.log("Registration attempt:", { ...req.body, password: "***" });
|
||||
|
||||
// Validate inputs
|
||||
const validationResult = loginSchema.safeParse(req.body);
|
||||
if (!validationResult.success) {
|
||||
console.log("Validation failed:", validationResult.error.errors);
|
||||
return res.status(400).json({ message: "Invalid input", errors: validationResult.error.errors });
|
||||
}
|
||||
|
||||
const { username, password } = req.body;
|
||||
|
||||
// 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" });
|
||||
const existingUser = await storage.getUserByUsername(username);
|
||||
if (existingUser) {
|
||||
return res.status(400).json({ message: "Username already exists" });
|
||||
}
|
||||
|
||||
// Hash password
|
||||
let hashedPassword;
|
||||
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
|
||||
const hashedPassword = await hashPassword(password);
|
||||
// Get the default role if role ID isn't specified
|
||||
let roleId = req.body.roleId;
|
||||
if (!roleId) {
|
||||
try {
|
||||
const defaultRole = await storage.getDefaultRole();
|
||||
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" });
|
||||
}
|
||||
const defaultRole = await storage.getDefaultRole();
|
||||
roleId = defaultRole?.id;
|
||||
}
|
||||
|
||||
// Create user
|
||||
let user;
|
||||
try {
|
||||
user = await storage.createUser({
|
||||
username,
|
||||
password: hashedPassword,
|
||||
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" });
|
||||
}
|
||||
const user = await storage.createUser({
|
||||
username,
|
||||
password: hashedPassword,
|
||||
email: req.body.email,
|
||||
fullName: req.body.fullName,
|
||||
roleId: roleId,
|
||||
});
|
||||
|
||||
// Remove password from response
|
||||
const userResponse = { ...user, password: undefined };
|
||||
|
||||
// Manual login instead of using req.login
|
||||
try {
|
||||
req.user = user;
|
||||
// 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"
|
||||
});
|
||||
}
|
||||
req.login(user, (err) => {
|
||||
if (err) return next(err);
|
||||
res.status(201).json(userResponse);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Unexpected error during registration:", error);
|
||||
return res.status(500).json({ message: "Internal server error during registration" });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Login endpoint - fixed without relying on req.isAuthenticated
|
||||
// Login endpoint
|
||||
app.post("/api/login", (req, res, next) => {
|
||||
try {
|
||||
passport.authenticate("local", (err: any, user: any, info: any) => {
|
||||
if (err) {
|
||||
console.error("Authentication error:", err);
|
||||
return res.status(500).json({ message: "Internal server error during authentication" });
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({ message: info?.message || "Authentication failed" });
|
||||
}
|
||||
|
||||
// Manual login with try-catch to safely handle errors
|
||||
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" });
|
||||
}
|
||||
passport.authenticate("local", (err: any, user: any, info: any) => {
|
||||
if (err) return next(err);
|
||||
if (!user) {
|
||||
return res.status(401).json({ message: info?.message || "Authentication failed" });
|
||||
}
|
||||
req.login(user, (loginErr) => {
|
||||
if (loginErr) return next(loginErr);
|
||||
// Remove password from response
|
||||
const userResponse = { ...user, password: undefined };
|
||||
res.json(userResponse);
|
||||
});
|
||||
})(req, res, next);
|
||||
});
|
||||
|
||||
// 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) => {
|
||||
try {
|
||||
// Check if user exists in session instead of using isAuthenticated
|
||||
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" });
|
||||
if (!req.isAuthenticated()) {
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
}
|
||||
// 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) => {
|
||||
try {
|
||||
// Check if user exists in session
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
}
|
||||
if (!req.isAuthenticated()) {
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
}
|
||||
|
||||
try {
|
||||
const { name, expiresAt, roleId, customPermissions } = req.body;
|
||||
if (!name) {
|
||||
return res.status(400).json({ message: "Token name is required" });
|
||||
@@ -289,8 +211,7 @@ export function setupAuth(app: Express) {
|
||||
|
||||
res.status(201).json(apiToken);
|
||||
} catch (error) {
|
||||
console.error("Error generating API token:", error);
|
||||
res.status(500).json({ message: "Failed to generate API token" });
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+49
-54
@@ -16,66 +16,61 @@ export type RequirePermissionOptions = RequireAuthOptions & {
|
||||
// Default middleware that verifies a user is authenticated
|
||||
export function requireAuth(options: RequireAuthOptions = {}) {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
// Check for session authentication - just check if req.user exists
|
||||
if (req.user) {
|
||||
return next();
|
||||
}
|
||||
// Check for session authentication
|
||||
if (req.isAuthenticated()) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Check for API token authentication if allowed
|
||||
if (options.allowApiToken) {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (authHeader && authHeader.startsWith("Bearer ")) {
|
||||
const token = authHeader.substring(7);
|
||||
|
||||
try {
|
||||
// Lookup the token
|
||||
const [apiToken] = await db.select()
|
||||
.from(apiTokens)
|
||||
.where(eq(apiTokens.token, token))
|
||||
.limit(1);
|
||||
// Check for API token authentication if allowed
|
||||
if (options.allowApiToken) {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (authHeader && authHeader.startsWith("Bearer ")) {
|
||||
const token = authHeader.substring(7);
|
||||
|
||||
try {
|
||||
// Lookup the token
|
||||
const [apiToken] = await db.select()
|
||||
.from(apiTokens)
|
||||
.where(eq(apiTokens.token, token))
|
||||
.limit(1);
|
||||
|
||||
if (!apiToken) {
|
||||
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" });
|
||||
if (!apiToken) {
|
||||
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" });
|
||||
}
|
||||
}
|
||||
|
||||
// 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 debugLib from "debug";
|
||||
import * as ldapjs from "ldapjs";
|
||||
import { promisify } from "util";
|
||||
import { EventEmitter } from 'events';
|
||||
import ldap from 'ldapjs';
|
||||
import { LdapConnection } from '@shared/schema';
|
||||
import { storage } from './storage';
|
||||
|
||||
const debug = debugLib("app:ldap");
|
||||
|
||||
/**
|
||||
* 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}`;
|
||||
class LdapClient extends EventEmitter {
|
||||
private clients: Map<number, ldap.Client> = new Map();
|
||||
private isConnected: Map<number, boolean> = new Map();
|
||||
|
||||
debug(`Connecting to LDAP server at ${url}`);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
async connect(connection: LdapConnection): Promise<boolean> {
|
||||
try {
|
||||
// Attempt to query the schema - we need to try different base DNs since the schema location varies
|
||||
let schemaResults: Record<string, any>[] = [];
|
||||
|
||||
// Try common locations for the schema (most AD servers will use one of these)
|
||||
const schemaLocations = [
|
||||
"CN=Schema,CN=Configuration,DC=domain,DC=com", // Generic example
|
||||
"CN=Schema,CN=Configuration,DC=ad,DC=example,DC=com",
|
||||
"CN=Schema,CN=Configuration,DC=example,DC=com",
|
||||
"CN=Schema,CN=Configuration",
|
||||
"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 clientOptions: ldap.ClientOptions = {
|
||||
url: `${connection.useTLS ? 'ldaps' : 'ldap'}://${connection.server}:${connection.port}`,
|
||||
reconnect: {
|
||||
initialDelay: 1000,
|
||||
maxDelay: 10000,
|
||||
failAfter: 10
|
||||
},
|
||||
timeout: 5000,
|
||||
connectTimeout: 10000
|
||||
};
|
||||
});
|
||||
|
||||
// Sort alphabetically by name
|
||||
return attributeMetadata.sort((a, b) => a.name.localeCompare(b.name));
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
debug("Failed to get LDAP available attributes:", error);
|
||||
|
||||
// Return a default set of attributes instead of throwing
|
||||
const defaultAttributes = [...commonAttributes, ...specificAttributes[targetObject]].sort();
|
||||
|
||||
// Convert to metadata objects with minimal information
|
||||
return defaultAttributes.map(name => ({ name }));
|
||||
|
||||
const client = ldap.createClient(clientOptions);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
client.on('error', async (err) => {
|
||||
console.error(`LDAP connection 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
|
||||
});
|
||||
});
|
||||
|
||||
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,
|
||||
initializeRBAC
|
||||
} from "./authorization";
|
||||
import { registerLdapQueryBuilderRoutes } from "./ldap-query-builder-routes";
|
||||
import express from "express";
|
||||
|
||||
export async function registerRoutes(app: Express): Promise<Server> {
|
||||
// Setup authentication
|
||||
@@ -23,72 +21,6 @@ export async function registerRoutes(app: Express): Promise<Server> {
|
||||
|
||||
// Initialize Role Based Access Control system
|
||||
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
|
||||
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);
|
||||
|
||||
return httpServer;
|
||||
|
||||
+15
-161
@@ -4,9 +4,8 @@ import {
|
||||
AdUser, InsertAdUser, AdGroup, InsertAdGroup,
|
||||
AdOrgUnit, InsertAdOrgUnit, AdComputer, InsertAdComputer,
|
||||
AdDomain, InsertAdDomain, Role, ApiQuery,
|
||||
LdapQuery, InsertLdapQuery, LdapQueryVersion, InsertLdapQueryVersion,
|
||||
users, apiTokens, ldapConnections, adUsers, adGroups, adOrgUnits, adComputers, adDomains,
|
||||
roles, ldapQueries, ldapQueryVersions
|
||||
roles
|
||||
} from "@shared/schema";
|
||||
import session from "express-session";
|
||||
import createMemoryStore from "memorystore";
|
||||
@@ -32,9 +31,6 @@ const pool = new Pool({
|
||||
const PostgresStore = connectPg(session);
|
||||
|
||||
export interface IStorage {
|
||||
// Session store for authentication
|
||||
sessionStore: session.Store;
|
||||
|
||||
// User management
|
||||
getUser(id: number): Promise<User | undefined>;
|
||||
getUserByUsername(username: string): Promise<User | undefined>;
|
||||
@@ -61,15 +57,6 @@ export interface IStorage {
|
||||
deleteLdapConnection(id: number): Promise<boolean>;
|
||||
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
|
||||
getAdUser(id: number): Promise<AdUser | undefined>;
|
||||
createAdUser(user: InsertAdUser): Promise<AdUser>;
|
||||
@@ -104,11 +91,14 @@ export interface IStorage {
|
||||
updateAdDomain(id: number, domain: Partial<AdDomain>): Promise<AdDomain | undefined>;
|
||||
deleteAdDomain(id: number): Promise<boolean>;
|
||||
listAdDomains(connectionId: number, query?: any): Promise<AdDomain[]>;
|
||||
|
||||
// Session store
|
||||
sessionStore: any;
|
||||
}
|
||||
|
||||
// Database Storage implementation
|
||||
export class DatabaseStorage implements IStorage {
|
||||
sessionStore: session.Store;
|
||||
sessionStore: any;
|
||||
|
||||
constructor() {
|
||||
this.sessionStore = new PostgresStore({
|
||||
@@ -208,142 +198,6 @@ export class DatabaseStorage implements IStorage {
|
||||
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
|
||||
async getAdUser(id: number): Promise<AdUser | undefined> {
|
||||
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
|
||||
if (whereClause) {
|
||||
baseQuery = baseQuery.where(whereClause as any);
|
||||
baseQuery = baseQuery.where(whereClause);
|
||||
}
|
||||
|
||||
// Apply ordering if any
|
||||
if (orderClauses.length > 0) {
|
||||
baseQuery = baseQuery.orderBy(...orderClauses as any[]);
|
||||
baseQuery = baseQuery.orderBy(...orderClauses);
|
||||
}
|
||||
|
||||
// Apply pagination if specified
|
||||
if (limit !== undefined) {
|
||||
baseQuery = baseQuery.limit(limit as any);
|
||||
baseQuery = baseQuery.limit(limit);
|
||||
}
|
||||
|
||||
if (offset !== undefined) {
|
||||
baseQuery = baseQuery.offset(offset as any);
|
||||
baseQuery = baseQuery.offset(offset);
|
||||
}
|
||||
|
||||
// Execute the query
|
||||
@@ -411,7 +265,7 @@ export class DatabaseStorage implements IStorage {
|
||||
const filtered: Partial<AdUser> = { id: user.id };
|
||||
selectedFields.forEach(field => {
|
||||
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;
|
||||
@@ -474,21 +328,21 @@ export class DatabaseStorage implements IStorage {
|
||||
|
||||
// Apply where conditions if any
|
||||
if (whereClause) {
|
||||
baseQuery = baseQuery.where(whereClause as any);
|
||||
baseQuery = baseQuery.where(whereClause);
|
||||
}
|
||||
|
||||
// Apply ordering if any
|
||||
if (orderClauses.length > 0) {
|
||||
baseQuery = baseQuery.orderBy(...orderClauses as any[]);
|
||||
baseQuery = baseQuery.orderBy(...orderClauses);
|
||||
}
|
||||
|
||||
// Apply pagination if specified
|
||||
if (limit !== undefined) {
|
||||
baseQuery = baseQuery.limit(limit as any);
|
||||
baseQuery = baseQuery.limit(limit);
|
||||
}
|
||||
|
||||
if (offset !== undefined) {
|
||||
baseQuery = baseQuery.offset(offset as any);
|
||||
baseQuery = baseQuery.offset(offset);
|
||||
}
|
||||
|
||||
// Execute the query
|
||||
@@ -500,7 +354,7 @@ export class DatabaseStorage implements IStorage {
|
||||
const filtered: Partial<AdGroup> = { id: group.id };
|
||||
selectedFields.forEach(field => {
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user