import type { Express, Request, Response, NextFunction } from "express"; import { createServer, type Server } from "http"; import { setupAuth } from "./auth"; import { setupSwagger } from "./swagger"; import { storage } from "./storage"; import { apiQuerySchema, PERMISSIONS } from "@shared/schema"; import { ZodError } from "zod"; import { requireAuth, requirePermission, requireAdmin, initializeRBAC } from "./authorization"; export async function registerRoutes(app: Express): Promise { // Setup authentication const { authenticateApiToken } = setupAuth(app); // Setup Swagger documentation setupSwagger(app); // Initialize Role Based Access Control system await initializeRBAC(); // Error handler for Zod validation errors const handleZodError = (err: ZodError, res: Response) => { return res.status(400).json({ message: "Validation error", errors: err.errors, }); }; // Parse query parameters const parseQueryParams = (req: Request) => { try { return apiQuerySchema.parse(req.query); } catch (err) { if (err instanceof ZodError) { console.error("Query parameter validation error:", err.errors); return undefined; } throw err; } }; /** * @swagger * /api/ldap-connections: * get: * summary: List all LDAP connections * tags: [LDAP Connections] * security: * - cookieAuth: [] * - bearerAuth: [] * responses: * 200: * description: A list of LDAP connections * content: * application/json: * schema: * type: array * items: * $ref: '#/components/schemas/LdapConnection' * 401: * $ref: '#/components/responses/UnauthorizedError' * 403: * $ref: '#/components/responses/ForbiddenError' */ app.get("/api/ldap-connections", requirePermission(PERMISSIONS.VIEW_LDAP_CONNECTIONS, { allowApiToken: true }), async (req, res, next) => { try { const connections = await storage.listLdapConnections(); // Hide sensitive fields like password const safeConnections = connections.map(conn => { const { password, ...safeConn } = conn; return safeConn; }); res.json(safeConnections); } catch (error) { next(error); } }); /** * @swagger * /api/ldap-connections: * post: * summary: Create a new LDAP connection * tags: [LDAP Connections] * security: * - cookieAuth: [] * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - name * - server * - domain * - username * - password * properties: * name: * type: string * server: * type: string * domain: * type: string * port: * type: integer * default: 389 * useSSL: * type: boolean * default: true * username: * type: string * password: * type: string * responses: * 201: * description: Connection created successfully * content: * application/json: * schema: * $ref: '#/components/schemas/LdapConnection' * 400: * $ref: '#/components/responses/BadRequestError' * 401: * $ref: '#/components/responses/UnauthorizedError' */ app.post("/api/ldap-connections", requireAdmin, async (req, res, next) => { try { const connection = await storage.createLdapConnection(req.body); // Hide password in response const { password, ...safeConn } = connection; res.status(201).json(safeConn); } catch (error) { next(error); } }); /** * @swagger * /api/ldap-connections/{id}: * get: * summary: Get a specific LDAP connection * tags: [LDAP Connections] * security: * - cookieAuth: [] * parameters: * - name: id * in: path * required: true * schema: * type: integer * responses: * 200: * description: LDAP connection details * content: * application/json: * schema: * $ref: '#/components/schemas/LdapConnection' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.get("/api/ldap-connections/:id", async (req, res, next) => { try { if (!req.isAuthenticated()) { return res.status(401).json({ message: "Not authenticated" }); } const connection = await storage.getLdapConnection(parseInt(req.params.id)); if (!connection) { return res.status(404).json({ message: "LDAP connection not found" }); } // Hide password in response const { password, ...safeConn } = connection; res.json(safeConn); } catch (error) { next(error); } }); /** * @swagger * /api/ldap-connections/{id}: * put: * summary: Update a LDAP connection * tags: [LDAP Connections] * security: * - cookieAuth: [] * parameters: * - name: id * in: path * required: true * schema: * type: integer * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * name: * type: string * server: * type: string * domain: * type: string * port: * type: integer * useSSL: * type: boolean * username: * type: string * password: * type: string * responses: * 200: * description: Connection updated successfully * content: * application/json: * schema: * $ref: '#/components/schemas/LdapConnection' * 400: * $ref: '#/components/responses/BadRequestError' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.put("/api/ldap-connections/:id", requireAdmin, async (req, res, next) => { try { const updatedConnection = await storage.updateLdapConnection(parseInt(req.params.id), req.body); if (!updatedConnection) { return res.status(404).json({ message: "LDAP connection not found" }); } // Hide password in response const { password, ...safeConn } = updatedConnection; res.json(safeConn); } catch (error) { next(error); } }); /** * @swagger * /api/ldap-connections/{id}: * delete: * summary: Delete a LDAP connection * tags: [LDAP Connections] * security: * - cookieAuth: [] * parameters: * - name: id * in: path * required: true * schema: * type: integer * responses: * 200: * description: Connection deleted successfully * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.delete("/api/ldap-connections/:id", requireAdmin, async (req, res, next) => { try { const deleted = await storage.deleteLdapConnection(parseInt(req.params.id)); if (!deleted) { return res.status(404).json({ message: "LDAP connection not found" }); } res.json({ success: true }); } catch (error) { next(error); } }); /** * @swagger * /api/tokens: * get: * summary: List all API tokens for current user * tags: [API Tokens] * security: * - cookieAuth: [] * responses: * 200: * description: A list of API tokens * content: * application/json: * schema: * type: array * items: * $ref: '#/components/schemas/ApiToken' * 401: * $ref: '#/components/responses/UnauthorizedError' */ app.get("/api/tokens", async (req, res, next) => { try { if (!req.isAuthenticated()) { return res.status(401).json({ message: "Not authenticated" }); } const tokens = await storage.listApiTokensByUserId(req.user.id); res.json(tokens); } catch (error) { next(error); } }); /** * @swagger * /api/tokens/{id}: * delete: * summary: Delete an API token * tags: [API Tokens] * security: * - cookieAuth: [] * parameters: * - name: id * in: path * required: true * schema: * type: integer * responses: * 200: * description: Token deleted successfully * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.delete("/api/tokens/:id", async (req, res, next) => { try { if (!req.isAuthenticated()) { return res.status(401).json({ message: "Not authenticated" }); } const token = await storage.getApiToken(parseInt(req.params.id)); if (!token) { return res.status(404).json({ message: "Token not found" }); } // Only allow users to delete their own tokens unless they're admin if (token.userId !== req.user.id) { // Get the user's role const userRole = await storage.getRole(req.user.roleId!); if (userRole?.name !== "admin") { return res.status(403).json({ message: "Forbidden: You cannot delete tokens that don't belong to you" }); } } const deleted = await storage.deleteApiToken(parseInt(req.params.id)); if (!deleted) { return res.status(404).json({ message: "Token not found" }); } res.json({ success: true }); } catch (error) { next(error); } }); /** * @swagger * /api/users: * get: * summary: List all users (admin only) * tags: [Users] * security: * - cookieAuth: [] * responses: * 200: * description: A list of users * content: * application/json: * schema: * type: array * items: * $ref: '#/components/schemas/User' * 401: * $ref: '#/components/responses/UnauthorizedError' * 403: * description: Forbidden - admin access required */ app.get("/api/users", requireAdmin, async (req, res, next) => { try { const users = await storage.listUsers(); // Remove passwords from response const safeUsers = users.map(user => { const { password, ...safeUser } = user; return safeUser; }); res.json(safeUsers); } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/ad-users: * get: * summary: List AD users from the specified LDAP connection * tags: [AD Users] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * - $ref: '#/components/parameters/filterParam' * - $ref: '#/components/parameters/selectParam' * - $ref: '#/components/parameters/expandParam' * - $ref: '#/components/parameters/orderByParam' * - $ref: '#/components/parameters/topParam' * - $ref: '#/components/parameters/skipParam' * responses: * 200: * description: A list of AD users * content: * application/json: * schema: * type: array * items: * $ref: '#/components/schemas/AdUser' * 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); res.json(users); } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/ad-users: * post: * summary: Create a new AD user * tags: [AD Users] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * requestBody: * required: true * content: * application/json: * schema: * type: object * required: * - distinguishedName * - sAMAccountName * properties: * distinguishedName: * type: string * sAMAccountName: * type: string * userPrincipalName: * type: string * givenName: * type: string * surname: * type: string * displayName: * type: string * email: * type: string * enabled: * type: boolean * adProperties: * type: object * responses: * 201: * description: User created successfully * content: * application/json: * schema: * $ref: '#/components/schemas/AdUser' * 400: * $ref: '#/components/responses/BadRequestError' * 401: * $ref: '#/components/responses/UnauthorizedError' */ app.post("/api/connections/:connectionId/ad-users", authenticateApiToken, async (req, res, next) => { try { const connectionId = parseInt(req.params.connectionId); const connection = await storage.getLdapConnection(connectionId); if (!connection) { return res.status(404).json({ message: "LDAP connection not found" }); } const userData = { ...req.body, connectionId }; const user = await storage.createAdUser(userData); res.status(201).json(user); } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/ad-users/{id}: * get: * summary: Get a specific AD user * tags: [AD Users] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * - name: id * in: path * required: true * schema: * type: integer * - $ref: '#/components/parameters/selectParam' * responses: * 200: * description: AD user details * content: * application/json: * schema: * $ref: '#/components/schemas/AdUser' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ app.get("/api/connections/:connectionId/ad-users/:id", async (req, res, next) => { try { if (!req.isAuthenticated() && !req.headers.authorization) { return res.status(401).json({ message: "Authentication required" }); } const user = await storage.getAdUser(parseInt(req.params.id)); if (!user || user.connectionId !== parseInt(req.params.connectionId)) { return res.status(404).json({ message: "AD user not found" }); } // Apply property selection if specified let result = user; if (req.query.select) { const properties = (req.query.select as string).split(','); const selectedUser: any = { id: user.id }; properties.forEach(prop => { if ((user as any)[prop] !== undefined) { selectedUser[prop] = (user as any)[prop]; } }); result = selectedUser as typeof user; } res.json(result); } catch (error) { next(error); } }); /** * @swagger * /api/connections/{connectionId}/ad-users/{id}: * put: * summary: Update an AD user * tags: [AD Users] * security: * - bearerAuth: [] * - cookieAuth: [] * parameters: * - name: connectionId * in: path * required: true * schema: * type: integer * - name: id * in: path * required: true * schema: * type: integer * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * distinguishedName: * type: string * sAMAccountName: * type: string * userPrincipalName: * type: string * givenName: * type: string * surname: * type: string * displayName: * type: string * email: * type: string * enabled: * type: boolean * adProperties: * type: object * responses: * 200: * description: User updated successfully * content: * application/json: * schema: * $ref: '#/components/schemas/AdUser' * 400: * $ref: '#/components/responses/BadRequestError' * 401: * $ref: '#/components/responses/UnauthorizedError' * 404: * $ref: '#/components/responses/NotFoundError' */ 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}: * 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); } }); // Similar endpoints for AD Groups 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); res.json(groups); } catch (error) { next(error); } }); // Organizational Units endpoints 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); res.json(orgUnits); } catch (error) { next(error); } }); // Computers endpoints 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); res.json(computers); } catch (error) { next(error); } }); // Domains endpoints 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); res.json(domains); } catch (error) { next(error); } }); const httpServer = createServer(app); return httpServer; }