import type { Express, Request as ExpressRequest, Response, NextFunction } from "express"; import { createServer, type Server } from "http"; import { setupAuth } from "./auth"; import { setupSwagger } from "./swagger"; import { storage } from "./storage"; import { db } from "./db"; import { apiQuerySchema, PERMISSIONS, moveComputerSchema, moveUserSchema, addToGroupSchema, removeFromGroupSchema, adUsers, adGroups, adOrgUnits, adComputers, adDomains, adSites, adSubnets } from "@shared/schema"; import { ZodError } from "zod"; import rateLimit from "express-rate-limit"; import { requireAuth, requirePermission, requireAdmin, initializeRBAC } from "./authorization"; import { ldapClient } from "./ldap"; import { applyFilterConditions, generatePaginationMetadata, parseFilter, parsePagination } from "./query-parser"; import { eq, sql, count } from "drizzle-orm"; import { v4 as uuidv4 } from "uuid"; // Extend Express Request to include user property interface Request extends ExpressRequest { user: { id: number; username: string; roleId?: number; [key: string]: any; }; } export async function registerRoutes(app: Express): Promise { // Setup authentication const { authenticateApiToken } = setupAuth(app); // Setup Swagger documentation setupSwagger(app); // Initialize Role Based Access Control system await initializeRBAC(); // Apply rate limiting middleware for API routes const apiLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // limit each IP to 100 requests per windowMs standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers legacyHeaders: false, // Disable the `X-RateLimit-*` headers message: { message: 'Too many requests, please try again later.' }, skip: (req: any) => { // Skip rate limiting for authenticated users with admin role if (req.isAuthenticated && typeof req.isAuthenticated === 'function' && req.isAuthenticated()) { return req.user?.role === 'admin'; } return false; } }); // Apply the rate limiter to API routes app.use('/api/', apiLimiter); // Error handler for Zod validation errors const handleZodError = (err: ZodError, res: Response) => { return res.status(400).json({ message: "Validation error", errors: err.errors, }); }; // Parse query parameters const parseQueryParams = (req: Request) => { try { return apiQuerySchema.parse(req.query); } catch (err) { if (err instanceof ZodError) { console.error("Query parameter validation error:", err.errors); return undefined; } throw err; } }; /** * @swagger * /api/ldap-connections: * get: * summary: List all LDAP connections * tags: [LDAP Connections] * security: * - cookieAuth: [] * - bearerAuth: [] * responses: * 200: * description: A list of LDAP connections * content: * application/json: * schema: * type: array * items: * $ref: '#/components/schemas/LdapConnection' * 401: * $ref: '#/components/responses/UnauthorizedError' * 403: * $ref: '#/components/responses/ForbiddenError' */ app.get("/api/ldap-connections", requirePermission(PERMISSIONS.VIEW_LDAP_CONNECTIONS, { allowApiToken: true }), async (req, res, next) => { try { const connections = await storage.listLdapConnections(); // Hide sensitive fields like password const safeConnections = connections.map(conn => { const { password, ...safeConn } = conn; return safeConn; }); res.json(safeConnections); } catch (error) { next(error); } }); /** * @swagger * /api/ldap-connections: * post: * summary: Create a new LDAP connection * tags: [LDAP Connections] * security: * - cookieAuth: [] * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - name * - server * - domain * - username * - password * properties: * name: * type: string * server: * type: string * domain: * type: string * port: * type: integer * default: 389 * useSSL: * type: boolean * default: true * username: * type: string * password: * type: string * responses: * 201: * description: Connection created successfully * content: * application/json: * schema: * $ref: '#/components/schemas/LdapConnection' * 400: * $ref: '#/components/responses/BadRequestError' * 401: * $ref: '#/components/responses/UnauthorizedError' */ app.post("/api/ldap-connections", requireAdmin, async (req, res, next) => { try { const connection = await storage.createLdapConnection(req.body); // Hide password in response const { password, ...safeConn } = connection; res.status(201).json(safeConn); } catch (error) { next(error); } }); /** * @swagger * /api/ldap-connections/{id}: * get: * summary: Get a specific LDAP connection * tags: [LDAP Connections] * security: * - cookieAuth: [] * parameters: * - name: id * in: path * required: true * schema: * type: integer * responses: * 200: * description: LDAP connection details * content: * application/json: * schema: * $ref: '#/components/schemas/LdapConnection' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.get("/api/ldap-connections/:id", async (req, res, next) => { try { if (!req.isAuthenticated()) { return res.status(401).json({ message: "Not authenticated" }); } const connection = await storage.getLdapConnection(parseInt(req.params.id)); if (!connection) { return res.status(404).json({ message: "LDAP connection not found" }); } // Hide password in response const { password, ...safeConn } = connection; res.json(safeConn); } catch (error) { next(error); } }); /** * @swagger * /api/ldap-connections/{id}: * put: * summary: Update a LDAP connection * tags: [LDAP Connections] * security: * - cookieAuth: [] * parameters: * - name: id * in: path * required: true * schema: * type: integer * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * name: * type: string * server: * type: string * domain: * type: string * port: * type: integer * useSSL: * type: boolean * username: * type: string * password: * type: string * responses: * 200: * description: Connection updated successfully * content: * application/json: * schema: * $ref: '#/components/schemas/LdapConnection' * 400: * $ref: '#/components/responses/BadRequestError' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.put("/api/ldap-connections/:id", requireAdmin, async (req, res, next) => { try { const updatedConnection = await storage.updateLdapConnection(parseInt(req.params.id), req.body); if (!updatedConnection) { return res.status(404).json({ message: "LDAP connection not found" }); } // Hide password in response const { password, ...safeConn } = updatedConnection; res.json(safeConn); } catch (error) { next(error); } }); /** * @swagger * /api/ldap-connections/{id}: * delete: * summary: Delete a LDAP connection * tags: [LDAP Connections] * security: * - cookieAuth: [] * parameters: * - name: id * in: path * required: true * schema: * type: integer * responses: * 200: * description: Connection deleted successfully * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.delete("/api/ldap-connections/:id", requireAdmin, async (req, res, next) => { try { const deleted = await storage.deleteLdapConnection(parseInt(req.params.id)); if (!deleted) { return res.status(404).json({ message: "LDAP connection not found" }); } res.json({ success: true }); } catch (error) { next(error); } }); /** * @swagger * /api/tokens: * get: * summary: List all API tokens for current user * tags: [API Tokens] * security: * - cookieAuth: [] * responses: * 200: * description: A list of API tokens * content: * application/json: * schema: * type: array * items: * $ref: '#/components/schemas/ApiToken' * 401: * $ref: '#/components/responses/UnauthorizedError' */ app.get("/api/tokens", async (req, res, next) => { try { if (!req.isAuthenticated()) { return res.status(401).json({ message: "Not authenticated" }); } const tokens = await storage.listApiTokensByUserId(req.user.id); res.json(tokens); } catch (error) { next(error); } }); /** * @swagger * /api/tokens/{id}: * delete: * summary: Delete an API token * tags: [API Tokens] * security: * - cookieAuth: [] * parameters: * - name: id * in: path * required: true * schema: * type: integer * responses: * 200: * description: Token deleted successfully * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.delete("/api/tokens/:id", async (req, res, next) => { try { if (!req.isAuthenticated()) { return res.status(401).json({ message: "Not authenticated" }); } const token = await storage.getApiToken(parseInt(req.params.id)); if (!token) { return res.status(404).json({ message: "Token not found" }); } // Only allow users to delete their own tokens unless they're admin if (token.userId !== req.user.id) { // Get the user's role const userRole = await storage.getRole(req.user.roleId!); if (userRole?.name !== "admin") { return res.status(403).json({ message: "Forbidden: You cannot delete tokens that don't belong to you" }); } } const deleted = await storage.deleteApiToken(parseInt(req.params.id)); if (!deleted) { return res.status(404).json({ message: "Token not found" }); } res.json({ success: true }); } catch (error) { next(error); } }); /** * @swagger * /api/users: * get: * summary: List all users (admin only) * tags: [Users] * security: * - cookieAuth: [] * responses: * 200: * description: A list of users * content: * application/json: * schema: * type: array * items: * $ref: '#/components/schemas/User' * 401: * $ref: '#/components/responses/UnauthorizedError' * 403: * description: Forbidden - admin access required */ app.get("/api/users", requireAdmin, async (req, res, next) => { try { const users = await storage.listUsers(); // Remove passwords from response const safeUsers = users.map(user => { const { password, ...safeUser } = user; return safeUser; }); res.json(safeUsers); } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/ad-users: * get: * summary: List AD users from the specified LDAP connection * tags: [AD Users] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * - $ref: '#/components/parameters/filterParam' * - $ref: '#/components/parameters/selectParam' * - $ref: '#/components/parameters/expandParam' * - $ref: '#/components/parameters/orderByParam' * - $ref: '#/components/parameters/topParam' * - $ref: '#/components/parameters/skipParam' * responses: * 200: * description: A list of AD users with pagination metadata * content: * application/json: * schema: * type: object * properties: * data: * type: array * items: * $ref: '#/components/schemas/AdUser' * metadata: * $ref: '#/components/responses/PaginatedResponse/content/application~1json/schema/properties/metadata' * 401: * $ref: '#/components/responses/UnauthorizedError' */ app.get("/api/connections/:connectionId/ad-users", async (req, res, next) => { try { if (!req.isAuthenticated() && !req.headers.authorization) { return res.status(401).json({ message: "Authentication required" }); } const connectionId = parseInt(req.params.connectionId); const connection = await storage.getLdapConnection(connectionId); if (!connection) { return res.status(404).json({ message: "LDAP connection not found" }); } const query = parseQueryParams(req); const users = await storage.listAdUsers(connectionId, query); // Count total records for pagination metadata const countQuery = db.select({ count: sql`count(*)` }).from(adUsers) .where(eq(adUsers.connectionId, connectionId)); // Apply filters if present if (query && query.filter) { const conditions = parseFilter(query.filter); const whereClause = applyFilterConditions(adUsers, conditions); if (whereClause) { countQuery.where(whereClause); } } const [countResult] = await countQuery; const totalRecords = Number(countResult?.count || 0); // Add objectType to each result const usersWithObjectType = users.map(user => ({ ...user, objectType: 'user' })); // Generate pagination metadata const { limit, offset } = parsePagination(query?.top, query?.skip); const paginationMetadata = generatePaginationMetadata( totalRecords, limit, offset, `${req.protocol}://${req.get('host')}${req.originalUrl}` ); // Return data with pagination metadata res.json({ data: usersWithObjectType, ...paginationMetadata }); } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/ad-users: * post: * summary: Create a new AD user * tags: [AD Users] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - distinguishedName * - sAMAccountName * properties: * distinguishedName: * type: string * sAMAccountName: * type: string * userPrincipalName: * type: string * givenName: * type: string * surname: * type: string * displayName: * type: string * email: * type: string * enabled: * type: boolean * adProperties: * type: object * responses: * 201: * description: User created successfully * content: * application/json: * schema: * $ref: '#/components/schemas/AdUser' * 400: * $ref: '#/components/responses/BadRequestError' * 401: * $ref: '#/components/responses/UnauthorizedError' */ app.post("/api/connections/:connectionId/ad-users", authenticateApiToken, async (req, res, next) => { try { const connectionId = parseInt(req.params.connectionId); const connection = await storage.getLdapConnection(connectionId); if (!connection) { return res.status(404).json({ message: "LDAP connection not found" }); } const userData = { ...req.body, connectionId }; const user = await storage.createAdUser(userData); res.status(201).json(user); } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/ad-users/{id}: * get: * summary: Get a specific AD user * tags: [AD Users] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * - name: id * in: path * required: true * schema: * type: integer * - $ref: '#/components/parameters/selectParam' * responses: * 200: * description: AD user details * content: * application/json: * schema: * $ref: '#/components/schemas/AdUser' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.get("/api/connections/:connectionId/ad-users/:id", async (req, res, next) => { try { if (!req.isAuthenticated() && !req.headers.authorization) { return res.status(401).json({ message: "Authentication required" }); } const user = await storage.getAdUser(parseInt(req.params.id)); if (!user || user.connectionId !== parseInt(req.params.connectionId)) { return res.status(404).json({ message: "AD user not found" }); } // Apply property selection if specified let result = user; if (req.query.select) { const properties = (req.query.select as string).split(','); const selectedUser: any = { id: user.id }; properties.forEach(prop => { if ((user as any)[prop] !== undefined) { selectedUser[prop] = (user as any)[prop]; } }); result = selectedUser as typeof user; } res.json(result); } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/ad-users/{id}: * put: * summary: Update an AD user * tags: [AD Users] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * - name: id * in: path * required: true * schema: * type: integer * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * distinguishedName: * type: string * sAMAccountName: * type: string * userPrincipalName: * type: string * givenName: * type: string * surname: * type: string * displayName: * type: string * email: * type: string * enabled: * type: boolean * adProperties: * type: object * responses: * 200: * description: User updated successfully * content: * application/json: * schema: * $ref: '#/components/schemas/AdUser' * 400: * $ref: '#/components/responses/BadRequestError' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ /** * @swagger * /api/connections/{connectionId}/ad-users/{id}: * put: * summary: Update an AD user * tags: [AD Users] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * - name: id * in: path * required: true * schema: * type: integer * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * givenName: * type: string * surname: * type: string * displayName: * type: string * email: * type: string * enabled: * type: boolean * userPrincipalName: * type: string * adProperties: * type: object * responses: * 200: * description: Updated AD user * content: * application/json: * schema: * $ref: '#/components/schemas/AdUser' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.put("/api/connections/:connectionId/ad-users/:id", authenticateApiToken, async (req, res, next) => { try { const user = await storage.getAdUser(parseInt(req.params.id)); if (!user || user.connectionId !== parseInt(req.params.connectionId)) { return res.status(404).json({ message: "AD user not found" }); } const updatedUser = await storage.updateAdUser(parseInt(req.params.id), req.body); res.json(updatedUser); } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/ad-users/{id}/managed-by: * patch: * summary: Update the managedBy attribute for an AD user * tags: [AD Users] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * - name: id * in: path * required: true * schema: * type: integer * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * managerDistinguishedName: * type: string * nullable: true * description: The distinguished name of the manager, or null to remove the manager * responses: * 200: * description: The updated AD user * content: * application/json: * schema: * $ref: '#/components/schemas/AdUser' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * description: User not found */ app.patch("/api/connections/:connectionId/ad-users/:id/managed-by", authenticateApiToken, async (req, res, next) => { try { const connectionId = parseInt(req.params.connectionId); const userId = parseInt(req.params.id); const { managerDistinguishedName } = req.body; // Get the connection const connection = await storage.getLdapConnection(connectionId); if (!connection) { return res.status(404).json({ message: "LDAP connection not found" }); } // Get the user const user = await storage.getAdUser(userId); if (!user || user.connectionId !== connectionId) { return res.status(404).json({ message: "AD user not found" }); } // Connect to LDAP try { await ldapClient.connect(connection); } catch (error) { console.error("LDAP connection error:", error); return res.status(500).json({ message: "Failed to connect to LDAP server", error: error.message }); } try { // Set the managedBy attribute await ldapClient.setManagedBy(connectionId, user.distinguishedName, managerDistinguishedName); // Update the user in the database const updatedUser = await storage.updateAdUser(userId, { managedBy: managerDistinguishedName }); // Log the action await storage.createAuditLogEntry({ action: managerDistinguishedName ? "update_manager" : "remove_manager", targetId: user.objectGUID, details: { objectType: "user", managerDN: managerDistinguishedName, userDN: user.distinguishedName }, userId: req.user?.id, connectionId }); return res.status(200).json(updatedUser); } catch (error) { console.error("Error updating managedBy attribute:", error); return res.status(500).json({ message: "Failed to update managedBy attribute", error: error.message }); } } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/ad-users/{id}: * delete: * summary: Delete an AD user * tags: [AD Users] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * - name: id * in: path * required: true * schema: * type: integer * responses: * 200: * description: User deleted successfully * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.delete("/api/connections/:connectionId/ad-users/:id", authenticateApiToken, async (req, res, next) => { try { const user = await storage.getAdUser(parseInt(req.params.id)); if (!user || user.connectionId !== parseInt(req.params.connectionId)) { return res.status(404).json({ message: "AD user not found" }); } const deleted = await storage.deleteAdUser(parseInt(req.params.id)); if (!deleted) { return res.status(404).json({ message: "AD user not found" }); } res.json({ success: true }); } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/ad-groups/{id}: * put: * summary: Update an AD group * tags: [AD Groups] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * - name: id * in: path * required: true * schema: * type: integer * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * distinguishedName: * type: string * name: * type: string * description: * type: string * groupScope: * type: string * enum: [DomainLocal, Global, Universal] * groupType: * type: string * enum: [Security, Distribution] * members: * type: array * items: * type: string * adProperties: * type: object * responses: * 200: * description: Group updated successfully * content: * application/json: * schema: * $ref: '#/components/schemas/AdGroup' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' * 400: * $ref: '#/components/responses/BadRequestError' */ app.put("/api/connections/:connectionId/ad-groups/:id", authenticateApiToken, async (req, res, next) => { try { const group = await storage.getAdGroup(parseInt(req.params.id)); if (!group || group.connectionId !== parseInt(req.params.connectionId)) { return res.status(404).json({ message: "AD group not found" }); } const updatedGroup = await storage.updateAdGroup(parseInt(req.params.id), req.body); res.json(updatedGroup); } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/ad-groups/{id}/managed-by: * patch: * summary: Update the managedBy attribute for an AD group * tags: [AD Groups] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * - name: id * in: path * required: true * schema: * type: integer * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * managerDistinguishedName: * type: string * nullable: true * description: The distinguished name of the manager, or null to remove the manager * responses: * 200: * description: The updated AD group * content: * application/json: * schema: * $ref: '#/components/schemas/AdGroup' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * description: Group not found */ app.patch("/api/connections/:connectionId/ad-groups/:id/managed-by", authenticateApiToken, async (req, res, next) => { try { const connectionId = parseInt(req.params.connectionId); const groupId = parseInt(req.params.id); const { managerDistinguishedName } = req.body; // Get the connection const connection = await storage.getLdapConnection(connectionId); if (!connection) { return res.status(404).json({ message: "LDAP connection not found" }); } // Get the group const group = await storage.getAdGroup(groupId); if (!group || group.connectionId !== connectionId) { return res.status(404).json({ message: "AD group not found" }); } // Connect to LDAP try { await ldapClient.connect(connection); } catch (error) { console.error("LDAP connection error:", error); return res.status(500).json({ message: "Failed to connect to LDAP server", error: error.message }); } try { // Set the managedBy attribute await ldapClient.setManagedBy(connectionId, group.distinguishedName, managerDistinguishedName); // Update the group in the database const updatedGroup = await storage.updateAdGroup(groupId, { managedBy: managerDistinguishedName }); // Log the action await storage.createAuditLogEntry({ action: managerDistinguishedName ? "update_manager" : "remove_manager", targetId: group.objectGUID, details: { objectType: "group", managerDN: managerDistinguishedName, groupDN: group.distinguishedName }, userId: req.user?.id, connectionId }); return res.status(200).json(updatedGroup); } catch (error) { console.error("Error updating managedBy attribute:", error); return res.status(500).json({ message: "Failed to update managedBy attribute", error: error.message }); } } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/ad-groups/{id}: * delete: * summary: Delete an AD group * tags: [AD Groups] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * - name: id * in: path * required: true * schema: * type: integer * responses: * 200: * description: Group deleted successfully * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.delete("/api/connections/:connectionId/ad-groups/:id", authenticateApiToken, async (req, res, next) => { try { const group = await storage.getAdGroup(parseInt(req.params.id)); if (!group || group.connectionId !== parseInt(req.params.connectionId)) { return res.status(404).json({ message: "AD group not found" }); } const deleted = await storage.deleteAdGroup(parseInt(req.params.id)); if (!deleted) { return res.status(404).json({ message: "AD group not found" }); } res.json({ success: true }); } catch (error) { next(error); } }); // Similar endpoints for AD Groups /** * @swagger * /api/connections/{connectionId}/ad-groups: * get: * summary: List AD groups from the specified LDAP connection * tags: [AD Groups] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * - $ref: '#/components/parameters/filterParam' * - $ref: '#/components/parameters/selectParam' * - $ref: '#/components/parameters/expandParam' * - $ref: '#/components/parameters/orderByParam' * - $ref: '#/components/parameters/topParam' * - $ref: '#/components/parameters/skipParam' * responses: * 200: * description: A list of AD groups * content: * application/json: * schema: * type: object * properties: * data: * type: array * items: * $ref: '#/components/schemas/AdGroup' * metadata: * $ref: '#/components/responses/PaginatedResponse/content/application~1json/schema/properties/metadata' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.get("/api/connections/:connectionId/ad-groups", async (req, res, next) => { try { if (!req.isAuthenticated() && !req.headers.authorization) { return res.status(401).json({ message: "Authentication required" }); } const connectionId = parseInt(req.params.connectionId); const connection = await storage.getLdapConnection(connectionId); if (!connection) { return res.status(404).json({ message: "LDAP connection not found" }); } const query = parseQueryParams(req); const groups = await storage.listAdGroups(connectionId, query); // Count total records for pagination metadata const countQuery = db.select({ count: sql`count(*)` }).from(adGroups) .where(eq(adGroups.connectionId, connectionId)); // Apply filters if present if (query && query.filter) { const conditions = parseFilter(query.filter); const whereClause = applyFilterConditions(adGroups, conditions); if (whereClause) { countQuery.where(whereClause); } } const [countResult] = await countQuery; const totalRecords = Number(countResult?.count || 0); // Add objectType to each result const groupsWithObjectType = groups.map(group => ({ ...group, objectType: 'group' })); // Generate pagination metadata const { limit, offset } = parsePagination(query?.top, query?.skip); const paginationMetadata = generatePaginationMetadata( totalRecords, limit, offset, `${req.protocol}://${req.get('host')}${req.originalUrl}` ); // Return data with pagination metadata res.json({ data: groupsWithObjectType, ...paginationMetadata }); } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/ad-groups: * post: * summary: Create a new Active Directory group * tags: [AD Groups] * security: * - cookieAuth: [] * - bearerAuth: [] * parameters: * - in: path * name: connectionId * required: true * schema: * type: integer * description: LDAP connection ID * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - name * - parentDN * properties: * name: * type: string * description: The name of the group (will be used for cn and sAMAccountName) * parentDN: * type: string * description: The distinguished name of the parent container (OU or domain) * description: * type: string * description: Group description * groupType: * type: string * enum: ['Global', 'DomainLocal', 'Universal'] * default: 'Global' * description: Group scope type * groupCategory: * type: string * enum: ['Security', 'Distribution'] * default: 'Security' * description: Group category type * responses: * 201: * description: Group created successfully * 400: * description: Invalid input * 401: * description: Unauthorized * 403: * description: Forbidden * 500: * description: Server error */ app.post("/api/connections/:connectionId/ad-groups", authenticateApiToken, requirePermission(PERMISSIONS.CREATE_AD_GROUPS), async (req: Request, res, next) => { try { const connectionId = parseInt(req.params.connectionId); const { name, parentDN, description, groupType = 'Global', groupCategory = 'Security' } = req.body; // Basic validation if (!name || !parentDN) { return res.status(400).json({ message: "Group name and parent DN are required" }); } // Get connection const connection = await storage.getLdapConnection(connectionId); if (!connection) { return res.status(404).json({ message: "LDAP connection not found" }); } // Create the DN for the new group const groupDN = `CN=${name},${parentDN}`; // Set the group type value based on type and category const groupTypeValue = (groupCategory === 'Security' ? 0x80000000 : 0) | (groupType === 'Global' ? 0x2 : groupType === 'Universal' ? 0x8 : 0x4); // Attributes for the new group const groupAttributes = { objectClass: ['top', 'group'], cn: name, sAMAccountName: name, description: description || '', groupType: groupTypeValue.toString() }; // Create the group in AD await ldapClient.createEntry(connectionId, groupDN, groupAttributes); // Search for the newly created group to get its attributes const searchResults = await ldapClient.searchGroups(connectionId, `(&(objectClass=group)(cn=${name}))`, [ 'objectGUID', 'distinguishedName', 'canonicalName', 'cn', 'sAMAccountName', 'description', 'groupType' ]); if (!searchResults || searchResults.length === 0) { return res.status(500).json({ message: "Group created but could not retrieve details" }); } const adGroup = searchResults[0]; // Store in database const storedGroup = await storage.createAdGroup({ connectionId, objectGUID: adGroup.objectGUID, distinguishedName: adGroup.distinguishedName, canonicalName: adGroup.canonicalName, cn: adGroup.cn, sAMAccountName: adGroup.sAMAccountName, groupType: groupCategory + ' ' + groupType, description: adGroup.description, members: [], // No members initially adProperties: adGroup // Store all attributes }); // Add audit log await storage.createAuditLogEntry({ userId: req.user.id, connectionId, action: 'create', targetId: storedGroup.id.toString(), details: { objectType: 'group', objectName: name, distinguishedName: groupDN, groupType: groupCategory + ' ' + groupType } }); res.status(201).json(storedGroup); } catch (err) { console.error("Error creating AD group:", err); if (err.message && err.message.includes('Entry Already Exists')) { return res.status(409).json({ message: "Group already exists" }); } next(err); } }); /** * @swagger * /api/connections/{connectionId}/ad-org-units/{id}: * put: * summary: Update an AD organizational unit * tags: [AD Organizational Units] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * - name: id * in: path * required: true * schema: * type: integer * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * distinguishedName: * type: string * name: * type: string * description: * type: string * parentOu: * type: string * adProperties: * type: object * responses: * 200: * description: Organizational unit updated successfully * content: * application/json: * schema: * $ref: '#/components/schemas/AdOrgUnit' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' * 400: * $ref: '#/components/responses/BadRequestError' */ app.put("/api/connections/:connectionId/ad-org-units/:id", authenticateApiToken, async (req, res, next) => { try { const orgUnit = await storage.getAdOrgUnit(parseInt(req.params.id)); if (!orgUnit || orgUnit.connectionId !== parseInt(req.params.connectionId)) { return res.status(404).json({ message: "AD organizational unit not found" }); } const updatedOrgUnit = await storage.updateAdOrgUnit(parseInt(req.params.id), req.body); res.json(updatedOrgUnit); } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/ad-org-units/{id}/managed-by: * patch: * summary: Update the managedBy attribute for an AD organizational unit * tags: [AD Organizational Units] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * - name: id * in: path * required: true * schema: * type: integer * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * managerDistinguishedName: * type: string * nullable: true * description: The distinguished name of the manager, or null to remove the manager * responses: * 200: * description: The updated AD organizational unit * content: * application/json: * schema: * $ref: '#/components/schemas/AdOrgUnit' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * description: Organizational unit not found */ app.patch("/api/connections/:connectionId/ad-org-units/:id/managed-by", authenticateApiToken, async (req, res, next) => { try { const connectionId = parseInt(req.params.connectionId); const ouId = parseInt(req.params.id); const { managerDistinguishedName } = req.body; // Get the connection const connection = await storage.getLdapConnection(connectionId); if (!connection) { return res.status(404).json({ message: "LDAP connection not found" }); } // Get the OU const ou = await storage.getAdOrgUnit(ouId); if (!ou || ou.connectionId !== connectionId) { return res.status(404).json({ message: "AD organizational unit not found" }); } // Connect to LDAP try { await ldapClient.connect(connection); } catch (error) { console.error("LDAP connection error:", error); return res.status(500).json({ message: "Failed to connect to LDAP server", error: error.message }); } try { // Set the managedBy attribute await ldapClient.setManagedBy(connectionId, ou.distinguishedName, managerDistinguishedName); // Update the OU in the database const updatedOU = await storage.updateAdOrgUnit(ouId, { managedBy: managerDistinguishedName }); // Log the action await storage.createAuditLogEntry({ action: managerDistinguishedName ? "update_manager" : "remove_manager", targetId: ou.objectGUID, details: { objectType: "organizationalUnit", managerDN: managerDistinguishedName, ouDN: ou.distinguishedName }, userId: req.user?.id, connectionId }); return res.status(200).json(updatedOU); } catch (error) { console.error("Error updating managedBy attribute:", error); return res.status(500).json({ message: "Failed to update managedBy attribute", error: error.message }); } } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/ad-org-units/{id}: * delete: * summary: Delete an AD organizational unit * tags: [AD Organizational Units] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * - name: id * in: path * required: true * schema: * type: integer * responses: * 200: * description: Organizational unit deleted successfully * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.delete("/api/connections/:connectionId/ad-org-units/:id", authenticateApiToken, async (req, res, next) => { try { const orgUnit = await storage.getAdOrgUnit(parseInt(req.params.id)); if (!orgUnit || orgUnit.connectionId !== parseInt(req.params.connectionId)) { return res.status(404).json({ message: "AD organizational unit not found" }); } const deleted = await storage.deleteAdOrgUnit(parseInt(req.params.id)); if (!deleted) { return res.status(404).json({ message: "AD organizational unit not found" }); } res.json({ success: true }); } catch (error) { next(error); } }); // Organizational Units endpoints /** * @swagger * /api/connections/{connectionId}/ad-org-units: * get: * summary: List AD organizational units from the specified LDAP connection * tags: [AD Organizational Units] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * - $ref: '#/components/parameters/filterParam' * - $ref: '#/components/parameters/selectParam' * - $ref: '#/components/parameters/expandParam' * - $ref: '#/components/parameters/orderByParam' * - $ref: '#/components/parameters/topParam' * - $ref: '#/components/parameters/skipParam' * responses: * 200: * description: A list of AD organizational units * content: * application/json: * schema: * type: object * properties: * data: * type: array * items: * $ref: '#/components/schemas/AdOrgUnit' * metadata: * $ref: '#/components/responses/PaginatedResponse/content/application~1json/schema/properties/metadata' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.get("/api/connections/:connectionId/ad-org-units", async (req, res, next) => { try { if (!req.isAuthenticated() && !req.headers.authorization) { return res.status(401).json({ message: "Authentication required" }); } const connectionId = parseInt(req.params.connectionId); const connection = await storage.getLdapConnection(connectionId); if (!connection) { return res.status(404).json({ message: "LDAP connection not found" }); } const query = parseQueryParams(req); const orgUnits = await storage.listAdOrgUnits(connectionId, query); // Count total records for pagination metadata const countQuery = db.select({ count: sql`count(*)` }).from(adOrgUnits) .where(eq(adOrgUnits.connectionId, connectionId)); // Apply filters if present if (query && query.filter) { const conditions = parseFilter(query.filter); const whereClause = applyFilterConditions(adOrgUnits, conditions); if (whereClause) { countQuery.where(whereClause); } } const [countResult] = await countQuery; const totalRecords = Number(countResult?.count || 0); // Add objectType to each result const orgUnitsWithObjectType = orgUnits.map(ou => ({ ...ou, objectType: 'organizationalUnit' })); // Generate pagination metadata const { limit, offset } = parsePagination(query?.top, query?.skip); const paginationMetadata = generatePaginationMetadata( totalRecords, limit, offset, `${req.protocol}://${req.get('host')}${req.originalUrl}` ); // Return data with pagination metadata res.json({ data: orgUnitsWithObjectType, ...paginationMetadata }); } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/ad-computers/{id}: * put: * summary: Update an AD computer * tags: [AD Computers] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * - name: id * in: path * required: true * schema: * type: integer * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * distinguishedName: * type: string * name: * type: string * dnsHostName: * type: string * description: * type: string * operatingSystem: * type: string * operatingSystemVersion: * type: string * enabled: * type: boolean * adProperties: * type: object * responses: * 200: * description: Computer updated successfully * content: * application/json: * schema: * $ref: '#/components/schemas/AdComputer' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' * 400: * $ref: '#/components/responses/BadRequestError' */ app.put("/api/connections/:connectionId/ad-computers/:id", authenticateApiToken, async (req, res, next) => { try { const computer = await storage.getAdComputer(parseInt(req.params.id)); if (!computer || computer.connectionId !== parseInt(req.params.connectionId)) { return res.status(404).json({ message: "AD computer not found" }); } const updatedComputer = await storage.updateAdComputer(parseInt(req.params.id), req.body); res.json(updatedComputer); } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/ad-computers/{id}/managed-by: * patch: * summary: Update the managedBy attribute for an AD computer * tags: [AD Computers] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * - name: id * in: path * required: true * schema: * type: integer * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * managerDistinguishedName: * type: string * nullable: true * description: The distinguished name of the manager, or null to remove the manager * responses: * 200: * description: The updated AD computer * content: * application/json: * schema: * $ref: '#/components/schemas/AdComputer' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * description: Computer not found */ app.patch("/api/connections/:connectionId/ad-computers/:id/managed-by", authenticateApiToken, async (req, res, next) => { try { const connectionId = parseInt(req.params.connectionId); const computerId = parseInt(req.params.id); const { managerDistinguishedName } = req.body; // Get the connection const connection = await storage.getLdapConnection(connectionId); if (!connection) { return res.status(404).json({ message: "LDAP connection not found" }); } // Get the computer const computer = await storage.getAdComputer(computerId); if (!computer || computer.connectionId !== connectionId) { return res.status(404).json({ message: "AD computer not found" }); } // Connect to LDAP try { await ldapClient.connect(connection); } catch (error) { console.error("LDAP connection error:", error); return res.status(500).json({ message: "Failed to connect to LDAP server", error: error.message }); } try { // Set the managedBy attribute await ldapClient.setManagedBy(connectionId, computer.distinguishedName, managerDistinguishedName); // Update the computer in the database const updatedComputer = await storage.updateAdComputer(computerId, { managedBy: managerDistinguishedName }); // Log the action await storage.createAuditLogEntry({ action: managerDistinguishedName ? "update_manager" : "remove_manager", targetId: computer.objectGUID, details: { objectType: "computer", managerDN: managerDistinguishedName, computerDN: computer.distinguishedName }, userId: req.user?.id, connectionId }); return res.status(200).json(updatedComputer); } catch (error) { console.error("Error updating managedBy attribute:", error); return res.status(500).json({ message: "Failed to update managedBy attribute", error: error.message }); } } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/ad-computers/{id}: * delete: * summary: Delete an AD computer * tags: [AD Computers] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * - name: id * in: path * required: true * schema: * type: integer * responses: * 200: * description: Computer deleted successfully * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.delete("/api/connections/:connectionId/ad-computers/:id", authenticateApiToken, async (req, res, next) => { try { const computer = await storage.getAdComputer(parseInt(req.params.id)); if (!computer || computer.connectionId !== parseInt(req.params.connectionId)) { return res.status(404).json({ message: "AD computer not found" }); } const deleted = await storage.deleteAdComputer(parseInt(req.params.id)); if (!deleted) { return res.status(404).json({ message: "AD computer not found" }); } res.json({ success: true }); } catch (error) { next(error); } }); // Computers endpoints /** * @swagger * /api/connections/{connectionId}/ad-computers: * get: * summary: List AD computers from the specified LDAP connection * tags: [AD Computers] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * - $ref: '#/components/parameters/filterParam' * - $ref: '#/components/parameters/selectParam' * - $ref: '#/components/parameters/expandParam' * - $ref: '#/components/parameters/orderByParam' * - $ref: '#/components/parameters/topParam' * - $ref: '#/components/parameters/skipParam' * responses: * 200: * description: A list of AD computers * content: * application/json: * schema: * type: object * properties: * data: * type: array * items: * $ref: '#/components/schemas/AdComputer' * metadata: * $ref: '#/components/responses/PaginatedResponse/content/application~1json/schema/properties/metadata' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.get("/api/connections/:connectionId/ad-computers", async (req, res, next) => { try { if (!req.isAuthenticated() && !req.headers.authorization) { return res.status(401).json({ message: "Authentication required" }); } const connectionId = parseInt(req.params.connectionId); const connection = await storage.getLdapConnection(connectionId); if (!connection) { return res.status(404).json({ message: "LDAP connection not found" }); } const query = parseQueryParams(req); const computers = await storage.listAdComputers(connectionId, query); // Count total records for pagination metadata const countQuery = db.select({ count: sql`count(*)` }).from(adComputers) .where(eq(adComputers.connectionId, connectionId)); // Apply filters if present if (query && query.filter) { const conditions = parseFilter(query.filter); const whereClause = applyFilterConditions(adComputers, conditions); if (whereClause) { countQuery.where(whereClause); } } const [countResult] = await countQuery; const totalRecords = Number(countResult?.count || 0); // Add objectType to each result const computersWithObjectType = computers.map(computer => ({ ...computer, objectType: 'computer' })); // Generate pagination metadata const { limit, offset } = parsePagination(query?.top, query?.skip); const paginationMetadata = generatePaginationMetadata( totalRecords, limit, offset, `${req.protocol}://${req.get('host')}${req.originalUrl}` ); // Return data with pagination metadata res.json({ data: computersWithObjectType, ...paginationMetadata }); } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/ad-computers: * post: * summary: Create a new Active Directory computer * tags: [AD Computers] * security: * - cookieAuth: [] * - bearerAuth: [] * parameters: * - in: path * name: connectionId * required: true * schema: * type: integer * description: LDAP connection ID * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - name * - parentDN * properties: * name: * type: string * description: The name of the computer (will be used for cn and sAMAccountName) * parentDN: * type: string * description: The distinguished name of the parent container (OU or domain) * description: * type: string * description: Computer description * dnsHostName: * type: string * description: DNS host name of the computer * operatingSystem: * type: string * description: Operating system of the computer * operatingSystemVersion: * type: string * description: Operating system version * enabled: * type: boolean * default: true * description: Whether the computer account is enabled * responses: * 201: * description: Computer created successfully * 400: * description: Invalid input * 401: * description: Unauthorized * 403: * description: Forbidden * 500: * description: Server error */ app.post("/api/connections/:connectionId/ad-computers", authenticateApiToken, requirePermission(PERMISSIONS.CREATE_AD_COMPUTERS), async (req: Request, res, next) => { try { const connectionId = parseInt(req.params.connectionId); const { name, parentDN, description, dnsHostName, operatingSystem, operatingSystemVersion, enabled = true } = req.body; // Basic validation if (!name || !parentDN) { return res.status(400).json({ message: "Computer name and parent DN are required" }); } // Get connection const connection = await storage.getLdapConnection(connectionId); if (!connection) { return res.status(404).json({ message: "LDAP connection not found" }); } // Create the DN for the new computer const computerDN = `CN=${name},${parentDN}`; // The sAMAccountName needs to end with $ const sAMAccountName = name.endsWith('$') ? name : `${name}$`; // Create the userAccountControl value // 4096 = WORKSTATION_TRUST_ACCOUNT // 2 = ACCOUNTDISABLE (if not enabled) const userAccountControl = enabled ? 4096 : 4098; // Set the dnsHostName if not provided const actualDnsHostName = dnsHostName || `${name}.${connection.domain}`; // Attributes for the new computer const computerAttributes: { objectClass: string[]; cn: string; sAMAccountName: string; userAccountControl: string; description: string; dNSHostName: string; operatingSystem?: string; operatingSystemVersion?: string; } = { objectClass: ['top', 'computer'], cn: name, sAMAccountName: sAMAccountName, userAccountControl: userAccountControl.toString(), description: description || '', dNSHostName: actualDnsHostName }; // Add operating system information if provided if (operatingSystem) { computerAttributes.operatingSystem = operatingSystem; } if (operatingSystemVersion) { computerAttributes.operatingSystemVersion = operatingSystemVersion; } // Create the computer in AD await ldapClient.createEntry(connectionId, computerDN, computerAttributes); // Search for the newly created computer to get its attributes const searchResults = await ldapClient.searchComputers(connectionId, `(&(objectClass=computer)(cn=${name}))`, [ 'objectGUID', 'distinguishedName', 'canonicalName', 'cn', 'sAMAccountName', 'description', 'dNSHostName', 'operatingSystem', 'operatingSystemVersion', 'userAccountControl' ]); if (!searchResults || searchResults.length === 0) { return res.status(500).json({ message: "Computer created but could not retrieve details" }); } const adComputer = searchResults[0]; // Store in database const storedComputer = await storage.createAdComputer({ connectionId, objectGUID: adComputer.objectGUID, distinguishedName: adComputer.distinguishedName, canonicalName: adComputer.canonicalName, cn: adComputer.cn, name: adComputer.cn, sAMAccountName: adComputer.sAMAccountName, dnsHostName: adComputer.dNSHostName, operatingSystem: adComputer.operatingSystem, operatingSystemVersion: adComputer.operatingSystemVersion, enabled: (parseInt(adComputer.userAccountControl) & 2) === 0, // Check if ACCOUNTDISABLE flag is not set adProperties: adComputer // Store all attributes }); // Add audit log await storage.createAuditLogEntry({ userId: req.user.id, connectionId, action: 'create', targetId: storedComputer.id.toString(), details: { distinguishedName: computerDN, enabled: enabled, name: name } }); res.status(201).json(storedComputer); } catch (err) { console.error("Error creating AD computer:", err); if (err.message && err.message.includes('Entry Already Exists')) { return res.status(409).json({ message: "Computer already exists" }); } next(err); } }); /** * @swagger * /api/connections/{connectionId}/ad-domains/{id}: * put: * summary: Update an AD domain * tags: [AD Domains] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * - name: id * in: path * required: true * schema: * type: integer * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * distinguishedName: * type: string * name: * type: string * netBIOSName: * type: string * forestName: * type: string * domainFunctionality: * type: string * adProperties: * type: object * responses: * 200: * description: Domain updated successfully * content: * application/json: * schema: * $ref: '#/components/schemas/AdDomain' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' * 400: * $ref: '#/components/responses/BadRequestError' */ app.put("/api/connections/:connectionId/ad-domains/:id", authenticateApiToken, async (req, res, next) => { try { const domain = await storage.getAdDomain(parseInt(req.params.id)); if (!domain || domain.connectionId !== parseInt(req.params.connectionId)) { return res.status(404).json({ message: "AD domain not found" }); } const updatedDomain = await storage.updateAdDomain(parseInt(req.params.id), req.body); res.json(updatedDomain); } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/ad-domains/{id}: * delete: * summary: Delete an AD domain * tags: [AD Domains] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * - name: id * in: path * required: true * schema: * type: integer * responses: * 200: * description: Domain deleted successfully * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.delete("/api/connections/:connectionId/ad-domains/:id", authenticateApiToken, async (req, res, next) => { try { const domain = await storage.getAdDomain(parseInt(req.params.id)); if (!domain || domain.connectionId !== parseInt(req.params.connectionId)) { return res.status(404).json({ message: "AD domain not found" }); } const deleted = await storage.deleteAdDomain(parseInt(req.params.id)); if (!deleted) { return res.status(404).json({ message: "AD domain not found" }); } res.json({ success: true }); } catch (error) { next(error); } }); // Domains endpoints /** * @swagger * /api/connections/{connectionId}/ad-domains: * get: * summary: List AD domains from the specified LDAP connection * tags: [AD Domains] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * - $ref: '#/components/parameters/filterParam' * - $ref: '#/components/parameters/selectParam' * - $ref: '#/components/parameters/expandParam' * - $ref: '#/components/parameters/orderByParam' * - $ref: '#/components/parameters/topParam' * - $ref: '#/components/parameters/skipParam' * responses: * 200: * description: A list of AD domains * content: * application/json: * schema: * type: object * properties: * data: * type: array * items: * $ref: '#/components/schemas/AdDomain' * metadata: * $ref: '#/components/responses/PaginatedResponse/content/application~1json/schema/properties/metadata' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.get("/api/connections/:connectionId/ad-domains", async (req, res, next) => { try { if (!req.isAuthenticated() && !req.headers.authorization) { return res.status(401).json({ message: "Authentication required" }); } const connectionId = parseInt(req.params.connectionId); const connection = await storage.getLdapConnection(connectionId); if (!connection) { return res.status(404).json({ message: "LDAP connection not found" }); } const query = parseQueryParams(req); const domains = await storage.listAdDomains(connectionId, query); // Count total records for pagination metadata const countQuery = db.select({ count: sql`count(*)` }).from(adDomains) .where(eq(adDomains.connectionId, connectionId)); // Apply filters if present if (query && query.filter) { const conditions = parseFilter(query.filter); const whereClause = applyFilterConditions(adDomains, conditions); if (whereClause) { countQuery.where(whereClause); } } const [countResult] = await countQuery; const totalRecords = Number(countResult?.count || 0); // Add objectType to each result const domainsWithObjectType = domains.map(domain => ({ ...domain, objectType: 'domain' })); // Generate pagination metadata const { limit, offset } = parsePagination(query?.top, query?.skip); const paginationMetadata = generatePaginationMetadata( totalRecords, limit, offset, `${req.protocol}://${req.get('host')}${req.originalUrl}` ); // Return data with pagination metadata res.json({ data: domainsWithObjectType, ...paginationMetadata }); } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/ldap-attributes: * get: * summary: Get LDAP attributes for a specific object class * tags: [LDAP Query Builder] * security: * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * - name: objectClass * in: query * required: true * schema: * type: string * enum: [user, group, organizationalUnit, computer, domain] * responses: * 200: * description: List of LDAP attributes * content: * application/json: * schema: * type: array * items: * $ref: '#/components/schemas/LdapAttribute' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.get("/api/connections/:connectionId/ldap-attributes", requireAuth, async (req: Request, res: Response, next: NextFunction) => { try { const connectionId = parseInt(req.params.connectionId); const objectClass = req.query.objectClass as string; if (!objectClass) { return res.status(400).json({ message: "objectClass parameter is required" }); } // Verify the connection exists const connection = await storage.getLdapConnection(connectionId); if (!connection) { return res.status(404).json({ message: "LDAP connection not found" }); } const attributes = await storage.getLdapAttributes(connectionId, objectClass); res.json(attributes); } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/ldap-attributes: * post: * summary: Create a new LDAP attribute * tags: [LDAP Query Builder] * security: * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - name * - objectClass * properties: * name: * type: string * displayName: * type: string * description: * type: string * type: * type: string * multiValued: * type: boolean * objectClass: * type: string * enum: [user, group, organizationalUnit, computer, domain] * isIndexed: * type: boolean * responses: * 201: * description: Attribute created successfully * content: * application/json: * schema: * $ref: '#/components/schemas/LdapAttribute' * 400: * $ref: '#/components/responses/BadRequestError' * 401: * $ref: '#/components/responses/UnauthorizedError' */ app.post("/api/connections/:connectionId/ldap-attributes", requireAdmin, async (req, res, next) => { try { const connectionId = parseInt(req.params.connectionId); // Verify the connection exists const connection = await storage.getLdapConnection(connectionId); if (!connection) { return res.status(404).json({ message: "LDAP connection not found" }); } const attribute = await storage.createLdapAttribute({ ...req.body, connectionId }); res.status(201).json(attribute); } catch (error) { next(error); } }); /** * @swagger * /api/ldap-attributes/{id}: * put: * summary: Update an LDAP attribute * tags: [LDAP Query Builder] * security: * - cookieAuth: [] * parameters: * - name: id * in: path * required: true * schema: * type: integer * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * displayName: * type: string * description: * type: string * type: * type: string * multiValued: * type: boolean * isIndexed: * type: boolean * responses: * 200: * description: Attribute updated successfully * content: * application/json: * schema: * $ref: '#/components/schemas/LdapAttribute' * 400: * $ref: '#/components/responses/BadRequestError' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.put("/api/ldap-attributes/:id", requireAdmin, async (req, res, next) => { try { const id = parseInt(req.params.id); const updatedAttribute = await storage.updateLdapAttribute(id, req.body); if (!updatedAttribute) { return res.status(404).json({ message: "LDAP attribute not found" }); } res.json(updatedAttribute); } catch (error) { next(error); } }); /** * @swagger * /api/ldap-attributes/{id}: * delete: * summary: Delete an LDAP attribute * tags: [LDAP Query Builder] * security: * - cookieAuth: [] * parameters: * - name: id * in: path * required: true * schema: * type: integer * responses: * 200: * description: Attribute deleted successfully * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.delete("/api/ldap-attributes/:id", requireAdmin, async (req, res, next) => { try { const id = parseInt(req.params.id); const deleted = await storage.deleteLdapAttribute(id); if (!deleted) { return res.status(404).json({ message: "LDAP attribute not found" }); } res.json({ success: true }); } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/ldap-filters: * get: * summary: List LDAP filters for a connection * tags: [LDAP Query Builder] * security: * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * responses: * 200: * description: List of LDAP filters * content: * application/json: * schema: * type: array * items: * $ref: '#/components/schemas/LdapFilter' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ /** * @swagger * /api/connections/{connectionId}/test-filter: * post: * summary: Test an LDAP filter against a connection * tags: [LDAP Query Builder] * security: * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - ldapFilter * - objectClass * properties: * ldapFilter: * type: string * objectClass: * type: string * enum: [user, group, organizationalUnit, computer, domain] * responses: * 200: * description: Filter test results * content: * application/json: * schema: * type: array * items: * type: object * 400: * $ref: '#/components/responses/BadRequestError' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.post("/api/connections/:connectionId/test-filter", requireAuth, async (req: Request, res: Response, next: NextFunction) => { try { const connectionId = parseInt(req.params.connectionId); const { ldapFilter, objectClass } = req.body; if (!ldapFilter || !objectClass) { return res.status(400).json({ message: "ldapFilter and objectClass are required" }); } // Verify the connection exists const connection = await storage.getLdapConnection(connectionId); if (!connection) { return res.status(404).json({ message: "LDAP connection not found" }); } // Test the filter const results = await storage.testLdapFilter(connectionId, ldapFilter, objectClass); res.json(results); } catch (error) { next(error); } }); app.get("/api/connections/:connectionId/ldap-filters", requireAuth, async (req: Request, res: Response, next: NextFunction) => { try { const connectionId = parseInt(req.params.connectionId); // Verify the connection exists const connection = await storage.getLdapConnection(connectionId); if (!connection) { return res.status(404).json({ message: "LDAP connection not found" }); } const filters = await storage.listLdapFilters(connectionId); res.json(filters); } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/ldap-filters: * post: * summary: Create a new LDAP filter * tags: [LDAP Query Builder] * security: * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - name * - objectClass * - filter * - ldapFilter * properties: * name: * type: string * description: * type: string * objectClass: * type: string * enum: [user, group, organizationalUnit, computer, domain] * filter: * type: object * ldapFilter: * type: string * responses: * 201: * description: Filter created successfully * content: * application/json: * schema: * $ref: '#/components/schemas/LdapFilter' * 400: * $ref: '#/components/responses/BadRequestError' * 401: * $ref: '#/components/responses/UnauthorizedError' */ app.post("/api/connections/:connectionId/ldap-filters", requireAuth, async (req: Request, res: Response, next: NextFunction) => { try { const connectionId = parseInt(req.params.connectionId); // Verify the connection exists const connection = await storage.getLdapConnection(connectionId); if (!connection) { return res.status(404).json({ message: "LDAP connection not found" }); } const filter = await storage.createLdapFilter({ ...req.body, connectionId, createdBy: req.user.id, modifiedBy: req.user.id }); res.status(201).json(filter); } catch (error) { next(error); } }); /** * @swagger * /api/ldap-filters/{id}: * get: * summary: Get a specific LDAP filter * tags: [LDAP Query Builder] * security: * - cookieAuth: [] * parameters: * - name: id * in: path * required: true * schema: * type: integer * responses: * 200: * description: LDAP filter details * content: * application/json: * schema: * $ref: '#/components/schemas/LdapFilter' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.get("/api/ldap-filters/:id", requireAuth, async (req: Request, res: Response, next: NextFunction) => { try { const id = parseInt(req.params.id); const filter = await storage.getLdapFilter(id); if (!filter) { return res.status(404).json({ message: "LDAP filter not found" }); } res.json(filter); } catch (error) { next(error); } }); /** * @swagger * /api/ldap-filters/{id}: * put: * summary: Update an LDAP filter * tags: [LDAP Query Builder] * security: * - cookieAuth: [] * parameters: * - name: id * in: path * required: true * schema: * type: integer * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * name: * type: string * description: * type: string * filter: * type: object * ldapFilter: * type: string * isActive: * type: boolean * responses: * 200: * description: Filter updated successfully * content: * application/json: * schema: * $ref: '#/components/schemas/LdapFilter' * 400: * $ref: '#/components/responses/BadRequestError' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.put("/api/ldap-filters/:id", requireAuth, async (req: Request, res: Response, next: NextFunction) => { try { const id = parseInt(req.params.id); // Check if the filter exists const existingFilter = await storage.getLdapFilter(id); if (!existingFilter) { return res.status(404).json({ message: "LDAP filter not found" }); } // Update with the current user as modifier const updatedFilter = await storage.updateLdapFilter(id, { ...req.body, modifiedBy: req.user.id }); res.json(updatedFilter); } catch (error) { next(error); } }); /** * @swagger * /api/ldap-filters/{id}: * delete: * summary: Delete an LDAP filter * tags: [LDAP Query Builder] * security: * - cookieAuth: [] * parameters: * - name: id * in: path * required: true * schema: * type: integer * responses: * 200: * description: Filter deleted successfully * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.delete("/api/ldap-filters/:id", requireAuth, async (req: Request, res: Response, next: NextFunction) => { try { const id = parseInt(req.params.id); // Check if the filter exists and if the user is allowed to delete it const filter = await storage.getLdapFilter(id); if (!filter) { return res.status(404).json({ message: "LDAP filter not found" }); } // Only allow the creator or admins to delete if (filter.createdBy !== req.user.id) { const userRole = await storage.getRole(req.user.roleId!); if (userRole?.name !== "admin") { return res.status(403).json({ message: "You are not authorized to delete this filter" }); } } const deleted = await storage.deleteLdapFilter(id); res.json({ success: deleted }); } catch (error) { next(error); } }); /** * @swagger * /api/ldap-filters/{filterId}/revisions: * get: * summary: Get the revision history for an LDAP filter * tags: [LDAP Query Builder] * security: * - cookieAuth: [] * parameters: * - name: filterId * in: path * required: true * schema: * type: integer * responses: * 200: * description: List of filter revisions * content: * application/json: * schema: * type: array * items: * $ref: '#/components/schemas/LdapFilterRevision' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.get("/api/ldap-filters/:filterId/revisions", requireAuth, async (req: Request, res: Response, next: NextFunction) => { try { const filterId = parseInt(req.params.filterId); // Check if the filter exists const filter = await storage.getLdapFilter(filterId); if (!filter) { return res.status(404).json({ message: "LDAP filter not found" }); } const revisions = await storage.getLdapFilterRevisions(filterId); res.json(revisions); } catch (error) { next(error); } }); /** * @swagger * /api/ldap-filters/{filterId}/revert/{revisionId}: * post: * summary: Revert a filter to a previous revision * tags: [LDAP Query Builder] * security: * - cookieAuth: [] * parameters: * - name: filterId * in: path * required: true * schema: * type: integer * - name: revisionId * in: path * required: true * schema: * type: integer * responses: * 200: * description: Filter reverted successfully * content: * application/json: * schema: * $ref: '#/components/schemas/LdapFilter' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.post("/api/ldap-filters/:filterId/revert/:revisionId", requireAuth, async (req: Request, res: Response, next: NextFunction) => { try { const filterId = parseInt(req.params.filterId); const revisionId = parseInt(req.params.revisionId); // Check if the filter exists const filter = await storage.getLdapFilter(filterId); if (!filter) { return res.status(404).json({ message: "LDAP filter not found" }); } // Check if the user has permission to modify this filter if (filter.createdBy !== req.user.id) { const userRole = await storage.getRole(req.user.roleId!); if (userRole?.name !== "admin") { return res.status(403).json({ message: "You are not authorized to modify this filter" }); } } // First update the modifiedBy to the current user await storage.updateLdapFilter(filterId, { modifiedBy: req.user.id }); // Then perform the revert const revertedFilter = await storage.revertLdapFilterToRevision(filterId, revisionId); if (!revertedFilter) { return res.status(404).json({ message: "Failed to revert filter or revision not found" }); } res.json(revertedFilter); } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/test-ldap-filter: * post: * summary: Test an LDAP filter against a connection * tags: [LDAP Query Builder] * security: * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - ldapFilter * - objectClass * properties: * ldapFilter: * type: string * objectClass: * type: string * enum: [user, group, organizationalUnit, computer, domain] * responses: * 200: * description: Test results * content: * application/json: * schema: * type: array * items: * type: object * 400: * $ref: '#/components/responses/BadRequestError' * 401: * $ref: '#/components/responses/UnauthorizedError' */ app.post("/api/connections/:connectionId/test-ldap-filter", requireAuth, async (req: Request, res: Response, next: NextFunction) => { try { const connectionId = parseInt(req.params.connectionId); const { ldapFilter, objectClass } = req.body; if (!ldapFilter || !objectClass) { return res.status(400).json({ message: "ldapFilter and objectClass are required" }); } // Verify the connection exists const connection = await storage.getLdapConnection(connectionId); if (!connection) { return res.status(404).json({ message: "LDAP connection not found" }); } const results = await storage.testLdapFilter(connectionId, ldapFilter, objectClass); res.json(results); } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/move-computer: * post: * summary: Move a computer to a different OU * tags: [AD Computers] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - computerObjectGUID * - targetOUDistinguishedName * properties: * computerObjectGUID: * type: string * targetOUDistinguishedName: * type: string * responses: * 200: * description: Computer moved successfully * content: * application/json: * schema: * type: object * properties: * success: * type: boolean * message: * type: string * 400: * $ref: '#/components/responses/BadRequestError' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.post("/api/connections/:connectionId/move-computer", authenticateApiToken, async (req, res, next) => { try { const connectionId = parseInt(req.params.connectionId); const connection = await storage.getLdapConnection(connectionId); if (!connection) { return res.status(404).json({ message: "LDAP connection not found" }); } try { const data = moveComputerSchema.parse(req.body); // This would call a method in the LDAP client to move the computer // For now we'll return a mock success response // In a real implementation, this would interact with the Active Directory // Record the action in the audit log const auditEntry = { action: "MOVE_COMPUTER", targetId: data.computerObjectGUID, details: { targetOU: data.targetOUDistinguishedName }, userId: req.user?.id || null, connectionId: connectionId }; // Save the audit entry to storage await storage.createAuditLogEntry(auditEntry); res.json({ success: true, message: `Computer with GUID ${data.computerObjectGUID} moved to ${data.targetOUDistinguishedName}` }); } catch (error) { if (error instanceof ZodError) { return handleZodError(error, res); } throw error; } } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/move-user: * post: * summary: Move a user to a different OU * tags: [AD Users] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - userObjectGUID * - targetOUDistinguishedName * properties: * userObjectGUID: * type: string * targetOUDistinguishedName: * type: string * responses: * 200: * description: User moved successfully * content: * application/json: * schema: * type: object * properties: * success: * type: boolean * message: * type: string * 400: * $ref: '#/components/responses/BadRequestError' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.post("/api/connections/:connectionId/move-user", authenticateApiToken, async (req, res, next) => { try { const connectionId = parseInt(req.params.connectionId); const connection = await storage.getLdapConnection(connectionId); if (!connection) { return res.status(404).json({ message: "LDAP connection not found" }); } try { const data = moveUserSchema.parse(req.body); // This would call a method in the LDAP client to move the user // For now we'll return a mock success response // In a real implementation, this would interact with the Active Directory // Record the action in the audit log const auditEntry = { action: "MOVE_USER", targetId: data.userObjectGUID, details: { targetOU: data.targetOUDistinguishedName }, userId: req.user?.id || null, connectionId: connectionId }; // Save the audit entry to storage await storage.createAuditLogEntry(auditEntry); res.json({ success: true, message: `User with GUID ${data.userObjectGUID} moved to ${data.targetOUDistinguishedName}` }); } catch (error) { if (error instanceof ZodError) { return handleZodError(error, res); } throw error; } } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/add-to-group: * post: * summary: Add a user or computer to a group * tags: [AD Groups] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - objectGUID * - groupObjectGUID * - objectType * properties: * objectGUID: * type: string * groupObjectGUID: * type: string * objectType: * type: string * enum: [user, computer] * responses: * 200: * description: Object added to group successfully * content: * application/json: * schema: * type: object * properties: * success: * type: boolean * message: * type: string * 400: * $ref: '#/components/responses/BadRequestError' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.post("/api/connections/:connectionId/add-to-group", authenticateApiToken, async (req, res, next) => { try { const connectionId = parseInt(req.params.connectionId); const connection = await storage.getLdapConnection(connectionId); if (!connection) { return res.status(404).json({ message: "LDAP connection not found" }); } try { const data = addToGroupSchema.parse(req.body); // This would call a method in the LDAP client to add the object to the group // For now we'll return a mock success response // In a real implementation, this would interact with the Active Directory // Record the action in the audit log const auditEntry = { action: "ADD_TO_GROUP", targetId: data.objectGUID, details: { groupGUID: data.groupObjectGUID, objectType: data.objectType }, userId: req.user?.id || null, connectionId: connectionId }; // Save the audit entry to storage await storage.createAuditLogEntry(auditEntry); res.json({ success: true, message: `${data.objectType} with GUID ${data.objectGUID} added to group with GUID ${data.groupObjectGUID}` }); } catch (error) { if (error instanceof ZodError) { return handleZodError(error, res); } throw error; } } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/audit-logs: * get: * summary: Retrieve audit logs for a connection * tags: [Audit] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * responses: * 200: * description: List of audit logs * content: * application/json: * schema: * type: array * items: * type: object * properties: * id: * type: integer * timestamp: * type: string * format: date-time * userId: * type: integer * nullable: true * action: * type: string * targetId: * type: string * details: * type: object * connectionId: * type: integer * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ // AD Sites endpoints /** * @swagger * /api/connections/{connectionId}/ad-sites: * get: * summary: List AD sites from the specified LDAP connection * tags: [AD Sites] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * - $ref: '#/components/parameters/filterParam' * - $ref: '#/components/parameters/selectParam' * - $ref: '#/components/parameters/expandParam' * - $ref: '#/components/parameters/orderByParam' * - $ref: '#/components/parameters/topParam' * - $ref: '#/components/parameters/skipParam' * responses: * 200: * description: A list of AD sites * content: * application/json: * schema: * type: object * properties: * data: * type: array * items: * $ref: '#/components/schemas/AdSite' * metadata: * $ref: '#/components/responses/PaginatedResponse/content/application~1json/schema/properties/metadata' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.get("/api/connections/:connectionId/ad-sites", authenticateApiToken, async (req, res, next) => { try { if (!req.isAuthenticated() && !req.headers.authorization) { return res.status(401).json({ message: "Authentication required" }); } const connectionId = parseInt(req.params.connectionId); const connection = await storage.getLdapConnection(connectionId); if (!connection) { return res.status(404).json({ message: "LDAP connection not found" }); } const query = parseQueryParams(req); const sites = await storage.listAdSites(connectionId, query); // Count total records for pagination metadata const countQuery = db.select({ count: sql`count(*)` }).from(adSites) .where(eq(adSites.connectionId, connectionId)); // Apply filters if present if (query && query.filter) { const conditions = parseFilter(query.filter); const whereClause = applyFilterConditions(adSites, conditions); if (whereClause) { countQuery.where(whereClause); } } const [countResult] = await countQuery; const totalRecords = Number(countResult?.count || 0); // Add objectType to each result const sitesWithObjectType = sites.map(site => ({ ...site, objectType: 'site' })); // Generate pagination metadata const { limit, offset } = parsePagination(query?.top, query?.skip); const paginationMetadata = generatePaginationMetadata( totalRecords, limit, offset, `${req.protocol}://${req.get('host')}${req.originalUrl}` ); res.json({ data: sitesWithObjectType, metadata: paginationMetadata }); } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/ad-sites/{objectGUID}: * get: * summary: Get details of a specific AD site * tags: [AD Sites] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * - name: objectGUID * in: path * required: true * schema: * type: string * responses: * 200: * description: Details of the AD site * content: * application/json: * schema: * $ref: '#/components/schemas/AdSite' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.get("/api/connections/:connectionId/ad-sites/:objectGUID", authenticateApiToken, async (req, res, next) => { try { if (!req.isAuthenticated() && !req.headers.authorization) { return res.status(401).json({ message: "Authentication required" }); } const connectionId = parseInt(req.params.connectionId); const objectGUID = req.params.objectGUID; const site = await storage.getAdSiteByObjectGUID(connectionId, objectGUID); if (!site) { return res.status(404).json({ message: "AD site not found" }); } // Add objectType to the result const siteWithObjectType = { ...site, objectType: 'site' }; res.json(siteWithObjectType); } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/ad-sites: * post: * summary: Create a new AD site * tags: [AD Sites] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * name: * type: string * description: * type: string * location: * type: string * responses: * 201: * description: Successfully created AD site * content: * application/json: * schema: * $ref: '#/components/schemas/AdSite' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.post("/api/connections/:connectionId/ad-sites", authenticateApiToken, async (req, res, next) => { try { if (!req.isAuthenticated() && !req.headers.authorization) { return res.status(401).json({ message: "Authentication required" }); } const connectionId = parseInt(req.params.connectionId); const connection = await storage.getLdapConnection(connectionId); if (!connection) { return res.status(404).json({ message: "LDAP connection not found" }); } // First create the site in AD const siteName = req.body.name; const description = req.body.description || ''; const location = req.body.location || ''; // Create site in AD using LDAP const client = ldapClient.getClient(connectionId); if (!client) { const connected = await ldapClient.connect(connection); if (!connected) { return res.status(500).json({ message: "Failed to connect to LDAP server" }); } } // Create a CN for the new site const siteDN = `CN=${siteName},CN=Sites,CN=Configuration,${ldapClient.getDomainDN(connection)}`; // Attributes for the new site const siteAttributes = { objectClass: ['site'], cn: siteName, description: description, location: location }; try { const success = await ldapClient.createEntry(connectionId, siteDN, siteAttributes); if (!success) { return res.status(500).json({ message: "Failed to create AD site" }); } // Search for the site to get all its attributes const siteResults = await ldapClient.searchSites(connectionId, `(cn=${siteName})`); if (!siteResults || siteResults.length === 0) { return res.status(500).json({ message: "Site created but could not retrieve details" }); } const siteData = siteResults[0]; // Create entry in our database const newSite = await storage.createAdSite({ connectionId, name: siteName, objectGUID: siteData.objectGUID || uuidv4(), objectType: 'site', dn: siteDN, canonicalName: `${connection.domain}/Configuration/Sites/${siteName}`, description: description, location: location, managedBy: null, adProperties: siteData }); // Create audit log entry await storage.createAuditLogEntry({ action: 'create', targetId: newSite.id.toString(), details: { type: 'site', name: siteName, dn: siteDN }, userId: req.user?.id, connectionId }); res.status(201).json(newSite); } catch (err) { console.error("Error creating AD site:", err); return res.status(500).json({ message: `Error creating AD site: ${err.message}` }); } } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/ad-sites/{objectGUID}: * patch: * summary: Update an AD site * tags: [AD Sites] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * - name: objectGUID * in: path * required: true * schema: * type: string * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * description: * type: string * location: * type: string * managedBy: * type: string * responses: * 200: * description: Successfully updated AD site * content: * application/json: * schema: * $ref: '#/components/schemas/AdSite' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.patch("/api/connections/:connectionId/ad-sites/:objectGUID", authenticateApiToken, async (req, res, next) => { try { if (!req.isAuthenticated() && !req.headers.authorization) { return res.status(401).json({ message: "Authentication required" }); } const connectionId = parseInt(req.params.connectionId); const objectGUID = req.params.objectGUID; const site = await storage.getAdSiteByObjectGUID(connectionId, objectGUID); if (!site) { return res.status(404).json({ message: "AD site not found" }); } const connection = await storage.getLdapConnection(connectionId); if (!connection) { return res.status(404).json({ message: "LDAP connection not found" }); } // Connect to LDAP if needed const client = ldapClient.getClient(connectionId); if (!client) { const connected = await ldapClient.connect(connection); if (!connected) { return res.status(500).json({ message: "Failed to connect to LDAP server" }); } } // Prepare changes const changes = []; const updateData: Partial = {}; if (req.body.description !== undefined) { changes.push({ operation: 'replace', modification: { description: req.body.description } }); updateData.description = req.body.description; } if (req.body.location !== undefined) { changes.push({ operation: 'replace', modification: { location: req.body.location } }); updateData.location = req.body.location; } // Handle managedBy separately if (req.body.managedBy !== undefined) { // Set the managedBy attribute const success = await ldapClient.setManagedBy(connectionId, site.dn, req.body.managedBy); if (!success) { return res.status(500).json({ message: "Failed to update managedBy attribute" }); } updateData.managedBy = req.body.managedBy; } // Apply changes to LDAP if there are any attribute changes if (changes.length > 0) { try { const success = await ldapClient.updateEntry(connectionId, site.dn, changes); if (!success) { return res.status(500).json({ message: "Failed to update AD site" }); } } catch (err) { console.error("Error updating AD site:", err); return res.status(500).json({ message: `Error updating AD site: ${err.message}` }); } } // Update our database record const updatedSite = await storage.updateAdSiteByObjectGUID(connectionId, objectGUID, updateData); // Create audit log entry await storage.createAuditLogEntry({ action: 'update', targetId: site.id.toString(), details: { type: 'site', name: site.name, dn: site.dn, changes: updateData }, userId: req.user?.id, connectionId }); res.json(updatedSite); } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/ad-sites/{objectGUID}: * delete: * summary: Delete an AD site * tags: [AD Sites] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * - name: objectGUID * in: path * required: true * schema: * type: string * responses: * 200: * description: Successfully deleted AD site * content: * application/json: * schema: * type: object * properties: * success: * type: boolean * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.delete("/api/connections/:connectionId/ad-sites/:objectGUID", authenticateApiToken, async (req, res, next) => { try { if (!req.isAuthenticated() && !req.headers.authorization) { return res.status(401).json({ message: "Authentication required" }); } const connectionId = parseInt(req.params.connectionId); const objectGUID = req.params.objectGUID; const site = await storage.getAdSiteByObjectGUID(connectionId, objectGUID); if (!site) { return res.status(404).json({ message: "AD site not found" }); } const connection = await storage.getLdapConnection(connectionId); if (!connection) { return res.status(404).json({ message: "LDAP connection not found" }); } // Connect to LDAP if needed const client = ldapClient.getClient(connectionId); if (!client) { const connected = await ldapClient.connect(connection); if (!connected) { return res.status(500).json({ message: "Failed to connect to LDAP server" }); } } // Delete from LDAP try { const success = await ldapClient.deleteEntry(connectionId, site.dn); if (!success) { return res.status(500).json({ message: "Failed to delete AD site" }); } // Delete from our database await storage.deleteAdSite(site.id); // Create audit log entry await storage.createAuditLogEntry({ action: 'delete', targetId: site.id.toString(), details: { type: 'site', name: site.name, dn: site.dn }, userId: req.user?.id, connectionId }); res.json({ success: true }); } catch (err) { console.error("Error deleting AD site:", err); return res.status(500).json({ message: `Error deleting AD site: ${err.message}` }); } } catch (error) { next(error); } }); // AD Subnets endpoints /** * @swagger * /api/connections/{connectionId}/ad-subnets: * get: * summary: List AD subnets from the specified LDAP connection * tags: [AD Subnets] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * - $ref: '#/components/parameters/filterParam' * - $ref: '#/components/parameters/selectParam' * - $ref: '#/components/parameters/expandParam' * - $ref: '#/components/parameters/orderByParam' * - $ref: '#/components/parameters/topParam' * - $ref: '#/components/parameters/skipParam' * responses: * 200: * description: A list of AD subnets * content: * application/json: * schema: * type: object * properties: * data: * type: array * items: * $ref: '#/components/schemas/AdSubnet' * metadata: * $ref: '#/components/responses/PaginatedResponse/content/application~1json/schema/properties/metadata' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.get("/api/connections/:connectionId/ad-subnets", authenticateApiToken, async (req, res, next) => { try { if (!req.isAuthenticated() && !req.headers.authorization) { return res.status(401).json({ message: "Authentication required" }); } const connectionId = parseInt(req.params.connectionId); const connection = await storage.getLdapConnection(connectionId); if (!connection) { return res.status(404).json({ message: "LDAP connection not found" }); } const query = parseQueryParams(req); const subnets = await storage.listAdSubnets(connectionId, query); // Count total records for pagination metadata const countQuery = db.select({ count: sql`count(*)` }).from(adSubnets) .where(eq(adSubnets.connectionId, connectionId)); // Apply filters if present if (query && query.filter) { const conditions = parseFilter(query.filter); const whereClause = applyFilterConditions(adSubnets, conditions); if (whereClause) { countQuery.where(whereClause); } } const [countResult] = await countQuery; const totalRecords = Number(countResult?.count || 0); // Add objectType to each result const subnetsWithObjectType = subnets.map(subnet => ({ ...subnet, objectType: 'subnet' })); // Generate pagination metadata const { limit, offset } = parsePagination(query?.top, query?.skip); const paginationMetadata = generatePaginationMetadata( totalRecords, limit, offset, `${req.protocol}://${req.get('host')}${req.originalUrl}` ); res.json({ data: subnetsWithObjectType, metadata: paginationMetadata }); } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/ad-subnets/{objectGUID}: * get: * summary: Get details of a specific AD subnet * tags: [AD Subnets] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * - name: objectGUID * in: path * required: true * schema: * type: string * responses: * 200: * description: Details of the AD subnet * content: * application/json: * schema: * $ref: '#/components/schemas/AdSubnet' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.get("/api/connections/:connectionId/ad-subnets/:objectGUID", authenticateApiToken, async (req, res, next) => { try { if (!req.isAuthenticated() && !req.headers.authorization) { return res.status(401).json({ message: "Authentication required" }); } const connectionId = parseInt(req.params.connectionId); const objectGUID = req.params.objectGUID; const subnet = await storage.getAdSubnetByObjectGUID(connectionId, objectGUID); if (!subnet) { return res.status(404).json({ message: "AD subnet not found" }); } // Add objectType to the result const subnetWithObjectType = { ...subnet, objectType: 'subnet' }; res.json(subnetWithObjectType); } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/ad-subnets: * post: * summary: Create a new AD subnet * tags: [AD Subnets] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * name: * type: string * description: The subnet name (e.g., 192.168.1.0/24) * description: * type: string * location: * type: string * siteObject: * type: string * description: The distinguishedName of the site this subnet belongs to * cidr: * type: string * description: The subnet CIDR notation (e.g., 192.168.1.0/24) * responses: * 201: * description: Successfully created AD subnet * content: * application/json: * schema: * $ref: '#/components/schemas/AdSubnet' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.post("/api/connections/:connectionId/ad-subnets", authenticateApiToken, async (req, res, next) => { try { if (!req.isAuthenticated() && !req.headers.authorization) { return res.status(401).json({ message: "Authentication required" }); } const connectionId = parseInt(req.params.connectionId); const connection = await storage.getLdapConnection(connectionId); if (!connection) { return res.status(404).json({ message: "LDAP connection not found" }); } // First create the subnet in AD const subnetName = req.body.name; const description = req.body.description || ''; const location = req.body.location || ''; const siteObject = req.body.siteObject || ''; const cidr = req.body.cidr || ''; if (!subnetName) { return res.status(400).json({ message: "Subnet name is required" }); } // Create subnet in AD using LDAP const client = ldapClient.getClient(connectionId); if (!client) { const connected = await ldapClient.connect(connection); if (!connected) { return res.status(500).json({ message: "Failed to connect to LDAP server" }); } } // Create a CN for the new subnet const subnetDN = `CN=${subnetName},CN=Subnets,CN=Sites,CN=Configuration,${ldapClient.getDomainDN(connection)}`; // Attributes for the new subnet const subnetAttributes = { objectClass: ['subnet'], cn: subnetName, description: description, location: location }; // Add siteObject if provided if (siteObject) { subnetAttributes.siteObject = siteObject; } try { const success = await ldapClient.createEntry(connectionId, subnetDN, subnetAttributes); if (!success) { return res.status(500).json({ message: "Failed to create AD subnet" }); } // Search for the subnet to get all its attributes const subnetResults = await ldapClient.searchSubnets(connectionId, `(cn=${subnetName})`); if (!subnetResults || subnetResults.length === 0) { return res.status(500).json({ message: "Subnet created but could not retrieve details" }); } const subnetData = subnetResults[0]; // Create entry in our database const newSubnet = await storage.createAdSubnet({ connectionId, name: subnetName, objectGUID: subnetData.objectGUID || uuidv4(), objectType: 'subnet', dn: subnetDN, canonicalName: `${connection.domain}/Configuration/Sites/Subnets/${subnetName}`, description: description, location: location, siteObject: siteObject, cidr: cidr, managedBy: null, adProperties: subnetData }); // Create audit log entry await storage.createAuditLogEntry({ action: 'create', targetId: newSubnet.id.toString(), details: { type: 'subnet', name: subnetName, dn: subnetDN }, userId: req.user?.id, connectionId }); res.status(201).json(newSubnet); } catch (err) { console.error("Error creating AD subnet:", err); return res.status(500).json({ message: `Error creating AD subnet: ${err.message}` }); } } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/ad-subnets/{objectGUID}: * patch: * summary: Update an AD subnet * tags: [AD Subnets] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * - name: objectGUID * in: path * required: true * schema: * type: string * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * description: * type: string * location: * type: string * siteObject: * type: string * description: The distinguishedName of the site this subnet belongs to * cidr: * type: string * description: The subnet CIDR notation (e.g., 192.168.1.0/24) * managedBy: * type: string * responses: * 200: * description: Successfully updated AD subnet * content: * application/json: * schema: * $ref: '#/components/schemas/AdSubnet' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.patch("/api/connections/:connectionId/ad-subnets/:objectGUID", authenticateApiToken, async (req, res, next) => { try { if (!req.isAuthenticated() && !req.headers.authorization) { return res.status(401).json({ message: "Authentication required" }); } const connectionId = parseInt(req.params.connectionId); const objectGUID = req.params.objectGUID; const subnet = await storage.getAdSubnetByObjectGUID(connectionId, objectGUID); if (!subnet) { return res.status(404).json({ message: "AD subnet not found" }); } const connection = await storage.getLdapConnection(connectionId); if (!connection) { return res.status(404).json({ message: "LDAP connection not found" }); } // Connect to LDAP if needed const client = ldapClient.getClient(connectionId); if (!client) { const connected = await ldapClient.connect(connection); if (!connected) { return res.status(500).json({ message: "Failed to connect to LDAP server" }); } } // Prepare changes const changes = []; const updateData: Partial = {}; if (req.body.description !== undefined) { changes.push({ operation: 'replace', modification: { description: req.body.description } }); updateData.description = req.body.description; } if (req.body.location !== undefined) { changes.push({ operation: 'replace', modification: { location: req.body.location } }); updateData.location = req.body.location; } if (req.body.siteObject !== undefined) { changes.push({ operation: 'replace', modification: { siteObject: req.body.siteObject } }); updateData.siteObject = req.body.siteObject; } if (req.body.cidr !== undefined) { updateData.cidr = req.body.cidr; } // Handle managedBy separately if (req.body.managedBy !== undefined) { // Set the managedBy attribute const success = await ldapClient.setManagedBy(connectionId, subnet.dn, req.body.managedBy); if (!success) { return res.status(500).json({ message: "Failed to update managedBy attribute" }); } updateData.managedBy = req.body.managedBy; } // Apply changes to LDAP if there are any attribute changes if (changes.length > 0) { try { const success = await ldapClient.updateEntry(connectionId, subnet.dn, changes); if (!success) { return res.status(500).json({ message: "Failed to update AD subnet" }); } } catch (err) { console.error("Error updating AD subnet:", err); return res.status(500).json({ message: `Error updating AD subnet: ${err.message}` }); } } // Update our database record const updatedSubnet = await storage.updateAdSubnetByObjectGUID(connectionId, objectGUID, updateData); // Create audit log entry await storage.createAuditLogEntry({ action: 'update', targetId: subnet.id.toString(), details: { type: 'subnet', name: subnet.name, dn: subnet.dn, changes: updateData }, userId: req.user?.id, connectionId }); res.json(updatedSubnet); } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/ad-subnets/{objectGUID}: * delete: * summary: Delete an AD subnet * tags: [AD Subnets] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * - name: objectGUID * in: path * required: true * schema: * type: string * responses: * 200: * description: Successfully deleted AD subnet * content: * application/json: * schema: * type: object * properties: * success: * type: boolean * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.delete("/api/connections/:connectionId/ad-subnets/:objectGUID", authenticateApiToken, async (req, res, next) => { try { if (!req.isAuthenticated() && !req.headers.authorization) { return res.status(401).json({ message: "Authentication required" }); } const connectionId = parseInt(req.params.connectionId); const objectGUID = req.params.objectGUID; const subnet = await storage.getAdSubnetByObjectGUID(connectionId, objectGUID); if (!subnet) { return res.status(404).json({ message: "AD subnet not found" }); } const connection = await storage.getLdapConnection(connectionId); if (!connection) { return res.status(404).json({ message: "LDAP connection not found" }); } // Connect to LDAP if needed const client = ldapClient.getClient(connectionId); if (!client) { const connected = await ldapClient.connect(connection); if (!connected) { return res.status(500).json({ message: "Failed to connect to LDAP server" }); } } // Delete from LDAP try { const success = await ldapClient.deleteEntry(connectionId, subnet.dn); if (!success) { return res.status(500).json({ message: "Failed to delete AD subnet" }); } // Delete from our database await storage.deleteAdSubnet(subnet.id); // Create audit log entry await storage.createAuditLogEntry({ action: 'delete', targetId: subnet.id.toString(), details: { type: 'subnet', name: subnet.name, dn: subnet.dn }, userId: req.user?.id, connectionId }); res.json({ success: true }); } catch (err) { console.error("Error deleting AD subnet:", err); return res.status(500).json({ message: `Error deleting AD subnet: ${err.message}` }); } } catch (error) { next(error); } }); app.get("/api/connections/:connectionId/audit-logs", authenticateApiToken, async (req, res, next) => { try { const connectionId = parseInt(req.params.connectionId); const connection = await storage.getLdapConnection(connectionId); if (!connection) { return res.status(404).json({ message: "LDAP connection not found" }); } const logs = await storage.getAuditLogs(connectionId); res.json(logs); } catch (error) { next(error); } }); /** * @swagger * /api/audit-logs: * get: * summary: Retrieve all audit logs * tags: [Audit] * security: * - bearerAuth: [] * - cookieAuth: [] * responses: * 200: * description: List of all audit logs * content: * application/json: * schema: * type: array * items: * type: object * properties: * id: * type: integer * timestamp: * type: string * format: date-time * userId: * type: integer * nullable: true * action: * type: string * targetId: * type: string * details: * type: object * connectionId: * type: integer * 401: * $ref: '#/components/responses/UnauthorizedError' */ app.get("/api/audit-logs", authenticateApiToken, async (req, res, next) => { try { const logs = await storage.getAuditLogs(); res.json(logs); } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/remove-from-group: * post: * summary: Remove a user or computer from a group * tags: [AD Groups] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - objectGUID * - groupObjectGUID * - objectType * properties: * objectGUID: * type: string * groupObjectGUID: * type: string * objectType: * type: string * enum: [user, computer] * responses: * 200: * description: Object removed from group successfully * content: * application/json: * schema: * type: object * properties: * success: * type: boolean * message: * type: string * 400: * $ref: '#/components/responses/BadRequestError' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.post("/api/connections/:connectionId/remove-from-group", authenticateApiToken, async (req, res, next) => { try { const connectionId = parseInt(req.params.connectionId); const connection = await storage.getLdapConnection(connectionId); if (!connection) { return res.status(404).json({ message: "LDAP connection not found" }); } try { const data = removeFromGroupSchema.parse(req.body); // This would call a method in the LDAP client to remove the object from the group // For now we'll return a mock success response // In a real implementation, this would interact with the Active Directory // Record the action in the audit log const auditEntry = { action: "REMOVE_FROM_GROUP", targetId: data.objectGUID, details: { groupGUID: data.groupObjectGUID, objectType: data.objectType }, userId: req.user?.id || null, connectionId: connectionId }; // Save the audit entry to storage await storage.createAuditLogEntry(auditEntry); res.json({ success: true, message: `${data.objectType} with GUID ${data.objectGUID} removed from group with GUID ${data.groupObjectGUID}` }); } catch (error) { if (error instanceof ZodError) { return handleZodError(error, res); } throw error; } } catch (error) { next(error); } }); // This dedicated API endpoint has been replaced by object-specific endpoints using the PATCH method // See: /api/connections/:connectionId/ad-users/:id/managed-by, // /api/connections/:connectionId/ad-groups/:id/managed-by, // /api/connections/:connectionId/ad-computers/:id/managed-by, // /api/connections/:connectionId/ad-org-units/:id/managed-by // This dedicated API endpoint has been replaced by object-specific endpoints using the PATCH method // See: /api/connections/:connectionId/ad-users/:objectGuid, // /api/connections/:connectionId/ad-groups/:objectGuid, // /api/connections/:connectionId/ad-computers/:objectGuid, // /api/connections/:connectionId/ad-org-units/:objectGuid /** * @swagger * /api/audit-logs: * get: * summary: Get all audit logs * tags: [Audit Logs] * security: * - cookieAuth: [] * parameters: * - name: page * in: query * required: false * schema: * type: integer * default: 1 * description: Page number * - name: pageSize * in: query * required: false * schema: * type: integer * default: 10 * description: Number of items per page * responses: * 200: * description: Paginated list of audit logs * content: * application/json: * schema: * type: object * properties: * data: * type: array * items: * $ref: '#/components/schemas/AuditLog' * metadata: * type: object * properties: * currentPage: * type: integer * totalPages: * type: integer * totalRecords: * type: integer * nextPage: * type: integer * nullable: true * prevPage: * type: integer * nullable: true * 401: * $ref: '#/components/responses/UnauthorizedError' */ app.get("/api/audit-logs", requireAuth, async (req, res, next) => { try { const page = req.query.page ? parseInt(req.query.page as string) : 1; const pageSize = req.query.pageSize ? parseInt(req.query.pageSize as string) : 10; const result = await storage.getAuditLogs(undefined, undefined, page, pageSize); res.json(result); } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/audit-logs: * get: * summary: Get audit logs for a specific connection * tags: [Audit Logs] * security: * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * - name: page * in: query * required: false * schema: * type: integer * default: 1 * description: Page number * - name: pageSize * in: query * required: false * schema: * type: integer * default: 10 * description: Number of items per page * responses: * 200: * description: Paginated list of audit logs for the specified connection * content: * application/json: * schema: * type: object * properties: * data: * type: array * items: * $ref: '#/components/schemas/AuditLog' * metadata: * type: object * properties: * currentPage: * type: integer * totalPages: * type: integer * totalRecords: * type: integer * nextPage: * type: integer * nullable: true * prevPage: * type: integer * nullable: true * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.get("/api/connections/:connectionId/audit-logs", requireAuth, async (req, res, next) => { try { const connectionId = parseInt(req.params.connectionId, 10); const page = req.query.page ? parseInt(req.query.page as string) : 1; const pageSize = req.query.pageSize ? parseInt(req.query.pageSize as string) : 10; // Verify the connection exists const connection = await storage.getLdapConnection(connectionId); if (!connection) { return res.status(404).json({ message: "Connection not found" }); } const result = await storage.getAuditLogs(connectionId, undefined, page, pageSize); res.json(result); } catch (error) { next(error); } }); /** * @swagger * /api/user-dashboard-data: * get: * summary: Get user data for dashboard visualizations * tags: [Dashboard] * security: * - cookieAuth: [] * - bearerAuth: [] * responses: * 200: * description: User data for dashboard * 401: * $ref: '#/components/responses/UnauthorizedError' */ app.get("/api/user-dashboard-data", requireAuth({ allowApiToken: true }), async (req, res, next) => { try { // Get data from the first available LDAP connection const connections = await storage.listLdapConnections(); if (connections.length === 0) { return res.status(200).json({ data: [], message: "No LDAP connections available" }); } const connectionId = connections[0].id; const users = await storage.listAdUsers(connectionId); // Add objectType to make filtering easier in the dashboard const result = users.map(user => ({ ...user, objectType: 'user' })); res.json({ data: result }); } catch (error) { next(error); } }); /** * @swagger * /api/computer-dashboard-data: * get: * summary: Get computer data for dashboard visualizations * tags: [Dashboard] * security: * - cookieAuth: [] * - bearerAuth: [] * responses: * 200: * description: Computer data for dashboard * 401: * $ref: '#/components/responses/UnauthorizedError' */ app.get("/api/computer-dashboard-data", requireAuth({ allowApiToken: true }), async (req, res, next) => { try { // Get data from the first available LDAP connection const connections = await storage.listLdapConnections(); if (connections.length === 0) { return res.status(200).json({ data: [], message: "No LDAP connections available" }); } const connectionId = connections[0].id; const computers = await storage.listAdComputers(connectionId); // Add objectType to make filtering easier in the dashboard const result = computers.map(computer => ({ ...computer, objectType: 'computer' })); res.json({ data: result }); } catch (error) { next(error); } }); /** * @swagger * /api/group-dashboard-data: * get: * summary: Get group data for dashboard visualizations * tags: [Dashboard] * security: * - cookieAuth: [] * - bearerAuth: [] * responses: * 200: * description: Group data for dashboard * 401: * $ref: '#/components/responses/UnauthorizedError' */ app.get("/api/group-dashboard-data", requireAuth({ allowApiToken: true }), async (req, res, next) => { try { // Get data from the first available LDAP connection const connections = await storage.listLdapConnections(); if (connections.length === 0) { return res.status(200).json({ data: [], message: "No LDAP connections available" }); } const connectionId = connections[0].id; const groups = await storage.listAdGroups(connectionId); // Add objectType to make filtering easier in the dashboard const result = groups.map(group => ({ ...group, objectType: 'group' })); res.json({ data: result }); } catch (error) { next(error); } }); /** * @swagger * /api/ou-dashboard-data: * get: * summary: Get Organizational Unit data for dashboard visualizations * tags: [Dashboard] * security: * - cookieAuth: [] * - bearerAuth: [] * responses: * 200: * description: Organizational Unit data for dashboard * 401: * $ref: '#/components/responses/UnauthorizedError' */ app.get("/api/ou-dashboard-data", requireAuth({ allowApiToken: true }), async (req, res, next) => { try { // Get data from the first available LDAP connection const connections = await storage.listLdapConnections(); if (connections.length === 0) { return res.status(200).json({ data: [], message: "No LDAP connections available" }); } const connectionId = connections[0].id; const ous = await storage.listAdOrgUnits(connectionId); // Add objectType to make filtering easier in the dashboard const result = ous.map(ou => ({ ...ou, objectType: 'organizationalUnit' })); res.json({ data: result }); } catch (error) { next(error); } }); /** * @swagger * /api/domain-dashboard-data: * get: * summary: Get domain data for dashboard visualizations * tags: [Dashboard] * security: * - cookieAuth: [] * - bearerAuth: [] * responses: * 200: * description: Domain data for dashboard * 401: * $ref: '#/components/responses/UnauthorizedError' */ app.get("/api/domain-dashboard-data", requireAuth({ allowApiToken: true }), async (req, res, next) => { try { // Get data from the first available LDAP connection const connections = await storage.listLdapConnections(); if (connections.length === 0) { return res.status(200).json({ data: [], message: "No LDAP connections available" }); } const connectionId = connections[0].id; const domains = await storage.listAdDomains(connectionId); // Add objectType to make filtering easier in the dashboard const result = domains.map(domain => ({ ...domain, objectType: 'domain' })); res.json({ data: result }); } catch (error) { next(error); } }); /** * @swagger * /api/site-dashboard-data: * get: * summary: Get site data for dashboard visualizations * tags: [Dashboard] * security: * - cookieAuth: [] * - bearerAuth: [] * responses: * 200: * description: Site data for dashboard * 401: * $ref: '#/components/responses/UnauthorizedError' */ app.get("/api/site-dashboard-data", requireAuth({ allowApiToken: true }), async (req, res, next) => { try { // Get data from the first available LDAP connection const connections = await storage.listLdapConnections(); if (connections.length === 0) { return res.status(200).json({ data: [], message: "No LDAP connections available" }); } const connectionId = connections[0].id; const sites = await storage.listAdSites(connectionId); // Add objectType to make filtering easier in the dashboard const result = sites.map(site => ({ ...site, objectType: 'site' })); res.json({ data: result }); } catch (error) { next(error); } }); /** * @swagger * /api/subnet-dashboard-data: * get: * summary: Get subnet data for dashboard visualizations * tags: [Dashboard] * security: * - cookieAuth: [] * - bearerAuth: [] * responses: * 200: * description: Subnet data for dashboard * 401: * $ref: '#/components/responses/UnauthorizedError' */ app.get("/api/subnet-dashboard-data", requireAuth({ allowApiToken: true }), async (req, res, next) => { try { // Get data from the first available LDAP connection const connections = await storage.listLdapConnections(); if (connections.length === 0) { return res.status(200).json({ data: [], message: "No LDAP connections available" }); } const connectionId = connections[0].id; const subnets = await storage.listAdSubnets(connectionId); // Add objectType to make filtering easier in the dashboard const result = subnets.map(subnet => ({ ...subnet, objectType: 'subnet' })); res.json({ data: result }); } catch (error) { next(error); } }); /** * @swagger * /api/dashboard-summary: * get: * summary: Get summary statistics for the dashboard * tags: [Dashboard] * security: * - cookieAuth: [] * - bearerAuth: [] * responses: * 200: * description: Summary statistics for the dashboard * 401: * $ref: '#/components/responses/UnauthorizedError' */ app.get("/api/dashboard-summary", requireAuth({ allowApiToken: true }), async (req, res, next) => { try { // Get data from the first available LDAP connection const connections = await storage.listLdapConnections(); if (connections.length === 0) { return res.status(200).json({ userCount: 0, computerCount: 0, groupCount: 0, sitesCount: 0, domainsCount: 0, message: "No LDAP connections available" }); } const connectionId = connections[0].id; // Get counts from all AD objects const usersCount = await db.select({ count: count() }).from(adUsers) .where(eq(adUsers.connectionId, connectionId)); const computersCount = await db.select({ count: count() }).from(adComputers) .where(eq(adComputers.connectionId, connectionId)); const groupsCount = await db.select({ count: count() }).from(adGroups) .where(eq(adGroups.connectionId, connectionId)); const sitesCount = await db.select({ count: count() }).from(adSites) .where(eq(adSites.connectionId, connectionId)); const domainsCount = await db.select({ count: count() }).from(adDomains) .where(eq(adDomains.connectionId, connectionId)); // Return the counts res.json({ userCount: Number(usersCount[0]?.count || 0), computerCount: Number(computersCount[0]?.count || 0), groupCount: Number(groupsCount[0]?.count || 0), sitesCount: Number(sitesCount[0]?.count || 0), domainsCount: Number(domainsCount[0]?.count || 0) }); } catch (error) { next(error); } }); const httpServer = createServer(app); return httpServer; }