Files
ActiveDirectoryManager/server/routes.ts
T
alphaeusmote 2204090d78 Improve LDAP connection handling and add permission checks.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 705f2157-ef97-4fbd-89e4-8c7f2ecaea90
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7ed01c5f-a82d-405a-b728-b2e3d127c60c/9f1689f9-354e-48da-9155-329f0295a865.jpg
2025-04-11 14:49:09 +00:00

5934 lines
188 KiB
TypeScript

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,
checkPermission
} 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";
// Helper function to connect to LDAP server
async function connectToLdap(connection: any) {
if (!ldapClient.isConnectionActive(connection.id)) {
await ldapClient.connect(connection);
}
return ldapClient.getClient(connection.id);
}
// 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<Server> {
// Setup authentication
const { authenticateApiToken } = setupAuth(app);
// Setup Swagger documentation
setupSwagger(app);
// Initialize Role Based Access Control system
await initializeRBAC();
// Authentication info and providers
app.get("/api/auth/providers", (req, res) => {
const ldapEnabled = process.env.LDAP_ENABLED === "true";
const oidcEnabled = process.env.OIDC_ENABLED === "true";
res.json({
ldap: {
enabled: ldapEnabled,
connectionName: ldapEnabled ? process.env.LDAP_CONNECTION_NAME || "LDAP Authentication" : null
},
oidc: {
enabled: oidcEnabled,
providerName: oidcEnabled ? process.env.OIDC_PROVIDER_NAME || "Single Sign-On" : null
},
localEnabled: process.env.DISABLE_LOCAL_AUTH !== "true",
registrationEnabled: process.env.DISABLE_REGISTRATION !== "true"
});
});
// 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);
}
});
// Removed from Swagger documentation - API Tokens endpoint
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);
}
});
// Removed from Swagger documentation - API Tokens endpoint
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);
}
});
// Removed from Swagger documentation - non-AD user endpoint
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 {
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" });
}
// Extract user data from request
const { name, samAccountName, description, email, firstName, lastName, ou, enabled = true, password } = req.body;
if (!name || !samAccountName || !ou) {
return res.status(400).json({ message: "Name, samAccountName, and OU are required" });
}
// Create user 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 DN for the new user
const userDN = `CN=${name},${ou}`;
// Attributes for the new user
const userAttributes = {
objectClass: ['user', 'person', 'organizationalPerson', 'top'],
cn: name,
sAMAccountName: samAccountName,
givenName: firstName || '',
sn: lastName || '',
displayName: name,
description: description || '',
mail: email || '',
userAccountControl: enabled ? '512' : '514' // 512 = enabled, 514 = disabled
};
// Add password if provided
if (password) {
// Unicode password format for Active Directory
const unicodePassword = Buffer.from(`"${password}"`, 'utf16le');
userAttributes.unicodePwd = unicodePassword;
}
try {
const success = await ldapClient.createEntry(connectionId, userDN, userAttributes);
if (!success) {
return res.status(500).json({ message: "Failed to create AD user" });
}
// Search for the user to get all its attributes
const userResults = await ldapClient.searchUsers(connectionId, `(cn=${name})`);
if (!userResults || userResults.length === 0) {
return res.status(500).json({ message: "User created but could not retrieve details" });
}
const userData = userResults[0];
// Create entry in our database
const newUser = await storage.createAdUser({
connectionId,
name,
objectGUID: userData.objectGUID || uuidv4(),
objectType: 'user',
dn: userDN,
canonicalName: userData.canonicalName || "",
samAccountName,
firstName: firstName || null,
lastName: lastName || null,
email: email || null,
description: description || null,
enabled,
lastLogon: null,
created: new Date(),
managedBy: null,
adProperties: userData
});
// Create audit log entry
await storage.createAuditLogEntry({
action: 'create',
targetId: newUser.id.toString(),
details: { type: 'user', name, dn: userDN },
userId: req.user?.id,
connectionId
});
res.status(201).json(newUser);
} catch (err) {
console.error("Error creating AD user:", err);
return res.status(500).json({ message: `Error creating AD user: ${err.message}` });
}
} 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'
*/
/**
* @swagger
* /api/connections/{connectionId}/ad-ous:
* post:
* summary: Create a new organizational unit
* tags: [AD OUs]
* security:
* - bearerAuth: []
* - cookieAuth: []
* parameters:
* - name: connectionId
* in: path
* required: true
* schema:
* type: integer
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required:
* - name
* - parentDN
* properties:
* name:
* type: string
* description: Name of the organizational unit
* parentDN:
* type: string
* description: Distinguished name of the parent container
* description:
* type: string
* description: Description of the organizational unit
* responses:
* 201:
* description: Organizational unit created successfully
* 400:
* $ref: '#/components/responses/BadRequestError'
* 401:
* $ref: '#/components/responses/UnauthorizedError'
*/
app.post("/api/connections/:connectionId/ad-ous", authenticateApiToken, requirePermission(PERMISSIONS.CREATE_AD_OUS), async (req, res, next) => {
try {
const connectionId = parseInt(req.params.connectionId);
const { name, parentDN, description } = req.body;
// Basic validation
if (!name || !parentDN) {
return res.status(400).json({ message: "OU 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 OU
const ouDN = `OU=${name},${parentDN}`;
// Attributes for the new organizational unit
const ouAttributes = {
objectClass: ['top', 'organizationalUnit'],
ou: name,
description: description || ''
};
// Create the OU in AD
await ldapClient.createEntry(connectionId, ouDN, ouAttributes);
// Search for the newly created OU to get its attributes
const searchResults = await ldapClient.searchOUs(connectionId, `(&(objectClass=organizationalUnit)(ou=${name}))`, [
'objectGUID', 'distinguishedName', 'canonicalName', 'ou', 'description', 'name'
]);
if (!searchResults || searchResults.length === 0) {
return res.status(500).json({ message: "OU created but could not retrieve details" });
}
const adOU = searchResults[0];
// Store in database
const storedOU = await storage.createAdOrgUnit({
connectionId,
objectGUID: adOU.objectGUID,
distinguishedName: adOU.distinguishedName,
canonicalName: adOU.canonicalName,
name: adOU.ou || adOU.name || name,
description: adOU.description,
adProperties: adOU // Store all attributes
});
// Add audit log
await storage.createAuditLogEntry({
userId: req.user.id,
connectionId,
action: 'create',
targetType: 'ou',
targetId: storedOU.id.toString(),
details: {
distinguishedName: ouDN,
name: name,
description: description
}
});
res.status(201).json(storedOU);
} catch (err) {
console.error("Error creating AD organizational unit:", err);
if (err.message && err.message.includes('Entry Already Exists')) {
return res.status(409).json({ message: "Organizational unit already exists" });
}
next(err);
}
});
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'
*/
/**
* @swagger
* /api/connections/{connectionId}/ad-domains:
* post:
* summary: Create a new Active Directory domain
* tags: [AD Domains]
* 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
* - distinguishedName
* properties:
* name:
* type: string
* description: The name of the domain
* distinguishedName:
* type: string
* description: The distinguished name of the domain
* netBIOSName:
* type: string
* description: The NetBIOS name of the domain
* forestName:
* type: string
* description: The forest name for the domain
* responses:
* 201:
* description: Domain created successfully
* 400:
* description: Invalid input
* 401:
* description: Unauthorized
* 403:
* description: Forbidden
* 500:
* description: Server error
*/
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}/ad-domains/{objectGUID}:
* patch:
* summary: Update an Active Directory domain
* tags: [AD Domains]
* security:
* - cookieAuth: []
* - bearerAuth: []
* parameters:
* - in: path
* name: connectionId
* required: true
* schema:
* type: integer
* description: LDAP connection ID
* - in: path
* name: objectGUID
* required: true
* schema:
* type: string
* description: Object GUID of the domain to update
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* description:
* type: string
* description: Description for the domain
* netBIOSName:
* type: string
* description: NetBIOS name for the domain
* responses:
* 200:
* description: Domain updated successfully
* 400:
* description: Invalid input
* 401:
* description: Unauthorized
* 403:
* description: Forbidden
* 404:
* description: Domain not found
* 500:
* description: Server error
*/
/**
* @swagger
* /api/connections/{connectionId}/ad-domains/{objectGUID}:
* delete:
* summary: Delete an Active Directory domain
* tags: [AD Domains]
* security:
* - cookieAuth: []
* - bearerAuth: []
* parameters:
* - in: path
* name: connectionId
* required: true
* schema:
* type: integer
* description: LDAP connection ID
* - in: path
* name: objectGUID
* required: true
* schema:
* type: string
* description: Object GUID of the domain to delete
* responses:
* 200:
* description: Domain deleted successfully
* 401:
* description: Unauthorized
* 403:
* description: Forbidden
* 404:
* description: Domain not found
* 500:
* description: Server error
*/
// Domain PATCH endpoint
app.patch("/api/connections/:connectionId/ad-domains/:objectGUID", authenticateApiToken, async (req, res, next) => {
try {
// Check permissions
const hasPermission = await checkPermission(req, PERMISSIONS.UPDATE_AD_DOMAINS);
if (!hasPermission) {
return res.status(403).json({ message: "Not authorized to update domains" });
}
const connectionId = parseInt(req.params.connectionId);
const objectGUID = req.params.objectGUID;
// Verify the connection exists
const connection = await storage.getLdapConnection(connectionId);
if (!connection) {
return res.status(404).json({ message: "LDAP connection not found" });
}
// Get domain to update
const domain = await storage.getAdDomain(connectionId, objectGUID);
if (!domain) {
return res.status(404).json({ message: "Domain not found" });
}
try {
// Connect to LDAP server
const ldapClient = await connectToLdap(connection);
// Create modification object based on the request body
const changes = {};
if (req.body.description !== undefined) {
changes.description = req.body.description;
}
if (req.body.netBIOSName !== undefined) {
changes.netBIOSName = req.body.netBIOSName;
}
// Skip modification if no changes
if (Object.keys(changes).length === 0) {
return res.status(400).json({ message: "No valid attributes provided for update" });
}
// Modify domain in LDAP
await ldapClient.modify(domain.distinguishedName, changes);
// Update domain in database
const updatedDomain = await storage.updateAdDomain(connectionId, objectGUID, {
...req.body,
adProperties: { ...domain.adProperties, ...changes }
});
// Add audit log
await storage.createAuditLogEntry({
action: "update_domain",
targetId: objectGUID,
userId: req.user?.id,
connectionId: connectionId,
details: {
domain: domain.name,
distinguishedName: domain.distinguishedName,
changes: changes
}
});
res.json({
...updatedDomain,
objectType: 'domain'
});
} catch (err) {
console.error("Error updating domain:", err);
return res.status(500).json({ message: `Error updating domain: ${err.message}` });
}
} catch (error) {
next(error);
}
});
// Domain DELETE endpoint
app.delete("/api/connections/:connectionId/ad-domains/:objectGUID", authenticateApiToken, async (req, res, next) => {
try {
// Check permissions
const hasPermission = await checkPermission(req, PERMISSIONS.DELETE_AD_DOMAINS);
if (!hasPermission) {
return res.status(403).json({ message: "Not authorized to delete domains" });
}
const connectionId = parseInt(req.params.connectionId);
const objectGUID = req.params.objectGUID;
// Verify the connection exists
const connection = await storage.getLdapConnection(connectionId);
if (!connection) {
return res.status(404).json({ message: "LDAP connection not found" });
}
// Get domain to delete
const domain = await storage.getAdDomain(connectionId, objectGUID);
if (!domain) {
return res.status(404).json({ message: "Domain not found" });
}
try {
// Connect to LDAP server
const ldapClient = await connectToLdap(connection);
// First check if there are any children under this domain
const searchResults = await ldapClient.search(domain.distinguishedName, {
scope: 'one',
filter: '(objectClass=*)'
});
if (searchResults.searchEntries && searchResults.searchEntries.length > 0) {
return res.status(409).json({
message: "Cannot delete domain with child objects. Remove all child objects first."
});
}
// Delete domain from LDAP
await ldapClient.del(domain.distinguishedName);
// Delete domain from database
await storage.deleteAdDomain(connectionId, objectGUID);
// Add audit log
await storage.createAuditLogEntry({
action: "delete_domain",
targetId: objectGUID,
userId: req.user?.id,
connectionId: connectionId,
details: {
domain: domain.name,
distinguishedName: domain.distinguishedName
}
});
res.json({ success: true });
} catch (err) {
console.error("Error deleting domain:", err);
return res.status(500).json({ message: `Error deleting domain: ${err.message}` });
}
} catch (error) {
next(error);
}
});
app.post("/api/connections/:connectionId/ad-domains", authenticateApiToken, async (req, res, next) => {
try {
// Check permissions
const hasPermission = await checkPermission(req, PERMISSIONS.CREATE_AD_DOMAINS);
if (!hasPermission) {
return res.status(403).json({ message: "Not authorized to create domains" });
}
const connectionId = parseInt(req.params.connectionId);
const connection = await storage.getLdapConnection(connectionId);
if (!connection) {
return res.status(404).json({ message: "LDAP connection not found" });
}
// Validate required fields
if (!req.body.name || !req.body.distinguishedName) {
return res.status(400).json({ message: "Name and distinguishedName are required" });
}
try {
// Connect to LDAP server
const ldapClient = await connectToLdap(connection);
// Create domain attributes
const domainAttributes = {
objectClass: ['domain'],
cn: req.body.name,
description: req.body.description || `Domain ${req.body.name}`
};
if (req.body.netBIOSName) {
domainAttributes.netBIOSName = req.body.netBIOSName;
}
// Add domain to LDAP
await ldapClient.add(req.body.distinguishedName, domainAttributes);
// Get the created domain's GUID and details
const searchResults = await ldapClient.search(req.body.distinguishedName, {
scope: 'base',
attributes: ['objectGUID', 'distinguishedName', 'cn', 'description', 'name', 'netBIOSName']
});
if (!searchResults.searchEntries || searchResults.searchEntries.length === 0) {
return res.status(500).json({ message: "Domain was created but could not be retrieved" });
}
const domainEntry = searchResults.searchEntries[0];
// Format objectGUID
const objectGUID = Buffer.from(domainEntry.objectGUID).toString('hex');
// Create domain record in database
const domainData = {
name: req.body.name,
connectionId: connectionId,
objectGUID: objectGUID,
distinguishedName: req.body.distinguishedName,
cn: domainEntry.cn,
netBIOSName: req.body.netBIOSName,
forestName: req.body.forestName,
adProperties: domainEntry
};
const createdDomain = await storage.createAdDomain(domainData);
// Add audit log
await storage.createAuditLogEntry({
action: "create_domain",
targetId: objectGUID,
userId: req.user?.id,
connectionId: connectionId,
details: {
domain: req.body.name,
distinguishedName: req.body.distinguishedName
}
});
res.status(201).json({
...createdDomain,
objectType: 'domain'
});
} catch (err) {
console.error("Error creating domain:", err);
// Check if the error is because the domain already exists
if (err.message && err.message.includes('entryAlreadyExists')) {
return res.status(409).json({ message: "Domain already exists" });
}
return res.status(500).json({ message: `Error creating domain: ${err.message}` });
}
} 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<AdSite> = {};
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<AdSubnet> = {};
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);
}
});
// Dashboard endpoints removed from Swagger API as requested
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);
}
});
// Dashboard endpoints removed from Swagger API as requested
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);
}
});
// Removed from Swagger documentation - Dashboard endpoint
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);
}
});
// Removed from Swagger documentation - Dashboard endpoint
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);
}
});
// Removed from Swagger documentation - Dashboard endpoint
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);
}
});
// Removed from Swagger documentation - Dashboard endpoint
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);
}
});
// Removed from Swagger documentation - Dashboard endpoint
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);
}
});
// Removed from Swagger documentation - Dashboard endpoint
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);
}
});
// Dynamic Group Rules routes
app.get("/api/dynamic-group-rules", requirePermission(PERMISSIONS.VIEW_DYNAMIC_GROUPS, { allowApiToken: true }), async (req, res) => {
try {
const rules = await storage.listDynamicGroupRules();
res.json(rules);
} catch (error) {
console.error('Error fetching dynamic group rules:', error);
res.status(500).json({ error: 'Failed to fetch dynamic group rules' });
}
});
app.get("/api/dynamic-group-rules/:id", requirePermission(PERMISSIONS.VIEW_DYNAMIC_GROUPS, { allowApiToken: true }), async (req, res) => {
try {
const id = Number(req.params.id);
const rule = await storage.getDynamicGroupRule(id);
if (!rule) {
return res.status(404).json({ error: 'Dynamic group rule not found' });
}
res.json(rule);
} catch (error) {
console.error('Error fetching dynamic group rule:', error);
res.status(500).json({ error: 'Failed to fetch dynamic group rule' });
}
});
app.post("/api/dynamic-group-rules", requirePermission(PERMISSIONS.MANAGE_DYNAMIC_GROUPS, { allowApiToken: true }), async (req, res) => {
try {
const rule = await storage.createDynamicGroupRule(req.body);
res.status(201).json(rule);
} catch (error) {
console.error('Error creating dynamic group rule:', error);
res.status(500).json({ error: 'Failed to create dynamic group rule' });
}
});
app.put("/api/dynamic-group-rules/:id", requirePermission(PERMISSIONS.MANAGE_DYNAMIC_GROUPS, { allowApiToken: true }), async (req, res) => {
try {
const id = Number(req.params.id);
const updatedRule = await storage.updateDynamicGroupRule(id, req.body);
if (!updatedRule) {
return res.status(404).json({ error: 'Dynamic group rule not found' });
}
res.json(updatedRule);
} catch (error) {
console.error('Error updating dynamic group rule:', error);
res.status(500).json({ error: 'Failed to update dynamic group rule' });
}
});
app.delete("/api/dynamic-group-rules/:id", requirePermission(PERMISSIONS.MANAGE_DYNAMIC_GROUPS, { allowApiToken: true }), async (req, res) => {
try {
const id = Number(req.params.id);
const deleted = await storage.deleteDynamicGroupRule(id);
if (!deleted) {
return res.status(404).json({ error: 'Dynamic group rule not found' });
}
res.status(204).send();
} catch (error) {
console.error('Error deleting dynamic group rule:', error);
res.status(500).json({ error: 'Failed to delete dynamic group rule' });
}
});
// Dynamic Group Conditions routes
app.get("/api/dynamic-group-rules/:ruleId/conditions", requirePermission(PERMISSIONS.VIEW_DYNAMIC_GROUPS, { allowApiToken: true }), async (req, res) => {
try {
const ruleId = Number(req.params.ruleId);
const conditions = await storage.getConditionsByRuleId(ruleId);
res.json(conditions);
} catch (error) {
console.error('Error fetching conditions for dynamic group rule:', error);
res.status(500).json({ error: 'Failed to fetch conditions' });
}
});
app.post("/api/dynamic-group-rules/:ruleId/conditions", requirePermission(PERMISSIONS.MANAGE_DYNAMIC_GROUPS, { allowApiToken: true }), async (req, res) => {
try {
const ruleId = Number(req.params.ruleId);
const conditionData = { ...req.body, ruleId };
const condition = await storage.createDynamicGroupCondition(conditionData);
res.status(201).json(condition);
} catch (error) {
console.error('Error creating condition for dynamic group rule:', error);
res.status(500).json({ error: 'Failed to create condition' });
}
});
// Dynamic Group Rule Connections routes
app.get("/api/dynamic-group-rules/:ruleId/connections", requirePermission(PERMISSIONS.VIEW_DYNAMIC_GROUPS, { allowApiToken: true }), async (req, res) => {
try {
const ruleId = Number(req.params.ruleId);
const connections = await storage.getRuleConnectionsByRuleId(ruleId);
res.json(connections);
} catch (error) {
console.error('Error fetching connections for dynamic group rule:', error);
res.status(500).json({ error: 'Failed to fetch connections' });
}
});
app.post("/api/dynamic-group-rules/:ruleId/connections", requirePermission(PERMISSIONS.MANAGE_DYNAMIC_GROUPS, { allowApiToken: true }), async (req, res) => {
try {
const ruleId = Number(req.params.ruleId);
const connectionData = { ...req.body, ruleId };
const connection = await storage.createDynamicGroupRuleConnection(connectionData);
res.status(201).json(connection);
} catch (error) {
console.error('Error creating connection for dynamic group rule:', error);
res.status(500).json({ error: 'Failed to create connection' });
}
});
// Schedule Rules routes
app.get("/api/schedule-rules", requirePermission(PERMISSIONS.VIEW_DYNAMIC_GROUPS, { allowApiToken: true }), async (req, res) => {
try {
const scheduleRules = await storage.getAllScheduleRules();
res.json(scheduleRules);
} catch (error) {
console.error('Error fetching schedule rules:', error);
res.status(500).json({ error: 'Failed to fetch schedule rules' });
}
});
app.get("/api/schedule-rules/:id", requirePermission(PERMISSIONS.VIEW_DYNAMIC_GROUPS, { allowApiToken: true }), async (req, res) => {
try {
const id = Number(req.params.id);
const scheduleRule = await storage.getScheduleRule(id);
if (!scheduleRule) {
return res.status(404).json({ error: 'Schedule rule not found' });
}
res.json(scheduleRule);
} catch (error) {
console.error('Error fetching schedule rule:', error);
res.status(500).json({ error: 'Failed to fetch schedule rule' });
}
});
app.get("/api/dynamic-group-rules/:ruleId/schedules", requirePermission(PERMISSIONS.VIEW_DYNAMIC_GROUPS, { allowApiToken: true }), async (req, res) => {
try {
const ruleId = Number(req.params.ruleId);
const schedules = await storage.getScheduleRulesByRuleId(ruleId);
res.json(schedules);
} catch (error) {
console.error('Error fetching schedules for dynamic group rule:', error);
res.status(500).json({ error: 'Failed to fetch schedules' });
}
});
app.post("/api/schedule-rules", requirePermission(PERMISSIONS.MANAGE_DYNAMIC_GROUPS, { allowApiToken: true }), async (req, res) => {
try {
const scheduleRule = await storage.createScheduleRule(req.body);
res.status(201).json(scheduleRule);
} catch (error) {
console.error('Error creating schedule rule:', error);
res.status(500).json({ error: 'Failed to create schedule rule' });
}
});
app.post("/api/dynamic-group-rules/:ruleId/schedules", requirePermission(PERMISSIONS.MANAGE_DYNAMIC_GROUPS, { allowApiToken: true }), async (req, res) => {
try {
const ruleId = Number(req.params.ruleId);
const scheduleData = { ...req.body, ruleId };
const schedule = await storage.createScheduleRule(scheduleData);
res.status(201).json(schedule);
} catch (error) {
console.error('Error creating schedule for dynamic group rule:', error);
res.status(500).json({ error: 'Failed to create schedule' });
}
});
app.put("/api/schedule-rules/:id", requirePermission(PERMISSIONS.MANAGE_DYNAMIC_GROUPS, { allowApiToken: true }), async (req, res) => {
try {
const id = Number(req.params.id);
const updatedRule = await storage.updateScheduleRule(id, req.body);
if (!updatedRule) {
return res.status(404).json({ error: 'Schedule rule not found' });
}
res.json(updatedRule);
} catch (error) {
console.error('Error updating schedule rule:', error);
res.status(500).json({ error: 'Failed to update schedule rule' });
}
});
app.delete("/api/schedule-rules/:id", requirePermission(PERMISSIONS.MANAGE_DYNAMIC_GROUPS, { allowApiToken: true }), async (req, res) => {
try {
const id = Number(req.params.id);
const deleted = await storage.deleteScheduleRule(id);
if (!deleted) {
return res.status(404).json({ error: 'Schedule rule not found' });
}
res.status(204).send();
} catch (error) {
console.error('Error deleting schedule rule:', error);
res.status(500).json({ error: 'Failed to delete schedule rule' });
}
});
const httpServer = createServer(app);
return httpServer;
}