diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml
deleted file mode 100644
index ed063cd..0000000
--- a/.github/workflows/docker-publish.yml
+++ /dev/null
@@ -1,69 +0,0 @@
-name: Docker
-
-on:
- push:
- branches: [ "main" ]
- tags: [ 'v*.*.*' ]
- pull_request:
- branches: [ "main" ]
-
-env:
- # Use docker.io for Docker Hub if empty
- REGISTRY: docker.io
- # github.repository as /
- IMAGE_NAME: ${{ github.repository }}
-
-jobs:
- build:
- runs-on: ubuntu-latest
- permissions:
- contents: read
- packages: write
- # This is needed for token authentication
- id-token: write
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v3
-
- # Workaround: https://github.com/docker/build-push-action/issues/461
- - name: Setup Docker buildx
- uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf
-
- # Login against a Docker registry
- # https://github.com/docker/login-action
- - name: Log into registry ${{ env.REGISTRY }}
- if: github.event_name != 'pull_request'
- uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
- with:
- registry: ${{ env.REGISTRY }}
- username: ${{ secrets.DOCKERHUB_USERNAME }}
- password: ${{ secrets.DOCKERHUB_TOKEN }}
-
- # Extract metadata (tags, labels) for Docker
- # https://github.com/docker/metadata-action
- - name: Extract Docker metadata
- id: meta
- uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
- with:
- images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- tags: |
- type=ref,event=branch
- type=ref,event=pr
- type=semver,pattern={{version}}
- type=semver,pattern={{major}}.{{minor}}
- type=semver,pattern={{major}}
- type=sha
-
- # Build and push Docker image with Buildx
- # https://github.com/docker/build-push-action
- - name: Build and push Docker image
- id: build-and-push
- uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a
- with:
- context: .
- push: ${{ github.event_name != 'pull_request' }}
- tags: ${{ steps.meta.outputs.tags }}
- labels: ${{ steps.meta.outputs.labels }}
- cache-from: type=gha
- cache-to: type=gha,mode=max
\ No newline at end of file
diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md
index 4fea112..0053f20 100644
--- a/DEPLOYMENT.md
+++ b/DEPLOYMENT.md
@@ -53,7 +53,7 @@ This document provides detailed instructions for deploying the Active Directory
```bash
cp .env.example .env
```
-
+
Edit the .env file with your specific configuration:
```bash
nano .env
@@ -62,9 +62,9 @@ This document provides detailed instructions for deploying the Active Directory
3. **SSL Configuration**
For production environments, the application is designed to work behind a reverse proxy or load balancer that handles SSL termination.
-
+
If you need to implement SSL directly in your environment, consider:
-
+
- Using a reverse proxy like Nginx or Traefik in front of the application
- Setting up SSL termination at the infrastructure level (e.g., using AWS Application Load Balancer)
- Configuring your cloud provider's SSL management services
@@ -391,6 +391,8 @@ For deployment on Microsoft Azure:
Detailed Azure deployment instructions can be found in [AZURE_DEPLOYMENT.md](AZURE_DEPLOYMENT.md).
+For comprehensive Kubernetes deployment instructions for any cloud provider, refer to the [Kubernetes Deployment Guide](docs/kubernetes-deployment.md).
+
### Google Cloud Deployment
For deployment on Google Cloud Platform:
@@ -411,6 +413,8 @@ For deployment on Google Cloud Platform:
Detailed GCP deployment instructions can be found in [GCP_DEPLOYMENT.md](GCP_DEPLOYMENT.md).
+For comprehensive Kubernetes deployment instructions for any cloud provider, refer to the [Kubernetes Deployment Guide](docs/kubernetes-deployment.md).
+
## Production Best Practices
### Security Considerations
diff --git a/README.md b/README.md
index dedcb57..e270459 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,9 @@ The Active Directory Management Platform provides the following key features:
### Core Functionality
- **User Management**: Create, read, update, and delete Active Directory users with comprehensive attribute support
+ - Password reset with policy validation options
+ - Account enabling/disabling
+ - Bulk account management operations
- **Group Management**: Manage security and distribution groups, including nested group structures
- **Organizational Unit (OU) Management**: Create and manage the hierarchical OU structure
- **Computer Management**: Manage computer accounts with detailed property configuration
@@ -76,6 +79,8 @@ The Active Directory Management Platform provides the following key features:
### DevOps
- **Docker**: Containerization platform for consistent deployment
+- **Kubernetes**: Container orchestration for scalable deployments
+- **Infrastructure as Code (IaC)**: Deployment configurations for Docker and Kubernetes
## Architecture
@@ -111,8 +116,8 @@ The application follows a modern web architecture with clear separation of conce
3. Create a .env file from the example:
```bash
- cp .env.example .env
- # Edit .env with your configuration
+ cp iac/docker/.env.example iac/docker/.env
+ # Edit iac/docker/.env with your configuration
```
4. Set up the database:
@@ -142,8 +147,8 @@ The application follows a modern web architecture with clear separation of conce
2. Create a .env file from the example:
```bash
- cp .env.example .env
- # Edit .env with your production configuration
+ cp iac/docker/.env.example iac/docker/.env
+ # Edit iac/docker/.env with your production configuration
```
3. Set up environment variables for production:
@@ -155,6 +160,7 @@ The application follows a modern web architecture with clear separation of conce
4. Start the application stack:
```bash
+ cd iac/docker
docker-compose up -d
```
@@ -187,6 +193,10 @@ The application is also available as a pre-built Docker image:
For manual deployment instructions, refer to the [Deployment Guide](DEPLOYMENT.md).
+#### Kubernetes Deployment
+
+For Kubernetes deployment instructions, refer to the [Kubernetes Deployment Guide](docs/kubernetes-deployment.md).
+
## API Documentation
The API is thoroughly documented using the OpenAPI specification (Swagger):
@@ -202,6 +212,10 @@ The API is thoroughly documented using the OpenAPI specification (Swagger):
- **Response Format**: Consistent JSON structure with appropriate status codes
- **Error Handling**: Descriptive error messages and appropriate HTTP status codes
+### API Examples
+
+For examples of using the API, including the password reset and account management endpoints, refer to the [API Examples Guide](docs/api-examples.md).
+
### Filterable Endpoints
All list endpoints support filtering with a flexible query syntax:
diff --git a/docs/api-examples.md b/docs/api-examples.md
new file mode 100644
index 0000000..ecd562f
--- /dev/null
+++ b/docs/api-examples.md
@@ -0,0 +1,319 @@
+# API Examples
+
+This document provides examples of using the Active Directory Management API, including the new password reset and account management endpoints.
+
+## Table of Contents
+
+- [Authentication](#authentication)
+- [User Management](#user-management)
+- [Password Management](#password-management)
+- [Account Status Management](#account-status-management)
+- [Group Management](#group-management)
+- [Bulk Operations](#bulk-operations)
+- [Error Handling](#error-handling)
+
+## Authentication
+
+### Login
+
+```bash
+curl -X POST http://localhost:5000/api/auth/login \
+ -H "Content-Type: application/json" \
+ -d '{
+ "username": "admin",
+ "password": "your-password"
+ }'
+```
+
+Response:
+
+```json
+{
+ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
+ "user": {
+ "id": 1,
+ "username": "admin",
+ "email": "admin@example.com",
+ "fullName": "System Administrator",
+ "role": "admin"
+ }
+}
+```
+
+### Using the Token
+
+For all subsequent requests, include the token in the Authorization header:
+
+```bash
+curl -X GET http://localhost:5000/api/users \
+ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
+```
+
+## User Management
+
+### Get All Users
+
+```bash
+curl -X GET http://localhost:5000/api/connections/1/ad-users \
+ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
+```
+
+### Get User by ObjectGUID
+
+```bash
+curl -X GET http://localhost:5000/api/connections/1/ad-users/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
+ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
+```
+
+### Create User
+
+```bash
+curl -X POST http://localhost:5000/api/connections/1/ad-users \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
+ -d '{
+ "sAMAccountName": "jdoe",
+ "givenName": "John",
+ "sn": "Doe",
+ "displayName": "John Doe",
+ "mail": "jdoe@example.com",
+ "userPrincipalName": "jdoe@example.com",
+ "parentOU": "OU=Users,DC=example,DC=com",
+ "password": "SecurePassword123!",
+ "enabled": true
+ }'
+```
+
+### Update User
+
+```bash
+curl -X PATCH http://localhost:5000/api/connections/1/ad-users/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
+ -d '{
+ "displayName": "John A. Doe",
+ "mail": "john.doe@example.com",
+ "telephoneNumber": "+1 (555) 123-4567"
+ }'
+```
+
+### Delete User
+
+```bash
+curl -X DELETE http://localhost:5000/api/connections/1/ad-users/a1b2c3d4-e5f6-7890-abcd-ef1234567890 \
+ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
+```
+
+## Password Management
+
+### Reset User Password
+
+```bash
+curl -X POST http://localhost:5000/api/connections/1/reset-password \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
+ -d '{
+ "userObjectGUID": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "newPassword": "NewSecurePassword456!",
+ "skipValidation": false,
+ "requirePasswordChangeAtNextLogon": true
+ }'
+```
+
+Response:
+
+```json
+{
+ "success": true,
+ "message": "Password reset successful for user with GUID a1b2c3d4-e5f6-7890-abcd-ef1234567890"
+}
+```
+
+### Reset Password with Skip Validation
+
+For scenarios where you need to bypass password policy validation:
+
+```bash
+curl -X POST http://localhost:5000/api/connections/1/reset-password \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
+ -d '{
+ "userObjectGUID": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "newPassword": "TempPass123",
+ "skipValidation": true,
+ "requirePasswordChangeAtNextLogon": true
+ }'
+```
+
+### Reset Password Without Requiring Change at Next Logon
+
+```bash
+curl -X POST http://localhost:5000/api/connections/1/reset-password \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
+ -d '{
+ "userObjectGUID": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "newPassword": "NewSecurePassword456!",
+ "requirePasswordChangeAtNextLogon": false
+ }'
+```
+
+## Account Status Management
+
+### Enable User Account
+
+```bash
+curl -X POST http://localhost:5000/api/connections/1/enable-user-account \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
+ -d '{
+ "userObjectGUID": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "enabled": true
+ }'
+```
+
+Response:
+
+```json
+{
+ "success": true,
+ "message": "User account enabled successfully for user with GUID a1b2c3d4-e5f6-7890-abcd-ef1234567890"
+}
+```
+
+### Disable User Account
+
+```bash
+curl -X POST http://localhost:5000/api/connections/1/enable-user-account \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
+ -d '{
+ "userObjectGUID": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "enabled": false
+ }'
+```
+
+Response:
+
+```json
+{
+ "success": true,
+ "message": "User account disabled successfully for user with GUID a1b2c3d4-e5f6-7890-abcd-ef1234567890"
+}
+```
+
+### Bulk Enable User Accounts
+
+```bash
+curl -X POST http://localhost:5000/api/connections/1/bulk-enable-user-accounts \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
+ -d '{
+ "userDNs": [
+ "CN=John Doe,OU=Users,DC=example,DC=com",
+ "CN=Jane Smith,OU=Users,DC=example,DC=com",
+ "CN=Bob Johnson,OU=Users,DC=example,DC=com"
+ ],
+ "enabled": true
+ }'
+```
+
+Response:
+
+```json
+{
+ "success": [
+ "CN=John Doe,OU=Users,DC=example,DC=com",
+ "CN=Jane Smith,OU=Users,DC=example,DC=com",
+ "CN=Bob Johnson,OU=Users,DC=example,DC=com"
+ ],
+ "failed": []
+}
+```
+
+### Bulk Disable User Accounts
+
+```bash
+curl -X POST http://localhost:5000/api/connections/1/bulk-enable-user-accounts \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
+ -d '{
+ "userDNs": [
+ "CN=John Doe,OU=Users,DC=example,DC=com",
+ "CN=Jane Smith,OU=Users,DC=example,DC=com",
+ "CN=Bob Johnson,OU=Users,DC=example,DC=com"
+ ],
+ "enabled": false
+ }'
+```
+
+## Group Management
+
+### Add User to Group
+
+```bash
+curl -X POST http://localhost:5000/api/connections/1/add-to-group \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
+ -d '{
+ "objectGUID": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "groupObjectGUID": "g1h2i3j4-k5l6-7890-mnop-qr1234567890",
+ "objectType": "user"
+ }'
+```
+
+### Remove User from Group
+
+```bash
+curl -X POST http://localhost:5000/api/connections/1/remove-from-group \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
+ -d '{
+ "objectGUID": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "groupObjectGUID": "g1h2i3j4-k5l6-7890-mnop-qr1234567890",
+ "objectType": "user"
+ }'
+```
+
+## Error Handling
+
+### Invalid Request
+
+```json
+{
+ "errors": [
+ {
+ "code": "invalid_type",
+ "expected": "string",
+ "received": "undefined",
+ "path": ["userObjectGUID"],
+ "message": "User ObjectGUID is required"
+ }
+ ]
+}
+```
+
+### User Not Found
+
+```json
+{
+ "message": "User not found"
+}
+```
+
+### Permission Denied
+
+```json
+{
+ "message": "You do not have permission to perform this action"
+}
+```
+
+### LDAP Operation Failed
+
+```json
+{
+ "message": "Failed to reset password",
+ "error": "LDAP operation failed: Constraint violation"
+}
+```
diff --git a/docs/deployment.md b/docs/deployment.md
new file mode 100644
index 0000000..eda1c35
--- /dev/null
+++ b/docs/deployment.md
@@ -0,0 +1,316 @@
+# Deployment Guide
+
+This guide provides instructions for deploying the Active Directory Management application using Docker, Docker Compose, and Kubernetes.
+
+## Table of Contents
+
+- [Docker Deployment](#docker-deployment)
+ - [Building the Docker Image](#building-the-docker-image)
+ - [Running with Docker](#running-with-docker)
+ - [Environment Variables](#environment-variables)
+- [Docker Compose Deployment](#docker-compose-deployment)
+ - [Configuration](#configuration)
+ - [Starting the Services](#starting-the-services)
+ - [Stopping the Services](#stopping-the-services)
+- [Kubernetes Deployment](#kubernetes-deployment)
+ - [Prerequisites](#prerequisites)
+ - [Configuration](#kubernetes-configuration)
+ - [Deployment](#deployment)
+ - [Accessing the Application](#accessing-the-application)
+ - [Scaling](#scaling)
+ - [Updating](#updating)
+ - [Monitoring](#monitoring)
+- [Production Considerations](#production-considerations)
+ - [Security](#security)
+ - [Backups](#backups)
+ - [High Availability](#high-availability)
+
+## Docker Deployment
+
+### Building the Docker Image
+
+To build the Docker image using the provided build scripts:
+
+#### Linux/macOS:
+```bash
+# Navigate to the iac/docker directory
+cd iac/docker
+
+# Make the script executable
+chmod +x build.sh
+
+# Build the image with default settings
+./build.sh
+
+# Build with custom tag
+./build.sh --tag v1.0.0
+
+# Build and push to a registry
+./build.sh --registry your-registry.com --tag v1.0.0 --push
+```
+
+#### Windows:
+```powershell
+# Navigate to the iac/docker directory
+cd iac\docker
+
+# Build the image with default settings
+.\build.ps1
+
+# Build with custom tag
+.\build.ps1 -tag v1.0.0
+
+# Build and push to a registry
+.\build.ps1 -registry your-registry.com -tag v1.0.0 -push
+```
+
+#### Manually:
+```bash
+# Navigate to the project root
+cd /path/to/project
+
+# Build the image
+docker build -t ad-management:latest -f iac/docker/Dockerfile .
+```
+
+### Running with Docker
+
+To run the application with Docker:
+
+```bash
+docker run -d --name ad-management \
+ -p 5000:5000 \
+ -e NODE_ENV=production \
+ -e POSTGRES_HOST=your-postgres-host \
+ -e POSTGRES_PORT=5432 \
+ -e POSTGRES_DB=admgr \
+ -e POSTGRES_USER=postgres \
+ -e POSTGRES_PASSWORD=your-password \
+ -e REDIS_HOST=your-redis-host \
+ -e REDIS_PORT=6379 \
+ -e JWT_SECRET=your-jwt-secret \
+ -e SESSION_SECRET=your-session-secret \
+ ad-management:latest
+```
+
+### Environment Variables
+
+The application uses the following environment variables:
+
+| Variable | Description | Default |
+|----------|-------------|---------|
+| `NODE_ENV` | Environment mode | `development` |
+| `PORT` | Port to run the server on | `5000` |
+| `BASE_URL` | Base URL for the application | `http://localhost:5000` |
+| `POSTGRES_HOST` | PostgreSQL host | `localhost` |
+| `POSTGRES_PORT` | PostgreSQL port | `5432` |
+| `POSTGRES_DB` | PostgreSQL database name | `admgr` |
+| `POSTGRES_USER` | PostgreSQL username | `postgres` |
+| `POSTGRES_PASSWORD` | PostgreSQL password | - |
+| `REDIS_HOST` | Redis host | `localhost` |
+| `REDIS_PORT` | Redis port | `6379` |
+| `JWT_SECRET` | Secret for JWT tokens | - |
+| `SESSION_SECRET` | Secret for session cookies | - |
+| `DEFAULT_ADMIN_USERNAME` | Default admin username | `admin` |
+| `DEFAULT_ADMIN_PASSWORD` | Default admin password | - |
+| `DEFAULT_ADMIN_EMAIL` | Default admin email | - |
+| `DEFAULT_ADMIN_FULLNAME` | Default admin full name | `System Administrator` |
+| `DISABLE_REGISTRATION` | Disable user registration | `false` |
+
+## Docker Compose Deployment
+
+### Configuration
+
+The application includes a `docker-compose.yml` file for easy deployment. You can customize the environment variables by creating or modifying the `.env` file in the `iac/docker` directory.
+
+Example `.env` file:
+
+```
+NODE_ENV=production
+PORT=5000
+BASE_URL=http://localhost:5000
+POSTGRES_HOST=postgres
+POSTGRES_PORT=5432
+POSTGRES_DB=admgr
+POSTGRES_USER=postgres
+POSTGRES_PASSWORD=your-postgres-password
+REDIS_HOST=redis
+REDIS_PORT=6379
+JWT_SECRET=your-jwt-secret
+SESSION_SECRET=your-session-secret
+DEFAULT_ADMIN_USERNAME=admin
+DEFAULT_ADMIN_PASSWORD=your-admin-password
+DEFAULT_ADMIN_EMAIL=admin@example.com
+DEFAULT_ADMIN_FULLNAME=System Administrator
+DISABLE_REGISTRATION=false
+```
+
+### Starting the Services
+
+To start all services:
+
+```bash
+# Navigate to the iac/docker directory
+cd iac/docker
+
+# Start the application stack
+docker-compose up -d
+```
+
+This will start the application, PostgreSQL, and Redis containers.
+
+### Stopping the Services
+
+To stop all services:
+
+```bash
+# Navigate to the iac/docker directory
+cd iac/docker
+
+# Stop the application stack
+docker-compose down
+```
+
+To stop and remove all data volumes:
+
+```bash
+# Navigate to the iac/docker directory
+cd iac/docker
+
+# Stop the application stack and remove volumes
+docker-compose down -v
+```
+
+## Kubernetes Deployment
+
+### Prerequisites
+
+- Kubernetes cluster (v1.19+)
+- kubectl configured to communicate with your cluster
+- [Kustomize](https://kustomize.io/) (v4.0+) or kubectl v1.14+ which includes kustomize
+- Optional: Ingress controller (e.g., NGINX Ingress Controller)
+- Optional: cert-manager for TLS certificates
+
+### Kubernetes Configuration
+
+The Kubernetes manifests are located in the `iac/kubernetes/` directory. Before deploying, you should customize the following files:
+
+1. `iac/kubernetes/configmap.yaml`: Update environment variables
+2. `iac/kubernetes/secrets.yaml`: Update secrets (use base64-encoded values)
+3. `iac/kubernetes/deployment.yaml`: Update resource limits if needed
+4. `iac/kubernetes/ingress.yaml`: Update host name and TLS configuration
+
+### Deployment
+
+To deploy the application to Kubernetes:
+
+```bash
+# Navigate to the iac/kubernetes directory
+cd iac/kubernetes
+
+# Apply all resources
+kubectl apply -k .
+
+# Or from anywhere in the project
+kubectl apply -k iac/kubernetes
+```
+
+This will create:
+- A namespace called `ad-management`
+- ConfigMap and Secret for environment variables
+- PostgreSQL StatefulSet with persistent storage
+- Redis StatefulSet with persistent storage
+- Application Deployment with 2 replicas
+- Services for all components
+- Ingress for external access
+
+### Accessing the Application
+
+If you've configured the Ingress, the application will be available at the hostname specified in `kubernetes/ingress.yaml`.
+
+Without Ingress, you can use port-forwarding to access the application:
+
+```bash
+kubectl port-forward -n ad-management svc/ad-management 5000:80
+```
+
+Then access the application at http://localhost:5000.
+
+### Scaling
+
+To scale the application:
+
+```bash
+kubectl scale -n ad-management deployment/ad-management --replicas=3
+```
+
+### Updating
+
+To update the application to a new version:
+
+1. Build and push the new Docker image
+2. Update the image tag in `kubernetes/deployment.yaml`
+3. Apply the changes:
+
+```bash
+kubectl apply -k kubernetes/
+```
+
+Or use kubectl set image:
+
+```bash
+kubectl set image -n ad-management deployment/ad-management ad-management=ad-management:new-tag
+```
+
+### Monitoring
+
+You can monitor the application using:
+
+```bash
+# Check pod status
+kubectl get pods -n ad-management
+
+# View logs
+kubectl logs -n ad-management deployment/ad-management
+
+# Describe deployment
+kubectl describe deployment -n ad-management ad-management
+```
+
+## Production Considerations
+
+### Security
+
+For production deployments:
+
+1. Use strong, unique passwords for all secrets
+2. Enable TLS for all connections
+3. Use network policies to restrict traffic between components
+4. Set up proper RBAC for Kubernetes resources
+5. Regularly update all components and dependencies
+6. Consider using a secrets management solution like HashiCorp Vault
+
+### Backups
+
+Set up regular backups for:
+
+1. PostgreSQL database
+2. Redis data (if persistence is enabled)
+3. Application configuration
+
+Example PostgreSQL backup:
+
+```bash
+kubectl exec -n ad-management postgres-0 -- pg_dump -U postgres admgr > backup.sql
+```
+
+### High Availability
+
+For high availability:
+
+1. Run multiple replicas of the application
+2. Use a PostgreSQL cluster with replication
+3. Configure Redis with replication or cluster mode
+4. Deploy across multiple availability zones
+5. Use a load balancer or ingress controller with multiple backends
+6. Implement proper health checks and readiness probes
diff --git a/docs/kubernetes-deployment.md b/docs/kubernetes-deployment.md
new file mode 100644
index 0000000..a00ae97
--- /dev/null
+++ b/docs/kubernetes-deployment.md
@@ -0,0 +1,324 @@
+# Kubernetes Deployment Guide
+
+This guide provides detailed instructions for deploying the Active Directory Management application on Kubernetes.
+
+## Prerequisites
+
+- Kubernetes cluster (v1.19+)
+- kubectl configured to communicate with your cluster
+- [Kustomize](https://kustomize.io/) (v4.0+) or kubectl v1.14+ which includes kustomize
+- Optional: Ingress controller (e.g., NGINX Ingress Controller)
+- Optional: cert-manager for TLS certificates
+
+## Deployment Architecture
+
+The Kubernetes deployment consists of:
+
+- **Application Deployment**: The main application with multiple replicas
+- **PostgreSQL StatefulSet**: Database with persistent storage
+- **Redis StatefulSet**: Cache and session storage with persistent storage
+- **Services**: For internal communication between components
+- **Ingress**: For external access to the application
+- **ConfigMap and Secrets**: For configuration and sensitive data
+
+All Kubernetes manifests are located in the `iac/kubernetes/` directory.
+
+## Step-by-Step Deployment
+
+### 1. Customize Configuration
+
+Before deploying, customize the configuration files in the `iac/kubernetes/` directory:
+
+#### ConfigMap (`iac/kubernetes/configmap.yaml`)
+
+Update environment variables according to your environment:
+
+```yaml
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: ad-management-config
+ namespace: ad-management
+data:
+ NODE_ENV: "production"
+ PORT: "5000"
+ BASE_URL: "https://your-domain.com" # Update this
+ DEFAULT_ADMIN_USERNAME: "admin"
+ DEFAULT_ADMIN_FULLNAME: "System Administrator"
+ DISABLE_REGISTRATION: "false"
+ POSTGRES_DB: "admgr"
+ POSTGRES_USER: "postgres"
+```
+
+#### Secrets (`kubernetes/secrets.yaml`)
+
+Update the secrets with your own base64-encoded values:
+
+```bash
+# Generate base64-encoded secrets
+echo -n "your-jwt-secret" | base64
+echo -n "your-session-secret" | base64
+echo -n "your-admin-password" | base64
+echo -n "admin@your-domain.com" | base64
+echo -n "your-postgres-password" | base64
+```
+
+Then update `iac/kubernetes/secrets.yaml`:
+
+```yaml
+apiVersion: v1
+kind: Secret
+metadata:
+ name: ad-management-secrets
+ namespace: ad-management
+type: Opaque
+data:
+ JWT_SECRET: "base64-encoded-jwt-secret"
+ SESSION_SECRET: "base64-encoded-session-secret"
+ DEFAULT_ADMIN_PASSWORD: "base64-encoded-admin-password"
+ DEFAULT_ADMIN_EMAIL: "base64-encoded-admin-email"
+ POSTGRES_PASSWORD: "base64-encoded-postgres-password"
+```
+
+#### Ingress (`iac/kubernetes/ingress.yaml`)
+
+Update the hostname and TLS configuration:
+
+```yaml
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: ad-management-ingress
+ namespace: ad-management
+ annotations:
+ kubernetes.io/ingress.class: "nginx"
+ cert-manager.io/cluster-issuer: "letsencrypt-prod"
+ nginx.ingress.kubernetes.io/ssl-redirect: "true"
+spec:
+ tls:
+ - hosts:
+ - your-domain.com # Update this
+ secretName: ad-management-tls
+ rules:
+ - host: your-domain.com # Update this
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: ad-management
+ port:
+ name: http
+```
+
+### 2. Deploy the Application
+
+Deploy all resources using Kustomize:
+
+```bash
+# Navigate to the iac/kubernetes directory
+cd iac/kubernetes
+
+# Apply all resources
+kubectl apply -k .
+
+# Or from anywhere in the project
+kubectl apply -k iac/kubernetes
+```
+
+### 3. Verify Deployment
+
+Check that all resources are created and running:
+
+```bash
+# Check namespace
+kubectl get namespace ad-management
+
+# Check pods
+kubectl get pods -n ad-management
+
+# Check services
+kubectl get services -n ad-management
+
+# Check deployments and statefulsets
+kubectl get deployment,statefulset -n ad-management
+
+# Check ingress
+kubectl get ingress -n ad-management
+```
+
+### 4. Access the Application
+
+If you've configured the Ingress, the application will be available at your specified domain.
+
+Without Ingress, use port-forwarding:
+
+```bash
+kubectl port-forward -n ad-management svc/ad-management 5000:80
+```
+
+Then access the application at http://localhost:5000.
+
+## Common Operations
+
+### Scaling the Application
+
+To scale the application horizontally:
+
+```bash
+kubectl scale -n ad-management deployment/ad-management --replicas=3
+```
+
+### Updating the Application
+
+To update the application to a new version:
+
+```bash
+# Update the image
+kubectl set image -n ad-management deployment/ad-management ad-management=ad-management:new-tag
+
+# Check rollout status
+kubectl rollout status -n ad-management deployment/ad-management
+```
+
+### Viewing Logs
+
+To view application logs:
+
+```bash
+# View logs from all pods
+kubectl logs -n ad-management -l app=ad-management
+
+# View logs from a specific pod
+kubectl logs -n ad-management pod/ad-management-xxxx-yyyy
+```
+
+### Restarting Components
+
+To restart the application:
+
+```bash
+kubectl rollout restart -n ad-management deployment/ad-management
+```
+
+To restart PostgreSQL or Redis:
+
+```bash
+kubectl rollout restart -n ad-management statefulset/postgres
+kubectl rollout restart -n ad-management statefulset/redis
+```
+
+## Troubleshooting
+
+### Pod Startup Issues
+
+If pods are not starting:
+
+```bash
+# Check pod status
+kubectl get pods -n ad-management
+
+# Describe the pod for more details
+kubectl describe pod -n ad-management pod/ad-management-xxxx-yyyy
+
+# Check container logs
+kubectl logs -n ad-management pod/ad-management-xxxx-yyyy
+```
+
+### Database Connection Issues
+
+If the application can't connect to the database:
+
+1. Check if the PostgreSQL pod is running:
+ ```bash
+ kubectl get pods -n ad-management -l app=postgres
+ ```
+
+2. Check PostgreSQL logs:
+ ```bash
+ kubectl logs -n ad-management pod/postgres-0
+ ```
+
+3. Verify the connection from the application pod:
+ ```bash
+ kubectl exec -it -n ad-management pod/ad-management-xxxx-yyyy -- nc -zv postgres 5432
+ ```
+
+### Ingress Issues
+
+If the Ingress is not working:
+
+1. Check the Ingress status:
+ ```bash
+ kubectl describe ingress -n ad-management ad-management-ingress
+ ```
+
+2. Check the Ingress controller logs:
+ ```bash
+ kubectl logs -n ingress-nginx deployment/ingress-nginx-controller
+ ```
+
+## Examples
+
+### Example: Setting Up TLS with cert-manager
+
+1. Install cert-manager:
+ ```bash
+ kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.9.1/cert-manager.yaml
+ ```
+
+2. Create a ClusterIssuer for Let's Encrypt:
+ ```yaml
+ apiVersion: cert-manager.io/v1
+ kind: ClusterIssuer
+ metadata:
+ name: letsencrypt-prod
+ spec:
+ acme:
+ server: https://acme-v02.api.letsencrypt.org/directory
+ email: your-email@example.com
+ privateKeySecretRef:
+ name: letsencrypt-prod
+ solvers:
+ - http01:
+ ingress:
+ class: nginx
+ ```
+
+3. Apply the ClusterIssuer:
+ ```bash
+ kubectl apply -f cluster-issuer.yaml
+ ```
+
+### Example: Setting Up Monitoring with Prometheus and Grafana
+
+1. Install Prometheus Operator:
+ ```bash
+ kubectl apply -f https://github.com/prometheus-operator/kube-prometheus/releases/download/v0.10.0/manifests-0.10.0.tar.gz
+ ```
+
+2. Create a ServiceMonitor for the application:
+ ```yaml
+ apiVersion: monitoring.coreos.com/v1
+ kind: ServiceMonitor
+ metadata:
+ name: ad-management
+ namespace: monitoring
+ spec:
+ selector:
+ matchLabels:
+ app: ad-management
+ namespaceSelector:
+ matchNames:
+ - ad-management
+ endpoints:
+ - port: http
+ path: /metrics
+ interval: 15s
+ ```
+
+3. Apply the ServiceMonitor:
+ ```bash
+ kubectl apply -f service-monitor.yaml
+ ```
diff --git a/iac/README.md b/iac/README.md
new file mode 100644
index 0000000..1d9fe8c
--- /dev/null
+++ b/iac/README.md
@@ -0,0 +1,257 @@
+# Infrastructure as Code (IaC)
+
+This directory contains all the infrastructure as code configurations for deploying the Active Directory Management application.
+
+## Directory Structure
+
+- **docker/**: Docker-related files
+ - `Dockerfile`: Multi-stage Docker build file
+ - `docker-compose.yml`: Docker Compose configuration
+ - `build.sh`: Bash script for building and pushing Docker images (Linux/macOS)
+ - `build.ps1`: PowerShell script for building and pushing Docker images (Windows)
+ - `.env`: Environment variables for Docker deployment
+ - `.env.example`: Example environment variables
+ - `.env.production`: Production environment variables
+
+- **version.txt**: Central version file in yyyy.MM.dd.HHmm format
+- **update-version.sh**: Bash script to update version.txt (Linux/macOS)
+- **update-version.ps1**: PowerShell script to update version.txt (Windows)
+- **update-all.sh**: Comprehensive script to update all version references (Linux/macOS)
+- **update-all.ps1**: Comprehensive script to update all version references (Windows)
+
+- **kubernetes/**: Kubernetes manifests
+ - `namespace.yaml`: Namespace definition
+ - `configmap.yaml`: ConfigMap for non-sensitive configuration
+ - `secrets.yaml`: Secrets for sensitive configuration
+ - `postgres.yaml`: PostgreSQL StatefulSet and Service
+ - `redis.yaml`: Redis StatefulSet and Service
+ - `deployment.yaml`: Application Deployment and Service
+ - `ingress.yaml`: Ingress for external access
+ - `kustomization.yaml`: Kustomize configuration
+
+## Docker
+
+### Versioning
+
+The application uses a versioning scheme in the format `yyyy.MM.dd.HHmm` (year, month, day, hour, minute).
+
+#### Comprehensive Version Update
+
+To update the version and apply it to all components (recommended):
+
+On Linux/macOS:
+```bash
+# Navigate to the iac directory
+cd iac
+
+# Make the script executable
+chmod +x update-all.sh
+
+# Update all version references
+./update-all.sh
+```
+
+On Windows:
+```powershell
+# Navigate to the iac directory
+cd iac
+
+# Update all version references
+.\update-all.ps1
+```
+
+#### Manual Version Update
+
+To update only the version.txt file:
+
+On Linux/macOS:
+```bash
+# Navigate to the iac directory
+cd iac
+
+# Make the script executable
+chmod +x update-version.sh
+
+# Update the version
+./update-version.sh
+```
+
+On Windows:
+```powershell
+# Navigate to the iac directory
+cd iac
+
+# Update the version
+.\update-version.ps1
+```
+
+### Building Docker Images
+
+#### Using the build scripts
+
+On Linux/macOS:
+```bash
+# Navigate to the iac/docker directory
+cd iac/docker
+
+# Make the script executable
+chmod +x build.sh
+
+# Build the image with version from version.txt
+./build.sh
+
+# Build with custom tag
+./build.sh --tag 2025.05.21.1430
+
+# Build and push to a registry
+./build.sh --registry your-registry.com --push
+```
+
+On Windows:
+```powershell
+# Navigate to the iac/docker directory
+cd iac\docker
+
+# Build the image with version from version.txt
+.\build.ps1
+
+# Build with custom tag
+.\build.ps1 -tag 2025.05.21.1430
+
+# Build and push to a registry
+.\build.ps1 -registry your-registry.com -push
+```
+
+#### Manually
+
+```bash
+# Navigate to the project root
+cd /path/to/project
+
+# Get the version from version.txt
+VERSION=$(cat iac/version.txt)
+
+# Build the image
+docker build -t ActiveDirectoryManager:$VERSION -f iac/docker/Dockerfile .
+
+# Tag the image for a registry
+docker tag ActiveDirectoryManager:$VERSION your-registry.com/ActiveDirectoryManager:$VERSION
+
+# Push to registry
+docker push your-registry.com/ActiveDirectoryManager:$VERSION
+```
+
+### Running with Docker Compose
+
+```bash
+# Navigate to the iac/docker directory
+cd iac/docker
+
+# Create a .env file from the example if you haven't already
+cp .env.example .env
+# Edit the .env file with your configuration
+
+# Load the version from version.txt
+source .env.version
+
+# Start the application stack
+docker-compose up -d
+
+# View logs
+docker-compose logs -f
+
+# Stop the application stack
+docker-compose down
+```
+
+## Kubernetes
+
+### Prerequisites
+
+- Kubernetes cluster (v1.19+)
+- kubectl configured to communicate with your cluster
+- Kustomize (v4.0+) or kubectl v1.14+ which includes kustomize
+
+### Updating Deployment Version
+
+Before deploying, you should update the Kubernetes deployment to use the current version:
+
+On Linux/macOS:
+```bash
+# Navigate to the iac/kubernetes directory
+cd iac/kubernetes
+
+# Make the script executable
+chmod +x update-deployment-version.sh
+
+# Update the deployment version
+./update-deployment-version.sh
+```
+
+On Windows:
+```powershell
+# Navigate to the iac/kubernetes directory
+cd iac\kubernetes
+
+# Update the deployment version
+.\update-deployment-version.ps1
+```
+
+### Deployment
+
+```bash
+# Navigate to the iac/kubernetes directory
+cd iac/kubernetes
+
+# Apply all resources
+kubectl apply -k .
+
+# Or from anywhere in the project
+kubectl apply -k iac/kubernetes
+```
+
+### Customization
+
+To customize the deployment for different environments:
+
+1. Create a new directory for your environment:
+ ```bash
+ mkdir -p iac/kubernetes/environments/production
+ ```
+
+2. Create a kustomization.yaml file that references the base configuration:
+ ```yaml
+ apiVersion: kustomize.config.k8s.io/v1beta1
+ kind: Kustomization
+
+ namespace: ad-management-production
+
+ resources:
+ - ../../ # Reference the base configuration
+
+ patches:
+ # Add your patches here
+ ```
+
+3. Apply the environment-specific configuration:
+ ```bash
+ kubectl apply -k iac/kubernetes/environments/production
+ ```
+
+## CI/CD Integration
+
+The Docker build scripts are designed to be used in CI/CD pipelines. Example integration with GitHub Actions:
+
+```yaml
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+
+ - name: Build and push Docker image
+ run: |
+ cd iac/docker
+ chmod +x build.sh
+ ./build.sh --registry ghcr.io --tag ${{ github.sha }} --push
+```
diff --git a/iac/docker/.env b/iac/docker/.env
new file mode 100644
index 0000000..48f6573
--- /dev/null
+++ b/iac/docker/.env
@@ -0,0 +1,21 @@
+# Environment Variables for Docker Deployment
+# Copy from .env.example and customize as needed
+
+# Database Configuration
+POSTGRES_USER=admanagement
+POSTGRES_PASSWORD=change_this_to_a_secure_password
+POSTGRES_DB=admanagement
+
+# Application Configuration
+NODE_ENV=production
+PORT=5000
+BASE_URL=http://localhost:5000
+SESSION_SECRET=change_this_to_a_random_string
+JWT_SECRET=change_this_to_a_different_random_string
+
+# Default Admin User Configuration
+DEFAULT_ADMIN_USERNAME=admin
+DEFAULT_ADMIN_PASSWORD=change_this_to_a_secure_password
+DEFAULT_ADMIN_EMAIL=admin@example.com
+DEFAULT_ADMIN_FULLNAME=System Administrator
+DISABLE_REGISTRATION=false
diff --git a/.env.example b/iac/docker/.env.example
similarity index 99%
rename from .env.example
rename to iac/docker/.env.example
index 58bc72f..5017a66 100644
--- a/.env.example
+++ b/iac/docker/.env.example
@@ -53,4 +53,4 @@ DISABLE_REGISTRATION=false
# OIDC_USERINFO_URL=https://openidconnect.googleapis.com/v1/userinfo
# OIDC_CLIENT_ID=your_client_id
# OIDC_CLIENT_SECRET=your_client_secret
-# OIDC_CALLBACK_URL=http://localhost:5000/api/auth/oidc/callback
\ No newline at end of file
+# OIDC_CALLBACK_URL=http://localhost:5000/api/auth/oidc/callback
diff --git a/.env.production b/iac/docker/.env.production
similarity index 94%
rename from .env.production
rename to iac/docker/.env.production
index ce7499a..a4916bb 100644
--- a/.env.production
+++ b/iac/docker/.env.production
@@ -22,4 +22,4 @@ SESSION_COOKIE_SECURE=true
SESSION_COOKIE_HTTPONLY=true
# API Settings
-SWAGGER_ENABLED=true
\ No newline at end of file
+SWAGGER_ENABLED=true
diff --git a/iac/docker/.env.version b/iac/docker/.env.version
new file mode 100644
index 0000000..4265d59
--- /dev/null
+++ b/iac/docker/.env.version
@@ -0,0 +1,3 @@
+# This file is used to set the VERSION environment variable for docker-compose
+# It reads the version from the version.txt file
+VERSION=$(cat ../version.txt)
diff --git a/Dockerfile b/iac/docker/Dockerfile
similarity index 97%
rename from Dockerfile
rename to iac/docker/Dockerfile
index d4b56cc..aef0911 100644
--- a/Dockerfile
+++ b/iac/docker/Dockerfile
@@ -62,4 +62,4 @@ USER appuser
EXPOSE 5000
# Start the application
-CMD ["node", "dist/server/index.js"]
\ No newline at end of file
+CMD ["node", "dist/server/index.js"]
diff --git a/iac/docker/build.ps1 b/iac/docker/build.ps1
new file mode 100644
index 0000000..8562add
--- /dev/null
+++ b/iac/docker/build.ps1
@@ -0,0 +1,74 @@
+# PowerShell script for building Docker images (Windows)
+param (
+ [string]$tag = "",
+ [string]$registry = "",
+ [switch]$push = $false,
+ [string]$imageName = "ActiveDirectoryManager"
+)
+
+# If no tag is provided, use the version from version.txt
+if (-not $tag) {
+ $versionFile = Join-Path (Split-Path $PSScriptRoot) "version.txt"
+ if (Test-Path $versionFile) {
+ $tag = Get-Content $versionFile -Raw
+ $tag = $tag.Trim()
+ } else {
+ # If version.txt doesn't exist, use current date/time in yyyy.MM.dd.HHmm format
+ $tag = Get-Date -Format "yyyy.MM.dd.HHmm"
+ }
+}
+
+# Set error action preference to stop on any error
+$ErrorActionPreference = "Stop"
+
+# Display build information
+Write-Host "Building Docker image: $imageName" -ForegroundColor Green
+Write-Host "Tag: $tag" -ForegroundColor Green
+if ($registry) {
+ Write-Host "Registry: $registry" -ForegroundColor Green
+}
+
+# Determine the full image name
+$fullImageName = if ($registry) { "$registry/$imageName" } else { $imageName }
+
+# Check if Docker is available
+try {
+ docker --version
+}
+catch {
+ Write-Host "Docker is not available. Please install Docker and try again." -ForegroundColor Red
+ exit 1
+}
+
+# Build the Docker image
+try {
+ Write-Host "Building image: $fullImageName`:$tag" -ForegroundColor Cyan
+
+ # Navigate to the root directory (two levels up from the script location)
+ $rootDir = (Get-Item $PSScriptRoot).Parent.Parent.FullName
+
+ # Build the Docker image
+ docker build -t "$fullImageName`:$tag" -f "$PSScriptRoot/Dockerfile" $rootDir
+
+ if ($LASTEXITCODE -ne 0) {
+ throw "Docker build failed with exit code $LASTEXITCODE"
+ }
+
+ Write-Host "Successfully built image: $fullImageName`:$tag" -ForegroundColor Green
+
+ # Push the image if requested
+ if ($push) {
+ Write-Host "Pushing image to registry: $fullImageName`:$tag" -ForegroundColor Cyan
+ docker push "$fullImageName`:$tag"
+
+ if ($LASTEXITCODE -ne 0) {
+ throw "Docker push failed with exit code $LASTEXITCODE"
+ }
+
+ Write-Host "Successfully pushed image: $fullImageName`:$tag" -ForegroundColor Green
+ }
+}
+catch {
+ Write-Host "Error: $_" -ForegroundColor Red
+ exit 1
+}
diff --git a/iac/docker/build.sh b/iac/docker/build.sh
new file mode 100644
index 0000000..ded0c4b
--- /dev/null
+++ b/iac/docker/build.sh
@@ -0,0 +1,91 @@
+#!/bin/bash
+# Bash script for building Docker images (Linux/macOS)
+
+# Default values
+TAG=""
+REGISTRY=""
+PUSH=false
+IMAGE_NAME="ActiveDirectoryManager"
+
+# If no tag is provided, use the version from version.txt
+if [ -z "$TAG" ]; then
+ VERSION_FILE="$(dirname "$(dirname "$0")")/version.txt"
+ if [ -f "$VERSION_FILE" ]; then
+ TAG=$(cat "$VERSION_FILE" | tr -d '[:space:]')
+ else
+ # If version.txt doesn't exist, use current date/time in yyyy.MM.dd.HHmm format
+ TAG=$(date +"%Y.%m.%d.%H%M")
+ fi
+fi
+
+# Parse command line arguments
+while [[ $# -gt 0 ]]; do
+ case $1 in
+ --tag)
+ TAG="$2"
+ shift 2
+ ;;
+ --registry)
+ REGISTRY="$2"
+ shift 2
+ ;;
+ --push)
+ PUSH=true
+ shift
+ ;;
+ --image-name)
+ IMAGE_NAME="$2"
+ shift 2
+ ;;
+ *)
+ echo "Unknown option: $1"
+ exit 1
+ ;;
+ esac
+done
+
+# Determine the full image name
+if [ -n "$REGISTRY" ]; then
+ FULL_IMAGE_NAME="$REGISTRY/$IMAGE_NAME"
+else
+ FULL_IMAGE_NAME="$IMAGE_NAME"
+fi
+
+# Display build information
+echo -e "\e[32mBuilding Docker image: $IMAGE_NAME\e[0m"
+echo -e "\e[32mTag: $TAG\e[0m"
+if [ -n "$REGISTRY" ]; then
+ echo -e "\e[32mRegistry: $REGISTRY\e[0m"
+fi
+
+# Check if Docker is available
+if ! command -v docker &> /dev/null; then
+ echo -e "\e[31mDocker is not available. Please install Docker and try again.\e[0m"
+ exit 1
+fi
+
+# Build the Docker image
+echo -e "\e[36mBuilding image: $FULL_IMAGE_NAME:$TAG\e[0m"
+
+# Navigate to the root directory (two levels up from the script location)
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+
+# Build the Docker image
+if ! docker build -t "$FULL_IMAGE_NAME:$TAG" -f "$(dirname "${BASH_SOURCE[0]}")/Dockerfile" "$ROOT_DIR"; then
+ echo -e "\e[31mDocker build failed\e[0m"
+ exit 1
+fi
+
+echo -e "\e[32mSuccessfully built image: $FULL_IMAGE_NAME:$TAG\e[0m"
+
+# Push the image if requested
+if [ "$PUSH" = true ]; then
+ echo -e "\e[36mPushing image to registry: $FULL_IMAGE_NAME:$TAG\e[0m"
+
+ if ! docker push "$FULL_IMAGE_NAME:$TAG"; then
+ echo -e "\e[31mDocker push failed\e[0m"
+ exit 1
+ fi
+
+ echo -e "\e[32mSuccessfully pushed image: $FULL_IMAGE_NAME:$TAG\e[0m"
+fi
diff --git a/docker-compose.yml b/iac/docker/docker-compose.yml
similarity index 87%
rename from docker-compose.yml
rename to iac/docker/docker-compose.yml
index b2acf7b..f817fd6 100644
--- a/docker-compose.yml
+++ b/iac/docker/docker-compose.yml
@@ -1,9 +1,10 @@
services:
app:
build:
- context: .
- dockerfile: Dockerfile
- container_name: ad-management-api
+ context: ../../
+ dockerfile: iac/docker/Dockerfile
+ image: ActiveDirectoryManager:${VERSION:-latest}
+ container_name: ActiveDirectoryManager-api
restart: unless-stopped
ports:
- "5000:5000"
@@ -34,7 +35,7 @@ services:
postgres:
image: postgres:15-alpine
- container_name: ad-management-postgres
+ container_name: ActiveDirectoryManager-postgres
restart: unless-stopped
environment:
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
@@ -55,7 +56,7 @@ services:
redis:
image: redis:7-alpine
- container_name: ad-management-redis
+ container_name: ActiveDirectoryManager-redis
restart: unless-stopped
command: redis-server --appendonly yes
volumes:
@@ -76,4 +77,4 @@ networks:
volumes:
postgres-data:
- redis-data:
\ No newline at end of file
+ redis-data:
diff --git a/iac/kubernetes/configmap.yaml b/iac/kubernetes/configmap.yaml
new file mode 100644
index 0000000..6a9ecdd
--- /dev/null
+++ b/iac/kubernetes/configmap.yaml
@@ -0,0 +1,14 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: ad-management-config
+ namespace: ad-management
+data:
+ NODE_ENV: "production"
+ PORT: "5000"
+ BASE_URL: "https://ad-management.example.com"
+ DEFAULT_ADMIN_USERNAME: "admin"
+ DEFAULT_ADMIN_FULLNAME: "System Administrator"
+ DISABLE_REGISTRATION: "false"
+ POSTGRES_DB: "admgr"
+ POSTGRES_USER: "postgres"
diff --git a/iac/kubernetes/deployment.yaml b/iac/kubernetes/deployment.yaml
new file mode 100644
index 0000000..dd6899a
--- /dev/null
+++ b/iac/kubernetes/deployment.yaml
@@ -0,0 +1,73 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: ad-management
+ namespace: ad-management
+spec:
+ replicas: 2
+ selector:
+ matchLabels:
+ app: ad-management
+ template:
+ metadata:
+ labels:
+ app: ad-management
+ spec:
+ containers:
+ - name: activedirectorymanager
+ image: ActiveDirectoryManager:2025.05.21.1430
+ imagePullPolicy: Always
+ ports:
+ - containerPort: 5000
+ name: http
+ envFrom:
+ - configMapRef:
+ name: ad-management-config
+ - secretRef:
+ name: ad-management-secrets
+ env:
+ - name: REDIS_HOST
+ value: "redis"
+ - name: REDIS_PORT
+ value: "6379"
+ - name: POSTGRES_HOST
+ value: "postgres"
+ - name: POSTGRES_PORT
+ value: "5432"
+ resources:
+ requests:
+ memory: "256Mi"
+ cpu: "100m"
+ limits:
+ memory: "512Mi"
+ cpu: "300m"
+ livenessProbe:
+ httpGet:
+ path: /api/health
+ port: http
+ initialDelaySeconds: 30
+ periodSeconds: 10
+ timeoutSeconds: 5
+ failureThreshold: 3
+ readinessProbe:
+ httpGet:
+ path: /api/health
+ port: http
+ initialDelaySeconds: 5
+ periodSeconds: 5
+ timeoutSeconds: 3
+ failureThreshold: 1
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: ad-management
+ namespace: ad-management
+spec:
+ selector:
+ app: ad-management
+ ports:
+ - port: 80
+ targetPort: 5000
+ name: http
+ type: ClusterIP
diff --git a/iac/kubernetes/ingress.yaml b/iac/kubernetes/ingress.yaml
new file mode 100644
index 0000000..26b2b2f
--- /dev/null
+++ b/iac/kubernetes/ingress.yaml
@@ -0,0 +1,26 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: ad-management-ingress
+ namespace: ad-management
+ annotations:
+ kubernetes.io/ingress.class: "nginx"
+ cert-manager.io/cluster-issuer: "letsencrypt-prod"
+ nginx.ingress.kubernetes.io/ssl-redirect: "true"
+ nginx.ingress.kubernetes.io/proxy-body-size: "50m"
+spec:
+ tls:
+ - hosts:
+ - ad-management.example.com
+ secretName: ad-management-tls
+ rules:
+ - host: ad-management.example.com
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: ad-management
+ port:
+ name: http
diff --git a/iac/kubernetes/kustomization.yaml b/iac/kubernetes/kustomization.yaml
new file mode 100644
index 0000000..6a5a295
--- /dev/null
+++ b/iac/kubernetes/kustomization.yaml
@@ -0,0 +1,13 @@
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+namespace: ad-management
+
+resources:
+ - namespace.yaml
+ - configmap.yaml
+ - secrets.yaml
+ - postgres.yaml
+ - redis.yaml
+ - deployment.yaml
+ - ingress.yaml
diff --git a/iac/kubernetes/namespace.yaml b/iac/kubernetes/namespace.yaml
new file mode 100644
index 0000000..7f7b1b6
--- /dev/null
+++ b/iac/kubernetes/namespace.yaml
@@ -0,0 +1,6 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: ad-management
+ labels:
+ name: ad-management
diff --git a/iac/kubernetes/postgres.yaml b/iac/kubernetes/postgres.yaml
new file mode 100644
index 0000000..eb7f65c
--- /dev/null
+++ b/iac/kubernetes/postgres.yaml
@@ -0,0 +1,89 @@
+apiVersion: apps/v1
+kind: StatefulSet
+metadata:
+ name: postgres
+ namespace: ad-management
+spec:
+ serviceName: postgres
+ replicas: 1
+ selector:
+ matchLabels:
+ app: postgres
+ template:
+ metadata:
+ labels:
+ app: postgres
+ spec:
+ containers:
+ - name: postgres
+ image: postgres:15-alpine
+ ports:
+ - containerPort: 5432
+ name: postgres
+ env:
+ - name: POSTGRES_DB
+ valueFrom:
+ configMapKeyRef:
+ name: ad-management-config
+ key: POSTGRES_DB
+ - name: POSTGRES_USER
+ valueFrom:
+ configMapKeyRef:
+ name: ad-management-config
+ key: POSTGRES_USER
+ - name: POSTGRES_PASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: ad-management-secrets
+ key: POSTGRES_PASSWORD
+ volumeMounts:
+ - name: postgres-data
+ mountPath: /var/lib/postgresql/data
+ resources:
+ requests:
+ memory: "256Mi"
+ cpu: "100m"
+ limits:
+ memory: "1Gi"
+ cpu: "500m"
+ livenessProbe:
+ exec:
+ command:
+ - pg_isready
+ - -U
+ - postgres
+ initialDelaySeconds: 30
+ periodSeconds: 10
+ timeoutSeconds: 5
+ failureThreshold: 3
+ readinessProbe:
+ exec:
+ command:
+ - pg_isready
+ - -U
+ - postgres
+ initialDelaySeconds: 5
+ periodSeconds: 5
+ timeoutSeconds: 3
+ failureThreshold: 1
+ volumeClaimTemplates:
+ - metadata:
+ name: postgres-data
+ spec:
+ accessModes: [ "ReadWriteOnce" ]
+ resources:
+ requests:
+ storage: 10Gi
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: postgres
+ namespace: ad-management
+spec:
+ selector:
+ app: postgres
+ ports:
+ - port: 5432
+ targetPort: 5432
+ clusterIP: None # Headless service for StatefulSet
diff --git a/iac/kubernetes/redis.yaml b/iac/kubernetes/redis.yaml
new file mode 100644
index 0000000..3a3b4c1
--- /dev/null
+++ b/iac/kubernetes/redis.yaml
@@ -0,0 +1,71 @@
+apiVersion: apps/v1
+kind: StatefulSet
+metadata:
+ name: redis
+ namespace: ad-management
+spec:
+ serviceName: redis
+ replicas: 1
+ selector:
+ matchLabels:
+ app: redis
+ template:
+ metadata:
+ labels:
+ app: redis
+ spec:
+ containers:
+ - name: redis
+ image: redis:7-alpine
+ ports:
+ - containerPort: 6379
+ name: redis
+ volumeMounts:
+ - name: redis-data
+ mountPath: /data
+ resources:
+ requests:
+ memory: "128Mi"
+ cpu: "50m"
+ limits:
+ memory: "256Mi"
+ cpu: "200m"
+ livenessProbe:
+ exec:
+ command:
+ - redis-cli
+ - ping
+ initialDelaySeconds: 30
+ periodSeconds: 10
+ timeoutSeconds: 5
+ failureThreshold: 3
+ readinessProbe:
+ exec:
+ command:
+ - redis-cli
+ - ping
+ initialDelaySeconds: 5
+ periodSeconds: 5
+ timeoutSeconds: 3
+ failureThreshold: 1
+ volumeClaimTemplates:
+ - metadata:
+ name: redis-data
+ spec:
+ accessModes: [ "ReadWriteOnce" ]
+ resources:
+ requests:
+ storage: 1Gi
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: redis
+ namespace: ad-management
+spec:
+ selector:
+ app: redis
+ ports:
+ - port: 6379
+ targetPort: 6379
+ clusterIP: None # Headless service for StatefulSet
diff --git a/iac/kubernetes/secrets.yaml b/iac/kubernetes/secrets.yaml
new file mode 100644
index 0000000..ba54619
--- /dev/null
+++ b/iac/kubernetes/secrets.yaml
@@ -0,0 +1,14 @@
+apiVersion: v1
+kind: Secret
+metadata:
+ name: ad-management-secrets
+ namespace: ad-management
+type: Opaque
+data:
+ # These are example values. In production, replace with your own base64-encoded secrets
+ # Example: echo -n "your-secret-value" | base64
+ JWT_SECRET: Y2hhbmdlLXRoaXMtaW4tcHJvZHVjdGlvbg==
+ SESSION_SECRET: Y2hhbmdlLXRoaXMtaW4tcHJvZHVjdGlvbg==
+ DEFAULT_ADMIN_PASSWORD: cGFzc3dvcmQ=
+ DEFAULT_ADMIN_EMAIL: YWRtaW5AZXhhbXBsZS5jb20=
+ POSTGRES_PASSWORD: cG9zdGdyZXM=
diff --git a/iac/kubernetes/update-deployment-version.ps1 b/iac/kubernetes/update-deployment-version.ps1
new file mode 100644
index 0000000..243cfcf
--- /dev/null
+++ b/iac/kubernetes/update-deployment-version.ps1
@@ -0,0 +1,40 @@
+# PowerShell script to update the Kubernetes deployment with the current version
+
+# Get the directory of this script
+$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
+$parentDir = Split-Path -Parent $scriptDir
+
+# Read the version from version.txt
+$versionFile = Join-Path $parentDir "version.txt"
+if (-not (Test-Path $versionFile)) {
+ Write-Host "Error: version.txt not found at $versionFile" -ForegroundColor Red
+ exit 1
+}
+
+$version = Get-Content $versionFile -Raw
+$version = $version.Trim()
+Write-Host "Using version: $version" -ForegroundColor Cyan
+
+# Update the deployment.yaml file
+$deploymentFile = Join-Path $scriptDir "deployment.yaml"
+if (-not (Test-Path $deploymentFile)) {
+ Write-Host "Error: deployment.yaml not found at $deploymentFile" -ForegroundColor Red
+ exit 1
+}
+
+# Read the deployment file
+$content = Get-Content $deploymentFile -Raw
+
+# Replace the image version
+$pattern1 = 'image: ActiveDirectoryManager:[0-9]{4}\.[0-9]{2}\.[0-9]{2}\.[0-9]{4}'
+$replacement1 = "image: ActiveDirectoryManager:$version"
+$content = $content -replace $pattern1, $replacement1
+
+$pattern2 = 'image: ActiveDirectoryManager:latest'
+$replacement2 = "image: ActiveDirectoryManager:$version"
+$content = $content -replace $pattern2, $replacement2
+
+# Write the updated content back to the file
+$content | Set-Content $deploymentFile
+
+Write-Host "Updated deployment.yaml with version $version" -ForegroundColor Green
diff --git a/iac/kubernetes/update-deployment-version.sh b/iac/kubernetes/update-deployment-version.sh
new file mode 100644
index 0000000..b1a5698
--- /dev/null
+++ b/iac/kubernetes/update-deployment-version.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+# Script to update the Kubernetes deployment with the current version
+
+# Get the directory of this script
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PARENT_DIR="$(dirname "$SCRIPT_DIR")"
+
+# Read the version from version.txt
+VERSION_FILE="$PARENT_DIR/version.txt"
+if [ ! -f "$VERSION_FILE" ]; then
+ echo "Error: version.txt not found at $VERSION_FILE"
+ exit 1
+fi
+
+VERSION=$(cat "$VERSION_FILE" | tr -d '[:space:]')
+echo "Using version: $VERSION"
+
+# Update the deployment.yaml file
+DEPLOYMENT_FILE="$SCRIPT_DIR/deployment.yaml"
+if [ ! -f "$DEPLOYMENT_FILE" ]; then
+ echo "Error: deployment.yaml not found at $DEPLOYMENT_FILE"
+ exit 1
+fi
+
+# Use sed to replace the image version
+if [[ "$OSTYPE" == "darwin"* ]]; then
+ # macOS requires a different sed syntax
+ sed -i '' "s|image: ActiveDirectoryManager:[0-9]\{4\}\.[0-9]\{2\}\.[0-9]\{2\}\.[0-9]\{4\}|image: ActiveDirectoryManager:$VERSION|g" "$DEPLOYMENT_FILE"
+ sed -i '' "s|image: ActiveDirectoryManager:latest|image: ActiveDirectoryManager:$VERSION|g" "$DEPLOYMENT_FILE"
+else
+ # Linux
+ sed -i "s|image: ActiveDirectoryManager:[0-9]\{4\}\.[0-9]\{2\}\.[0-9]\{2\}\.[0-9]\{4\}|image: ActiveDirectoryManager:$VERSION|g" "$DEPLOYMENT_FILE"
+ sed -i "s|image: ActiveDirectoryManager:latest|image: ActiveDirectoryManager:$VERSION|g" "$DEPLOYMENT_FILE"
+fi
+
+echo "Updated deployment.yaml with version $VERSION"
diff --git a/iac/update-all.ps1 b/iac/update-all.ps1
new file mode 100644
index 0000000..4ad7026
--- /dev/null
+++ b/iac/update-all.ps1
@@ -0,0 +1,24 @@
+# PowerShell script to update version and apply it to all components
+
+# Get the directory of this script
+$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
+
+# Update version.txt with current date/time
+Write-Host "Updating version.txt..." -ForegroundColor Cyan
+& "$scriptDir\update-version.ps1"
+
+# Read the new version
+$version = Get-Content "$scriptDir\version.txt" -Raw
+$version = $version.Trim()
+Write-Host "New version: $version" -ForegroundColor Green
+
+# Update Kubernetes deployment
+Write-Host "Updating Kubernetes deployment..." -ForegroundColor Cyan
+& "$scriptDir\kubernetes\update-deployment-version.ps1"
+
+Write-Host "Version update complete. New version: $version" -ForegroundColor Green
+Write-Host ""
+Write-Host "Next steps:" -ForegroundColor Yellow
+Write-Host "1. Build Docker image: cd $scriptDir\docker; .\build.ps1" -ForegroundColor Yellow
+Write-Host "2. Push Docker image: cd $scriptDir\docker; .\build.ps1 -push" -ForegroundColor Yellow
+Write-Host "3. Deploy to Kubernetes: cd $scriptDir\kubernetes; kubectl apply -k ." -ForegroundColor Yellow
diff --git a/iac/update-all.sh b/iac/update-all.sh
new file mode 100644
index 0000000..39b5e34
--- /dev/null
+++ b/iac/update-all.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+# Comprehensive script to update version and apply it to all components
+
+# Get the directory of this script
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+
+# Update version.txt with current date/time
+echo "Updating version.txt..."
+"$SCRIPT_DIR/update-version.sh"
+
+# Read the new version
+VERSION=$(cat "$SCRIPT_DIR/version.txt" | tr -d '[:space:]')
+echo "New version: $VERSION"
+
+# Update Kubernetes deployment
+echo "Updating Kubernetes deployment..."
+"$SCRIPT_DIR/kubernetes/update-deployment-version.sh"
+
+echo "Version update complete. New version: $VERSION"
+echo ""
+echo "Next steps:"
+echo "1. Build Docker image: cd $SCRIPT_DIR/docker && ./build.sh"
+echo "2. Push Docker image: cd $SCRIPT_DIR/docker && ./build.sh --push"
+echo "3. Deploy to Kubernetes: cd $SCRIPT_DIR/kubernetes && kubectl apply -k ."
diff --git a/iac/update-version.ps1 b/iac/update-version.ps1
new file mode 100644
index 0000000..ea93351
--- /dev/null
+++ b/iac/update-version.ps1
@@ -0,0 +1,10 @@
+# PowerShell script to update the version.txt file with the current date and time
+
+# Generate version in yyyy.MM.dd.HHmm format
+$version = Get-Date -Format "yyyy.MM.dd.HHmm"
+
+# Update version.txt
+$versionFile = Join-Path $PSScriptRoot "version.txt"
+$version | Out-File -FilePath $versionFile -NoNewline
+
+Write-Host "Version updated to: $version"
diff --git a/iac/update-version.sh b/iac/update-version.sh
new file mode 100644
index 0000000..5379c1f
--- /dev/null
+++ b/iac/update-version.sh
@@ -0,0 +1,10 @@
+#!/bin/bash
+# Script to update the version.txt file with the current date and time
+
+# Generate version in yyyy.MM.dd.HHmm format
+VERSION=$(date +"%Y.%m.%d.%H%M")
+
+# Update version.txt
+echo "$VERSION" > "$(dirname "$0")/version.txt"
+
+echo "Version updated to: $VERSION"
diff --git a/iac/version.txt b/iac/version.txt
new file mode 100644
index 0000000..55d03ad
--- /dev/null
+++ b/iac/version.txt
@@ -0,0 +1 @@
+2025.05.21.1430
diff --git a/server/ldap.ts b/server/ldap.ts
index 2f81108..fbb9ebe 100644
--- a/server/ldap.ts
+++ b/server/ldap.ts
@@ -3,247 +3,535 @@ import ldap from 'ldapjs';
import { LdapConnection } from '@shared/schema';
import { storage } from './storage';
+// DN escaping utilities
+const DN_SPECIAL_CHARS = /[,=+<>#;\\"\n]/;
+const RDN_SPECIAL_CHARS = /[,=+<>#;\\"\n/]/;
+
+// Escape a string for use in a DN
+function escapeDN(value: string): string {
+ if (!value) return '';
+
+ // If the value doesn't contain special characters, return it as is
+ if (!DN_SPECIAL_CHARS.test(value)) return value;
+
+ // Otherwise, escape each special character
+ return value.replace(/([\\\,\=\+\<\>\#\;\"\n])/g, '\\$1');
+}
+
+// Escape a string for use in an RDN
+function escapeRDN(value: string): string {
+ if (!value) return '';
+
+ // If the value doesn't contain special characters, return it as is
+ if (!RDN_SPECIAL_CHARS.test(value)) return value;
+
+ // Otherwise, escape each special character
+ return value.replace(/([\\\,\=\+\<\>\#\;\"\n\/])/g, '\\$1');
+}
+
+// Parse a DN into its component parts
+function parseDN(dn: string): { attribute: string, value: string }[] {
+ if (!dn) return [];
+
+ const parts = [];
+ let inEscape = false;
+ let currentPart = '';
+
+ for (let i = 0; i < dn.length; i++) {
+ const char = dn[i];
+
+ if (inEscape) {
+ currentPart += char;
+ inEscape = false;
+ } else if (char === '\\') {
+ inEscape = true;
+ } else if (char === ',' && !inEscape) {
+ if (currentPart) {
+ const [attribute, value] = currentPart.split('=', 2);
+ parts.push({ attribute: attribute.trim(), value: value.trim() });
+ currentPart = '';
+ }
+ } else {
+ currentPart += char;
+ }
+ }
+
+ if (currentPart) {
+ const [attribute, value] = currentPart.split('=', 2);
+ parts.push({ attribute: attribute.trim(), value: value.trim() });
+ }
+
+ return parts;
+}
+
+// Build a DN from component parts
+function buildDN(parts: { attribute: string, value: string }[]): string {
+ return parts.map(part => `${part.attribute}=${escapeDN(part.value)}`).join(',');
+}
+
+// Connection pool configuration
+interface ConnectionPoolConfig {
+ maxConnections: number;
+ idleTimeout: number; // in milliseconds
+ acquireTimeout: number; // in milliseconds
+}
+
+// Default connection pool configuration
+const DEFAULT_POOL_CONFIG: ConnectionPoolConfig = {
+ maxConnections: 10,
+ idleTimeout: 60000, // 1 minute
+ acquireTimeout: 10000 // 10 seconds
+};
+
+// Connection pool entry
+interface PoolEntry {
+ client: ldap.Client;
+ lastUsed: number;
+ inUse: boolean;
+}
+
class LdapClient extends EventEmitter {
- private clients: Map = new Map();
- private isConnected: Map = new Map();
-
+ private readonly clients: Map = new Map();
+ private readonly isConnected: Map = new Map();
+ private readonly connectionPools: Map = new Map();
+ private readonly poolConfig: ConnectionPoolConfig;
+
+ constructor(poolConfig?: Partial) {
+ super();
+ this.poolConfig = { ...DEFAULT_POOL_CONFIG, ...poolConfig };
+
+ // Start the idle connection cleanup timer
+ setInterval(() => this.cleanupIdleConnections(), this.poolConfig.idleTimeout);
+ }
+
+ // Clean up idle connections
+ private cleanupIdleConnections(): void {
+ const now = Date.now();
+
+ for (const [connectionId, pool] of this.connectionPools.entries()) {
+ // Filter out idle connections that exceed the idle timeout
+ const activeConnections = pool.filter(entry => {
+ const isIdle = !entry.inUse && (now - entry.lastUsed > this.poolConfig.idleTimeout);
+
+ if (isIdle) {
+ try {
+ // Unbind the idle connection
+ entry.client.unbind();
+ console.log(`Closed idle LDAP connection for connection ID ${connectionId}`);
+ } catch (error) {
+ console.error(`Error closing idle LDAP connection: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ return false;
+ }
+
+ return true;
+ });
+
+ // Update the pool with only active connections
+ this.connectionPools.set(connectionId, activeConnections);
+
+ // If all connections are gone, update the connection status
+ if (activeConnections.length === 0 && this.isConnected.get(connectionId)) {
+ this.isConnected.set(connectionId, false);
+ }
+ }
+ }
+
+ // Get a connection from the pool or create a new one
+ private async getPooledConnection(connection: LdapConnection): Promise {
+ const connectionId = connection.id;
+ let pool = this.connectionPools.get(connectionId) || [];
+
+ // Look for an available connection in the pool
+ const availableEntry = pool.find(entry => !entry.inUse);
+
+ if (availableEntry) {
+ // Mark the connection as in use
+ availableEntry.inUse = true;
+ availableEntry.lastUsed = Date.now();
+ return availableEntry.client;
+ }
+
+ // Check if we've reached the maximum number of connections
+ if (pool.length >= this.poolConfig.maxConnections) {
+ // Wait for a connection to become available
+ return new Promise((resolve, reject) => {
+ const timeout = setTimeout(() => {
+ reject(new Error(`Timed out waiting for an available LDAP connection after ${this.poolConfig.acquireTimeout}ms`));
+ }, this.poolConfig.acquireTimeout);
+
+ const checkForAvailableConnection = () => {
+ const pool = this.connectionPools.get(connectionId) || [];
+ const availableEntry = pool.find(entry => !entry.inUse);
+
+ if (availableEntry) {
+ clearTimeout(timeout);
+ availableEntry.inUse = true;
+ availableEntry.lastUsed = Date.now();
+ resolve(availableEntry.client);
+ } else {
+ // Check again in 100ms
+ setTimeout(checkForAvailableConnection, 100);
+ }
+ };
+
+ checkForAvailableConnection();
+ });
+ }
+
+ // Create a new connection
+ const clientOptions: ldap.ClientOptions = {
+ url: `${connection.useTLS ? 'ldaps' : 'ldap'}://${connection.server}:${connection.port}`,
+ reconnect: {
+ initialDelay: 1000,
+ maxDelay: 10000,
+ failAfter: 10
+ },
+ timeout: 5000,
+ connectTimeout: 10000
+ };
+
+ const client = ldap.createClient(clientOptions);
+
+ return new Promise((resolve, reject) => {
+ const timeout = setTimeout(() => {
+ reject(new Error(`Connection timeout after ${this.poolConfig.acquireTimeout}ms`));
+ }, this.poolConfig.acquireTimeout);
+
+ client.on('error', async (err) => {
+ console.error(`LDAP connection error for ${connection.name}:`, err);
+
+ // Remove this client from the pool if it exists
+ const pool = this.connectionPools.get(connectionId) || [];
+ const updatedPool = pool.filter(entry => entry.client !== client);
+ this.connectionPools.set(connectionId, updatedPool);
+
+ if (updatedPool.length === 0) {
+ this.isConnected.set(connectionId, false);
+ await storage.updateLdapConnection(connectionId, { status: 'disconnected' });
+ this.emit('status', {
+ connectionId,
+ status: 'disconnected',
+ error: err.message
+ });
+ }
+ });
+
+ client.bind(connection.username, connection.password, (err) => {
+ clearTimeout(timeout);
+
+ if (err) {
+ console.error(`LDAP bind error for ${connection.name}:`, err);
+ this.isConnected.set(connectionId, false);
+
+ // Update connection status in database
+ storage.updateLdapConnection(connectionId, { status: 'disconnected' })
+ .catch(dbErr => console.error(`Error updating LDAP connection status: ${dbErr}`));
+
+ this.emit('status', {
+ connectionId,
+ status: 'disconnected',
+ error: err.message
+ });
+
+ reject(err);
+ return;
+ }
+
+ // Add the new connection to the pool
+ const newEntry: PoolEntry = {
+ client,
+ lastUsed: Date.now(),
+ inUse: true
+ };
+
+ pool.push(newEntry);
+ this.connectionPools.set(connectionId, pool);
+ this.isConnected.set(connectionId, true);
+
+ // Update connection status in database
+ storage.updateLdapConnection(connectionId, {
+ status: 'connected',
+ lastConnected: new Date()
+ }).catch(dbErr => console.error(`Error updating LDAP connection status: ${dbErr}`));
+
+ this.emit('status', {
+ connectionId,
+ status: 'connected'
+ });
+
+ resolve(client);
+ });
+ });
+ }
+
+ // Release a connection back to the pool
+ private releaseConnection(connectionId: number, client: ldap.Client): void {
+ const pool = this.connectionPools.get(connectionId);
+ if (!pool) return;
+
+ const entry = pool.find(e => e.client === client);
+ if (entry) {
+ entry.inUse = false;
+ entry.lastUsed = Date.now();
+ }
+ }
+
async connect(connection: LdapConnection): Promise {
try {
- const clientOptions: ldap.ClientOptions = {
- url: `${connection.useTLS ? 'ldaps' : 'ldap'}://${connection.server}:${connection.port}`,
- reconnect: {
- initialDelay: 1000,
- maxDelay: 10000,
- failAfter: 10
- },
- timeout: 5000,
- connectTimeout: 10000
- };
-
- const client = ldap.createClient(clientOptions);
-
- return new Promise((resolve, reject) => {
- client.on('error', async (err) => {
- console.error(`LDAP connection error for ${connection.name}:`, err);
- this.isConnected.set(connection.id, false);
- await storage.updateLdapConnection(connection.id, { status: 'disconnected' });
- this.emit('status', {
- connectionId: connection.id,
- status: 'disconnected',
- error: err.message
- });
- });
-
- client.bind(connection.username, connection.password, async (err) => {
- if (err) {
- console.error(`LDAP bind error for ${connection.name}:`, err);
- this.isConnected.set(connection.id, false);
- await storage.updateLdapConnection(connection.id, { status: 'disconnected' });
- this.emit('status', {
- connectionId: connection.id,
- status: 'disconnected',
- error: err.message
- });
- reject(err);
- return;
- }
-
- this.clients.set(connection.id, client);
- this.isConnected.set(connection.id, true);
- await storage.updateLdapConnection(connection.id, {
- status: 'connected',
- lastConnected: new Date()
- });
-
- this.emit('status', {
- connectionId: connection.id,
- status: 'connected'
- });
-
- resolve(true);
- });
- });
+ await this.getPooledConnection(connection);
+ return true;
} catch (error) {
console.error(`LDAP connection error for ${connection.name}:`, error);
this.isConnected.set(connection.id, false);
await storage.updateLdapConnection(connection.id, { status: 'disconnected' });
- this.emit('status', {
- connectionId: connection.id,
- status: 'disconnected',
- error: error instanceof Error ? error.message : String(error)
+ this.emit('status', {
+ connectionId: connection.id,
+ status: 'disconnected',
+ error: error instanceof Error ? error.message : String(error)
});
throw error;
}
}
-
+
async disconnect(connectionId: number): Promise {
+ // Close all connections in the pool
+ const pool = this.connectionPools.get(connectionId) || [];
+
+ for (const entry of pool) {
+ try {
+ entry.client.unbind();
+ } catch (error) {
+ console.error(`Error unbinding LDAP connection: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+
+ // Clear the pool
+ this.connectionPools.delete(connectionId);
+ this.isConnected.set(connectionId, false);
+
+ // Also handle legacy client if it exists
const client = this.clients.get(connectionId);
if (client) {
- return new Promise((resolve) => {
- client.unbind(() => {
- this.clients.delete(connectionId);
- this.isConnected.set(connectionId, false);
- resolve();
- });
- });
+ try {
+ client.unbind();
+ this.clients.delete(connectionId);
+ } catch (error) {
+ console.error(`Error unbinding legacy LDAP connection: ${error instanceof Error ? error.message : String(error)}`);
+ }
}
}
-
- getClient(connectionId: number): ldap.Client | undefined {
+
+ async getClient(connectionId: number): Promise {
+ // First check if we have a connection in the pool
+ const pool = this.connectionPools.get(connectionId) || [];
+ const availableEntry = pool.find(entry => !entry.inUse);
+
+ if (availableEntry) {
+ availableEntry.inUse = true;
+ availableEntry.lastUsed = Date.now();
+ return availableEntry.client;
+ }
+
+ // Fall back to legacy client
return this.clients.get(connectionId);
}
-
+
isConnectionActive(connectionId: number): boolean {
+ // Check if we have any active connections in the pool
+ const pool = this.connectionPools.get(connectionId) || [];
+ if (pool.length > 0) return true;
+
+ // Fall back to legacy connection status
return this.isConnected.get(connectionId) || false;
}
-
- // LDAP CRUD operations
- async searchUsers(connectionId: number, filter = '(objectClass=user)', attributes?: string[]): Promise {
- const client = this.getClient(connectionId);
+
+ // LDAP CRUD operations with paging support
+ async searchWithPaging(
+ connectionId: number,
+ baseDN: string,
+ filter: string,
+ attributes: string[],
+ pageSize = 1000
+ ): Promise {
+ const client = await this.getClient(connectionId);
if (!client) throw new Error('LDAP connection not established');
-
- const connection = await storage.getLdapConnection(connectionId);
- if (!connection) throw new Error('LDAP connection not found');
-
- const baseDN = connection.baseDN || '';
- const defaultAttributes = ['cn', 'sAMAccountName', 'mail', 'distinguishedName'];
- const searchAttributes = attributes?.length ? attributes : defaultAttributes;
-
+
return new Promise((resolve, reject) => {
- const results: any[] = [];
-
+ const allResults: any[] = [];
+ let finished = false;
+
+ // Create a paged search control
+ const controls = [
+ new ldap.PagedResultsControl({ value: { size: pageSize } })
+ ];
+
+ const processPage = (err: any, res: any) => {
+ if (err) {
+ reject(new Error(`LDAP search error: ${err.message}`));
+ return;
+ }
+
+ let cookie: Buffer | undefined;
+
+ res.on('searchEntry', (entry: any) => {
+ allResults.push(entry.object);
+ });
+
+ res.on('error', (err: any) => {
+ reject(new Error(`LDAP search error: ${err.message}`));
+ });
+
+ res.on('end', (result: any) => {
+ if (finished) return;
+
+ // Check if we need to get more pages
+ if (result && result.controls) {
+ const control = result.controls.find(
+ (c: any) => c instanceof ldap.PagedResultsControl
+ );
+
+ if (control && control.value.cookie && control.value.cookie.length > 0) {
+ cookie = control.value.cookie;
+
+ // Get the next page
+ const nextControls = [
+ new ldap.PagedResultsControl({ value: { size: pageSize, cookie } })
+ ];
+
+ client.search(baseDN, {
+ filter,
+ scope: 'sub',
+ attributes,
+ controls: nextControls
+ }, processPage);
+ } else {
+ // No more pages, we're done
+ finished = true;
+ resolve(allResults);
+ }
+ } else {
+ // No paging control in response, we're done
+ finished = true;
+ resolve(allResults);
+ }
+ });
+ };
+
+ // Start the paged search
client.search(baseDN, {
filter,
scope: 'sub',
- attributes: searchAttributes
- }, (err, res) => {
- if (err) {
- reject(err);
- return;
- }
-
- res.on('searchEntry', (entry) => {
- results.push(entry.object);
- });
-
- res.on('error', (err) => {
- reject(err);
- });
-
- res.on('end', (result) => {
- resolve(results);
- });
- });
+ attributes,
+ controls
+ }, processPage);
});
}
-
- async searchGroups(connectionId: number, filter = '(objectClass=group)', attributes?: string[]): Promise {
- const defaultAttributes = ['cn', 'distinguishedName', 'member'];
- return this.searchUsers(connectionId, filter, attributes || defaultAttributes);
- }
-
- async searchOUs(connectionId: number, filter = '(objectClass=organizationalUnit)', attributes?: string[]): Promise {
- const defaultAttributes = ['ou', 'distinguishedName'];
- return this.searchUsers(connectionId, filter, attributes || defaultAttributes);
- }
-
- async searchComputers(connectionId: number, filter = '(objectClass=computer)', attributes?: string[]): Promise {
- const defaultAttributes = ['cn', 'distinguishedName', 'operatingSystem'];
- return this.searchUsers(connectionId, filter, attributes || defaultAttributes);
- }
-
- async searchSites(connectionId: number, filter = '(objectClass=site)', attributes?: string[]): Promise {
- const defaultAttributes = ['cn', 'distinguishedName', 'description', 'location', 'managedBy'];
- // Sites are typically stored in the Configuration naming context
- const client = this.getClient(connectionId);
- if (!client) throw new Error('LDAP connection not established');
-
+
+ async searchUsers(connectionId: number, filter = '(objectClass=user)', attributes?: string[], pageSize = 1000): Promise {
const connection = await storage.getLdapConnection(connectionId);
if (!connection) throw new Error('LDAP connection not found');
-
+
+ const baseDN = connection.baseDN || '';
+ const defaultAttributes = ['cn', 'sAMAccountName', 'mail', 'distinguishedName', 'objectGUID', 'userAccountControl'];
+ const searchAttributes = attributes?.length ? attributes : defaultAttributes;
+
+ try {
+ return await this.searchWithPaging(connectionId, baseDN, filter, searchAttributes, pageSize);
+ } catch (error) {
+ throw new Error(`Failed to search users: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+
+ async searchGroups(connectionId: number, filter = '(objectClass=group)', attributes?: string[], pageSize = 1000): Promise {
+ const connection = await storage.getLdapConnection(connectionId);
+ if (!connection) throw new Error('LDAP connection not found');
+
+ const baseDN = connection.baseDN || '';
+ const defaultAttributes = ['cn', 'distinguishedName', 'member', 'objectGUID', 'sAMAccountName'];
+ const searchAttributes = attributes?.length ? attributes : defaultAttributes;
+
+ try {
+ return await this.searchWithPaging(connectionId, baseDN, filter, searchAttributes, pageSize);
+ } catch (error) {
+ throw new Error(`Failed to search groups: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+
+ async searchOUs(connectionId: number, filter = '(objectClass=organizationalUnit)', attributes?: string[], pageSize = 1000): Promise {
+ const connection = await storage.getLdapConnection(connectionId);
+ if (!connection) throw new Error('LDAP connection not found');
+
+ const baseDN = connection.baseDN || '';
+ const defaultAttributes = ['ou', 'distinguishedName', 'objectGUID'];
+ const searchAttributes = attributes?.length ? attributes : defaultAttributes;
+
+ try {
+ return await this.searchWithPaging(connectionId, baseDN, filter, searchAttributes, pageSize);
+ } catch (error) {
+ throw new Error(`Failed to search OUs: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+
+ async searchComputers(connectionId: number, filter = '(objectClass=computer)', attributes?: string[], pageSize = 1000): Promise {
+ const connection = await storage.getLdapConnection(connectionId);
+ if (!connection) throw new Error('LDAP connection not found');
+
+ const baseDN = connection.baseDN || '';
+ const defaultAttributes = ['cn', 'distinguishedName', 'dNSHostName', 'operatingSystem', 'objectGUID', 'userAccountControl'];
+ const searchAttributes = attributes?.length ? attributes : defaultAttributes;
+
+ try {
+ return await this.searchWithPaging(connectionId, baseDN, filter, searchAttributes, pageSize);
+ } catch (error) {
+ throw new Error(`Failed to search computers: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+
+ async searchSites(connectionId: number, filter = '(objectClass=site)', attributes?: string[], pageSize = 1000): Promise {
+ const defaultAttributes = ['cn', 'distinguishedName', 'description', 'location', 'managedBy', 'objectGUID'];
+
+ const connection = await storage.getLdapConnection(connectionId);
+ if (!connection) throw new Error('LDAP connection not found');
+
// Sites are located in the Configuration container
const baseDN = 'CN=Sites,CN=Configuration,' + this.getDomainDN(connection);
const searchAttributes = attributes?.length ? attributes : defaultAttributes;
-
- return new Promise((resolve, reject) => {
- const results: any[] = [];
-
- client.search(baseDN, {
- filter,
- scope: 'sub',
- attributes: searchAttributes
- }, (err, res) => {
- if (err) {
- reject(err);
- return;
- }
-
- res.on('searchEntry', (entry) => {
- results.push(entry.object);
- });
-
- res.on('error', (err) => {
- reject(err);
- });
-
- res.on('end', (result) => {
- resolve(results);
- });
- });
- });
+
+ try {
+ return await this.searchWithPaging(connectionId, baseDN, filter, searchAttributes, pageSize);
+ } catch (error) {
+ throw new Error(`Failed to search sites: ${error instanceof Error ? error.message : String(error)}`);
+ }
}
-
- async searchSubnets(connectionId: number, filter = '(objectClass=subnet)', attributes?: string[]): Promise {
- const defaultAttributes = ['cn', 'distinguishedName', 'description', 'location', 'siteObject', 'managedBy'];
- // Subnets are typically stored in the Configuration naming context
- const client = this.getClient(connectionId);
- if (!client) throw new Error('LDAP connection not established');
-
+
+ async searchSubnets(connectionId: number, filter = '(objectClass=subnet)', attributes?: string[], pageSize = 1000): Promise {
+ const defaultAttributes = ['cn', 'distinguishedName', 'description', 'location', 'siteObject', 'managedBy', 'objectGUID'];
+
const connection = await storage.getLdapConnection(connectionId);
if (!connection) throw new Error('LDAP connection not found');
-
+
// Subnets are located in the Configuration container
const baseDN = 'CN=Subnets,CN=Sites,CN=Configuration,' + this.getDomainDN(connection);
const searchAttributes = attributes?.length ? attributes : defaultAttributes;
-
- return new Promise((resolve, reject) => {
- const results: any[] = [];
-
- client.search(baseDN, {
- filter,
- scope: 'sub',
- attributes: searchAttributes
- }, (err, res) => {
- if (err) {
- reject(err);
- return;
- }
-
- res.on('searchEntry', (entry) => {
- results.push(entry.object);
- });
-
- res.on('error', (err) => {
- reject(err);
- });
-
- res.on('end', (result) => {
- resolve(results);
- });
- });
- });
+
+ try {
+ return await this.searchWithPaging(connectionId, baseDN, filter, searchAttributes, pageSize);
+ } catch (error) {
+ throw new Error(`Failed to search subnets: ${error instanceof Error ? error.message : String(error)}`);
+ }
}
-
+
// Helper function to extract domain DN from connection
getDomainDN(connection: LdapConnection): string {
const domainParts = connection.domain.split('.');
return domainParts.map(part => `DC=${part}`).join(',');
}
-
+
async createEntry(connectionId: number, dn: string, attributes: any): Promise {
const client = this.getClient(connectionId);
if (!client) throw new Error('LDAP connection not established');
-
+
return new Promise((resolve, reject) => {
client.add(dn, attributes, (err) => {
if (err) {
@@ -254,11 +542,11 @@ class LdapClient extends EventEmitter {
});
});
}
-
+
async updateEntry(connectionId: number, dn: string, changes: any[]): Promise {
const client = this.getClient(connectionId);
if (!client) throw new Error('LDAP connection not established');
-
+
return new Promise((resolve, reject) => {
client.modify(dn, changes, (err) => {
if (err) {
@@ -269,11 +557,95 @@ class LdapClient extends EventEmitter {
});
});
}
-
+
+ // Update a single attribute of an LDAP entry
+ async updateAttribute(
+ connectionId: number,
+ dn: string,
+ attributeName: string,
+ attributeValue: string | string[] | null,
+ operation: 'add' | 'replace' | 'delete' = 'replace'
+ ): Promise {
+ const client = this.getClient(connectionId);
+ if (!client) throw new Error('LDAP connection not established');
+
+ // Ensure DN is properly escaped
+ const escapedDN = buildDN(parseDN(dn));
+
+ let change: any;
+
+ if (attributeValue === null) {
+ // If value is null, we're removing the attribute
+ change = new ldap.Change({
+ operation: 'delete',
+ modification: {
+ [attributeName]: []
+ }
+ });
+ } else {
+ // Otherwise, we're adding or replacing the attribute
+ change = new ldap.Change({
+ operation,
+ modification: {
+ [attributeName]: Array.isArray(attributeValue) ? attributeValue : [attributeValue]
+ }
+ });
+ }
+
+ try {
+ return await this.updateEntry(connectionId, escapedDN, [change]);
+ } catch (error) {
+ throw new Error(`Failed to update attribute ${attributeName}: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+
+ // Update multiple attributes of an LDAP entry in a single operation
+ async updateAttributes(
+ connectionId: number,
+ dn: string,
+ attributes: Record,
+ operation: 'add' | 'replace' | 'delete' = 'replace'
+ ): Promise {
+ const client = this.getClient(connectionId);
+ if (!client) throw new Error('LDAP connection not established');
+
+ // Ensure DN is properly escaped
+ const escapedDN = buildDN(parseDN(dn));
+
+ const changes: any[] = [];
+
+ // Create a change for each attribute
+ for (const [attributeName, attributeValue] of Object.entries(attributes)) {
+ if (attributeValue === null) {
+ // If value is null, we're removing the attribute
+ changes.push(new ldap.Change({
+ operation: 'delete',
+ modification: {
+ [attributeName]: []
+ }
+ }));
+ } else {
+ // Otherwise, we're adding or replacing the attribute
+ changes.push(new ldap.Change({
+ operation,
+ modification: {
+ [attributeName]: Array.isArray(attributeValue) ? attributeValue : [attributeValue]
+ }
+ }));
+ }
+ }
+
+ try {
+ return await this.updateEntry(connectionId, escapedDN, changes);
+ } catch (error) {
+ throw new Error(`Failed to update attributes: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+
async deleteEntry(connectionId: number, dn: string): Promise {
const client = this.getClient(connectionId);
if (!client) throw new Error('LDAP connection not established');
-
+
return new Promise((resolve, reject) => {
client.del(dn, (err) => {
if (err) {
@@ -289,7 +661,7 @@ class LdapClient extends EventEmitter {
async getManagedBy(connectionId: number, dn: string): Promise {
const client = this.getClient(connectionId);
if (!client) throw new Error('LDAP connection not established');
-
+
return new Promise((resolve, reject) => {
client.search(dn, {
scope: 'base',
@@ -299,9 +671,9 @@ class LdapClient extends EventEmitter {
reject(err);
return;
}
-
+
let managedBy: string | null = null;
-
+
res.on('searchEntry', (entry) => {
const attrs = entry.attributes;
for (const attr of attrs) {
@@ -310,11 +682,11 @@ class LdapClient extends EventEmitter {
}
}
});
-
+
res.on('error', (err) => {
reject(err);
});
-
+
res.on('end', (result) => {
if (result.status !== 0) {
reject(new Error(`LDAP search error: ${result.errorMessage}`));
@@ -325,14 +697,14 @@ class LdapClient extends EventEmitter {
});
});
}
-
+
// Set the managed by attribute for an object
async setManagedBy(connectionId: number, dn: string, managerDn: string | null): Promise {
const client = this.getClient(connectionId);
if (!client) throw new Error('LDAP connection not established');
-
+
const changes = [];
-
+
if (managerDn === null) {
// Remove the managedBy attribute
changes.push(new ldap.Change({
@@ -350,9 +722,608 @@ class LdapClient extends EventEmitter {
}
}));
}
-
+
return this.updateEntry(connectionId, dn, changes);
}
+
+ // Validate password against common AD password policies
+ private validatePassword(password: string): { valid: boolean; message?: string } {
+ if (!password) {
+ return { valid: false, message: 'Password cannot be empty' };
+ }
+
+ if (password.length < 8) {
+ return { valid: false, message: 'Password must be at least 8 characters long' };
+ }
+
+ // Check for complexity requirements (at least 3 of 4 character types)
+ let complexity = 0;
+ if (/[a-z]/.test(password)) complexity++; // lowercase
+ if (/[A-Z]/.test(password)) complexity++; // uppercase
+ if (/[0-9]/.test(password)) complexity++; // digits
+ if (/[^a-zA-Z0-9]/.test(password)) complexity++; // special chars
+
+ if (complexity < 3) {
+ return {
+ valid: false,
+ message: 'Password must contain at least 3 of the following: lowercase letters, uppercase letters, digits, and special characters'
+ };
+ }
+
+ // Check for common patterns
+ if (/(.)\1{2,}/.test(password)) {
+ return { valid: false, message: 'Password cannot contain repeated character sequences' };
+ }
+
+ return { valid: true };
+ }
+
+ // Reset a user's password
+ async resetUserPassword(connectionId: number, userDN: string, newPassword: string, skipValidation = false): Promise {
+ const client = this.getClient(connectionId);
+ if (!client) throw new Error('LDAP connection not established');
+
+ // Validate the password against common AD policies
+ if (!skipValidation) {
+ const validation = this.validatePassword(newPassword);
+ if (!validation.valid) {
+ throw new Error(`Password validation failed: ${validation.message}`);
+ }
+ }
+
+ // Ensure the DN is properly escaped
+ const escapedUserDN = buildDN(parseDN(userDN));
+
+ // Unicode password format for Active Directory
+ const unicodePassword = Buffer.from(`"${newPassword}"`, 'utf16le');
+
+ const changes = [
+ new ldap.Change({
+ operation: 'replace',
+ modification: {
+ unicodePwd: unicodePassword
+ }
+ })
+ ];
+
+ try {
+ return await this.updateEntry(connectionId, escapedUserDN, changes);
+ } catch (error) {
+ throw new Error(`Failed to reset password: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+
+ // Enable or disable a user account
+ async setUserAccountStatus(connectionId: number, userDN: string, enabled: boolean): Promise {
+ const client = this.getClient(connectionId);
+ if (!client) throw new Error('LDAP connection not established');
+
+ // Ensure the DN is properly escaped
+ const escapedUserDN = buildDN(parseDN(userDN));
+
+ // First, get the current userAccountControl value
+ const users = await this.searchUsers(connectionId, `(distinguishedName=${escapedUserDN})`, ['userAccountControl']);
+ if (!users || users.length === 0) {
+ throw new Error('User not found');
+ }
+
+ let userAccountControl = parseInt(users[0].userAccountControl ?? '0');
+
+ // The ACCOUNTDISABLE flag is 0x2 (2)
+ if (enabled) {
+ // Remove the ACCOUNTDISABLE flag
+ userAccountControl &= ~2;
+ } else {
+ // Add the ACCOUNTDISABLE flag
+ userAccountControl |= 2;
+ }
+
+ const changes = [
+ new ldap.Change({
+ operation: 'replace',
+ modification: {
+ userAccountControl: userAccountControl.toString()
+ }
+ })
+ ];
+
+ try {
+ return await this.updateEntry(connectionId, escapedUserDN, changes);
+ } catch (error) {
+ throw new Error(`Failed to ${enabled ? 'enable' : 'disable'} user account: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+
+ // Move an object to a different OU
+ async moveObject(connectionId: number, objectDN: string, targetOU: string): Promise {
+ const client = this.getClient(connectionId);
+ if (!client) throw new Error('LDAP connection not established');
+
+ // Extract the RDN (Relative Distinguished Name) from the DN
+ const dnParts = parseDN(objectDN);
+ if (!dnParts.length) throw new Error('Invalid distinguished name format');
+
+ // The first part is the RDN
+ const rdn = `${dnParts[0].attribute}=${escapeDN(dnParts[0].value)}`;
+
+ // Ensure the target OU is properly escaped
+ const escapedTargetOU = targetOU.split(',').map(part => {
+ const [attr, val] = part.split('=', 2);
+ return `${attr}=${escapeDN(val.trim())}`;
+ }).join(',');
+
+ return new Promise((resolve, reject) => {
+ client.modifyDN(objectDN, {
+ newRdn: rdn,
+ deleteOldRdn: true,
+ newSuperior: escapedTargetOU
+ }, (err) => {
+ if (err) {
+ reject(new Error(`Failed to move object: ${err.message}`));
+ return;
+ }
+ resolve(true);
+ });
+ });
+ }
+
+ // Check if a user is a member of a group (direct or nested)
+ async isUserMemberOfGroup(connectionId: number, userDN: string, groupDN: string, maxDepth = 10): Promise {
+ const client = this.getClient(connectionId);
+ if (!client) throw new Error('LDAP connection not established');
+
+ // Ensure DNs are properly escaped
+ const escapedUserDN = buildDN(parseDN(userDN));
+ const escapedGroupDN = buildDN(parseDN(groupDN));
+
+ // First check direct membership
+ try {
+ const groups = await this.searchGroups(
+ connectionId,
+ `(&(distinguishedName=${escapedGroupDN})(member=${escapedUserDN}))`,
+ ['distinguishedName']
+ );
+
+ if (groups && groups.length > 0) {
+ // User is directly in the group
+ return true;
+ }
+ } catch (error) {
+ console.warn(`Error checking direct group membership: ${error instanceof Error ? error.message : String(error)}`);
+ }
+
+ // If not a direct member, check nested groups
+ return this.checkNestedGroupMembership(connectionId, userDN, groupDN, 0, maxDepth);
+ }
+
+ // Recursively check nested group membership
+ private async checkNestedGroupMembership(
+ connectionId: number,
+ memberDN: string,
+ groupDN: string,
+ currentDepth: number,
+ maxDepth: number,
+ visitedGroups: Set = new Set()
+ ): Promise {
+ // Prevent infinite recursion
+ if (currentDepth >= maxDepth) return false;
+
+ // Prevent cycles
+ if (visitedGroups.has(groupDN)) return false;
+ visitedGroups.add(groupDN);
+
+ // Ensure DNs are properly escaped
+ const escapedMemberDN = buildDN(parseDN(memberDN));
+ const escapedGroupDN = buildDN(parseDN(groupDN));
+
+ try {
+ // Get all groups that the member is directly a member of
+ const memberGroups = await this.searchGroups(
+ connectionId,
+ `(member=${escapedMemberDN})`,
+ ['distinguishedName']
+ );
+
+ // For each of these groups, check if they are members of the target group
+ for (const memberGroup of memberGroups) {
+ // Check if this group is a direct member of the target group
+ const isDirectMember = await this.searchGroups(
+ connectionId,
+ `(&(distinguishedName=${escapedGroupDN})(member=${memberGroup.distinguishedName}))`,
+ ['distinguishedName']
+ );
+
+ if (isDirectMember && isDirectMember.length > 0) {
+ return true;
+ }
+
+ // Recursively check if this group is a nested member of the target group
+ const isNestedMember = await this.checkNestedGroupMembership(
+ connectionId,
+ memberGroup.distinguishedName,
+ groupDN,
+ currentDepth + 1,
+ maxDepth,
+ visitedGroups
+ );
+
+ if (isNestedMember) {
+ return true;
+ }
+ }
+ } catch (error) {
+ console.warn(`Error checking nested group membership: ${error instanceof Error ? error.message : String(error)}`);
+ }
+
+ return false;
+ }
+
+ // Get all groups a user is a member of (direct and nested)
+ async getUserGroups(connectionId: number, userDN: string, maxDepth = 10): Promise {
+ const client = this.getClient(connectionId);
+ if (!client) throw new Error('LDAP connection not established');
+
+ // Ensure DN is properly escaped
+ const escapedUserDN = buildDN(parseDN(userDN));
+
+ // Get direct group memberships
+ const directGroups = await this.searchGroups(
+ connectionId,
+ `(member=${escapedUserDN})`,
+ ['distinguishedName', 'cn', 'objectGUID', 'sAMAccountName']
+ );
+
+ // Set to track all groups to avoid duplicates
+ const allGroupDNs = new Set();
+ const allGroups: any[] = [];
+
+ // Add direct groups to the result
+ for (const group of directGroups) {
+ if (!allGroupDNs.has(group.distinguishedName)) {
+ allGroupDNs.add(group.distinguishedName);
+ allGroups.push({
+ ...group,
+ membershipType: 'direct'
+ });
+ }
+ }
+
+ // Get nested groups
+ const nestedGroups = await this.getNestedGroups(
+ connectionId,
+ directGroups.map(g => g.distinguishedName),
+ 1,
+ maxDepth,
+ allGroupDNs
+ );
+
+ // Add nested groups to the result
+ for (const group of nestedGroups) {
+ allGroups.push({
+ ...group,
+ membershipType: 'nested'
+ });
+ }
+
+ return allGroups;
+ }
+
+ // Recursively get nested groups
+ private async getNestedGroups(
+ connectionId: number,
+ groupDNs: string[],
+ currentDepth: number,
+ maxDepth: number,
+ visitedGroups: Set
+ ): Promise {
+ if (currentDepth >= maxDepth || groupDNs.length === 0) return [];
+
+ const nestedGroups: any[] = [];
+
+ for (const groupDN of groupDNs) {
+ // Ensure DN is properly escaped
+ const escapedGroupDN = buildDN(parseDN(groupDN));
+
+ // Get groups that this group is a member of
+ const parentGroups = await this.searchGroups(
+ connectionId,
+ `(member=${escapedGroupDN})`,
+ ['distinguishedName', 'cn', 'objectGUID', 'sAMAccountName']
+ );
+
+ // Filter out groups we've already seen
+ const newGroups = parentGroups.filter(g => !visitedGroups.has(g.distinguishedName));
+
+ // Add new groups to the result and visited set
+ for (const group of newGroups) {
+ visitedGroups.add(group.distinguishedName);
+ nestedGroups.push(group);
+ }
+
+ // Recursively get the next level of nested groups
+ if (newGroups.length > 0) {
+ const nextLevelGroups = await this.getNestedGroups(
+ connectionId,
+ newGroups.map(g => g.distinguishedName),
+ currentDepth + 1,
+ maxDepth,
+ visitedGroups
+ );
+
+ nestedGroups.push(...nextLevelGroups);
+ }
+ }
+
+ return nestedGroups;
+ }
+
+ // Add a member to a group
+ async addToGroup(connectionId: number, groupDN: string, memberDN: string): Promise {
+ const client = this.getClient(connectionId);
+ if (!client) throw new Error('LDAP connection not established');
+
+ // Ensure DNs are properly escaped
+ const escapedGroupDN = buildDN(parseDN(groupDN));
+ const escapedMemberDN = buildDN(parseDN(memberDN));
+
+ // First check if the member is already in the group to avoid errors
+ try {
+ const groups = await this.searchGroups(
+ connectionId,
+ `(&(distinguishedName=${escapedGroupDN})(member=${escapedMemberDN}))`,
+ ['distinguishedName']
+ );
+
+ if (groups && groups.length > 0) {
+ // Member is already in the group
+ return true;
+ }
+
+ // Also check for nested membership to warn about it
+ const isNestedMember = await this.isUserMemberOfGroup(connectionId, memberDN, groupDN);
+ if (isNestedMember) {
+ console.warn(`Member ${memberDN} is already a nested member of group ${groupDN}`);
+ // We still add the direct membership even if there's a nested membership
+ }
+ } catch (error) {
+ // Continue with the add operation even if the check fails
+ console.warn(`Error checking group membership: ${error instanceof Error ? error.message : String(error)}`);
+ }
+
+ const changes = [
+ new ldap.Change({
+ operation: 'add',
+ modification: {
+ member: escapedMemberDN
+ }
+ })
+ ];
+
+ try {
+ return await this.updateEntry(connectionId, escapedGroupDN, changes);
+ } catch (error) {
+ throw new Error(`Failed to add member to group: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+
+ // Remove a member from a group
+ async removeFromGroup(connectionId: number, groupDN: string, memberDN: string): Promise {
+ const client = this.getClient(connectionId);
+ if (!client) throw new Error('LDAP connection not established');
+
+ // Ensure DNs are properly escaped
+ const escapedGroupDN = buildDN(parseDN(groupDN));
+ const escapedMemberDN = buildDN(parseDN(memberDN));
+
+ // First check if the member is in the group to avoid errors
+ try {
+ const groups = await this.searchGroups(
+ connectionId,
+ `(&(distinguishedName=${escapedGroupDN})(member=${escapedMemberDN}))`,
+ ['distinguishedName']
+ );
+
+ if (!groups || groups.length === 0) {
+ // Member is not directly in the group
+
+ // Check if it's a nested member
+ const isNestedMember = await this.isUserMemberOfGroup(connectionId, memberDN, groupDN);
+ if (isNestedMember) {
+ throw new Error(`Cannot remove nested group membership. Member ${memberDN} is a nested member of group ${groupDN}, not a direct member.`);
+ }
+
+ // Not a member at all
+ return true;
+ }
+ } catch (error) {
+ // Only continue if it's not our specific nested membership error
+ if (error instanceof Error && error.message.includes('Cannot remove nested group membership')) {
+ throw error;
+ }
+
+ // Continue with the remove operation for other errors
+ console.warn(`Error checking group membership: ${error instanceof Error ? error.message : String(error)}`);
+ }
+
+ const changes = [
+ new ldap.Change({
+ operation: 'delete',
+ modification: {
+ member: escapedMemberDN
+ }
+ })
+ ];
+
+ try {
+ return await this.updateEntry(connectionId, escapedGroupDN, changes);
+ } catch (error) {
+ throw new Error(`Failed to remove member from group: ${error instanceof Error ? error.message : String(error)}`);
+ }
+ }
+
+ // Bulk Operations
+
+ // Move multiple objects to a target OU
+ async bulkMoveObjects(connectionId: number, objectDNs: string[], targetOU: string): Promise<{ success: string[]; failed: { dn: string; error: string }[] }> {
+ const results = {
+ success: [] as string[],
+ failed: [] as { dn: string; error: string }[]
+ };
+
+ for (const dn of objectDNs) {
+ try {
+ const success = await this.moveObject(connectionId, dn, targetOU);
+ if (success) {
+ results.success.push(dn);
+ } else {
+ results.failed.push({ dn, error: 'Unknown error' });
+ }
+ } catch (error) {
+ results.failed.push({
+ dn,
+ error: error instanceof Error ? error.message : String(error)
+ });
+ }
+ }
+
+ return results;
+ }
+
+ // Add multiple members to a group
+ async bulkAddToGroup(connectionId: number, groupDN: string, memberDNs: string[]): Promise<{ success: string[]; failed: { dn: string; error: string }[] }> {
+ const results = {
+ success: [] as string[],
+ failed: [] as { dn: string; error: string }[]
+ };
+
+ for (const dn of memberDNs) {
+ try {
+ const success = await this.addToGroup(connectionId, groupDN, dn);
+ if (success) {
+ results.success.push(dn);
+ } else {
+ results.failed.push({ dn, error: 'Unknown error' });
+ }
+ } catch (error) {
+ results.failed.push({
+ dn,
+ error: error instanceof Error ? error.message : String(error)
+ });
+ }
+ }
+
+ return results;
+ }
+
+ // Remove multiple members from a group
+ async bulkRemoveFromGroup(connectionId: number, groupDN: string, memberDNs: string[]): Promise<{ success: string[]; failed: { dn: string; error: string }[] }> {
+ const results = {
+ success: [] as string[],
+ failed: [] as { dn: string; error: string }[]
+ };
+
+ for (const dn of memberDNs) {
+ try {
+ const success = await this.removeFromGroup(connectionId, groupDN, dn);
+ if (success) {
+ results.success.push(dn);
+ } else {
+ results.failed.push({ dn, error: 'Unknown error' });
+ }
+ } catch (error) {
+ results.failed.push({
+ dn,
+ error: error instanceof Error ? error.message : String(error)
+ });
+ }
+ }
+
+ return results;
+ }
+
+ // Enable or disable multiple user accounts
+ async bulkSetUserAccountStatus(connectionId: number, userDNs: string[], enabled: boolean): Promise<{ success: string[]; failed: { dn: string; error: string }[] }> {
+ const results = {
+ success: [] as string[],
+ failed: [] as { dn: string; error: string }[]
+ };
+
+ for (const dn of userDNs) {
+ try {
+ const success = await this.setUserAccountStatus(connectionId, dn, enabled);
+ if (success) {
+ results.success.push(dn);
+ } else {
+ results.failed.push({ dn, error: 'Unknown error' });
+ }
+ } catch (error) {
+ results.failed.push({
+ dn,
+ error: error instanceof Error ? error.message : String(error)
+ });
+ }
+ }
+
+ return results;
+ }
+
+ // Update the same attribute for multiple objects
+ async bulkUpdateAttribute(
+ connectionId: number,
+ dns: string[],
+ attributeName: string,
+ attributeValue: string | string[] | null,
+ operation: 'add' | 'replace' | 'delete' = 'replace'
+ ): Promise<{ success: string[]; failed: { dn: string; error: string }[] }> {
+ const results = {
+ success: [] as string[],
+ failed: [] as { dn: string; error: string }[]
+ };
+
+ for (const dn of dns) {
+ try {
+ const success = await this.updateAttribute(connectionId, dn, attributeName, attributeValue, operation);
+ if (success) {
+ results.success.push(dn);
+ } else {
+ results.failed.push({ dn, error: 'Unknown error' });
+ }
+ } catch (error) {
+ results.failed.push({
+ dn,
+ error: error instanceof Error ? error.message : String(error)
+ });
+ }
+ }
+
+ return results;
+ }
+
+ // Helper method to handle LDAP errors with more detailed information
+ private handleLdapError(err: any): Error {
+ // Map common LDAP error codes to more user-friendly messages
+ const errorMap: Record = {
+ '32': 'No such object exists in the directory',
+ '49': 'Invalid credentials',
+ '50': 'Insufficient access rights',
+ '53': 'Unable to perform operation, directory server unwilling to process request',
+ '65': 'Object class violation',
+ '68': 'Entry already exists',
+ '69': 'Object class modifications prohibited',
+ '71': 'Affects multiple DSAs (directory server agents)',
+ '80': 'Other error'
+ };
+
+ const code = err.code?.toString();
+ const message = errorMap[code] || err.message || 'Unknown LDAP error';
+
+ // Create a more detailed error
+ const error = new Error(`LDAP Error ${code}: ${message}`);
+ (error as any).ldapCode = code;
+ (error as any).originalError = err;
+
+ return error;
+ }
}
export const ldapClient = new LdapClient();
diff --git a/server/routes.ts b/server/routes.ts
index 9554589..899e0af 100644
--- a/server/routes.ts
+++ b/server/routes.ts
@@ -4,13 +4,20 @@ import { setupAuth } from "./auth";
import { setupSwagger } from "./swagger";
import { storage } from "./storage";
import { db } from "./db";
-import {
- apiQuerySchema,
- PERMISSIONS,
- moveComputerSchema,
- moveUserSchema,
- addToGroupSchema,
+import {
+ apiQuerySchema,
+ PERMISSIONS,
+ moveComputerSchema,
+ moveUserSchema,
+ addToGroupSchema,
removeFromGroupSchema,
+ resetPasswordSchema,
+ enableUserAccountSchema,
+ bulkMoveObjectsSchema,
+ bulkAddToGroupSchema,
+ bulkRemoveFromGroupSchema,
+ bulkEnableUserAccountsSchema,
+ bulkUpdateAttributeSchema,
adUsers,
adGroups,
adOrgUnits,
@@ -21,19 +28,19 @@ import {
} from "@shared/schema";
import { ZodError } from "zod";
import rateLimit from "express-rate-limit";
-import {
- requireAuth,
- requirePermission,
- requireAdmin,
+import {
+ requireAuth,
+ requirePermission,
+ requireAdmin,
initializeRBAC,
checkPermission
} from "./authorization";
import { ldapClient } from "./ldap";
-import {
- applyFilterConditions,
- generatePaginationMetadata,
- parseFilter,
- parsePagination
+import {
+ applyFilterConditions,
+ generatePaginationMetadata,
+ parseFilter,
+ parsePagination
} from "./query-parser";
import { eq, sql, count } from "drizzle-orm";
@@ -67,12 +74,12 @@ export async function registerRoutes(app: Express): Promise {
// 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,
@@ -86,7 +93,7 @@ export async function registerRoutes(app: Express): Promise {
registrationEnabled: process.env.DISABLE_REGISTRATION !== "true"
});
});
-
+
// Apply rate limiting middleware for API routes
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
@@ -153,13 +160,13 @@ export async function registerRoutes(app: Express): Promise {
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);
@@ -218,10 +225,10 @@ export async function registerRoutes(app: Express): Promise {
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);
@@ -259,16 +266,16 @@ export async function registerRoutes(app: Express): Promise {
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);
@@ -327,14 +334,14 @@ export async function registerRoutes(app: Express): Promise {
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);
@@ -366,11 +373,11 @@ export async function registerRoutes(app: Express): Promise {
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);
@@ -383,7 +390,7 @@ export async function registerRoutes(app: Express): Promise {
if (!req.isAuthenticated()) {
return res.status(401).json({ message: "Not authenticated" });
}
-
+
const tokens = await storage.listApiTokensByUserId(req.user.id);
res.json(tokens);
} catch (error) {
@@ -397,29 +404,29 @@ export async function registerRoutes(app: Express): Promise {
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);
@@ -430,13 +437,13 @@ export async function registerRoutes(app: Express): Promise {
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);
@@ -486,21 +493,21 @@ export async function registerRoutes(app: Express): Promise {
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);
@@ -509,16 +516,16 @@ export async function registerRoutes(app: Express): Promise {
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(
@@ -527,7 +534,7 @@ export async function registerRoutes(app: Express): Promise {
offset,
`${req.protocol}://${req.get('host')}${req.originalUrl}`
);
-
+
// Return data with pagination metadata
res.json({
data: usersWithObjectType,
@@ -598,34 +605,34 @@ export async function registerRoutes(app: Express): Promise {
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'],
@@ -638,30 +645,30 @@ export async function registerRoutes(app: Express): Promise {
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,
@@ -681,7 +688,7 @@ export async function registerRoutes(app: Express): Promise {
managedBy: null,
adProperties: userData
});
-
+
// Create audit log entry
await storage.createAuditLogEntry({
action: 'create',
@@ -690,7 +697,7 @@ export async function registerRoutes(app: Express): Promise {
userId: req.user?.id,
connectionId
});
-
+
res.status(201).json(newUser);
} catch (err) {
console.error("Error creating AD user:", err);
@@ -739,13 +746,13 @@ export async function registerRoutes(app: Express): Promise {
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) {
@@ -758,7 +765,7 @@ export async function registerRoutes(app: Express): Promise {
});
result = selectedUser as typeof user;
}
-
+
res.json(result);
} catch (error) {
next(error);
@@ -851,7 +858,7 @@ export async function registerRoutes(app: Express): Promise {
* schema:
* type: object
* properties:
- * givenName:
+ * givenName:
* type: string
* surname:
* type: string
@@ -880,18 +887,18 @@ export async function registerRoutes(app: Express): Promise {
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:
@@ -940,19 +947,19 @@ export async function registerRoutes(app: Express): Promise {
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);
@@ -960,14 +967,14 @@ export async function registerRoutes(app: Express): Promise {
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",
@@ -980,7 +987,7 @@ export async function registerRoutes(app: Express): Promise {
userId: req.user?.id,
connectionId
});
-
+
return res.status(200).json(updatedUser);
} catch (error) {
console.error("Error updating managedBy attribute:", error);
@@ -1022,17 +1029,17 @@ export async function registerRoutes(app: Express): Promise {
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);
@@ -1101,18 +1108,18 @@ export async function registerRoutes(app: Express): Promise {
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:
@@ -1161,19 +1168,19 @@ export async function registerRoutes(app: Express): Promise {
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);
@@ -1181,14 +1188,14 @@ export async function registerRoutes(app: Express): Promise {
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",
@@ -1201,7 +1208,7 @@ export async function registerRoutes(app: Express): Promise {
userId: req.user?.id,
connectionId
});
-
+
return res.status(200).json(updatedGroup);
} catch (error) {
console.error("Error updating managedBy attribute:", error);
@@ -1243,23 +1250,23 @@ export async function registerRoutes(app: Express): Promise {
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
@@ -1306,21 +1313,21 @@ export async function registerRoutes(app: Express): Promise {
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);
@@ -1329,16 +1336,16 @@ export async function registerRoutes(app: Express): Promise {
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(
@@ -1347,7 +1354,7 @@ export async function registerRoutes(app: Express): Promise {
offset,
`${req.protocol}://${req.get('host')}${req.originalUrl}`
);
-
+
// Return data with pagination metadata
res.json({
data: groupsWithObjectType,
@@ -1357,7 +1364,7 @@ export async function registerRoutes(app: Express): Promise {
next(error);
}
});
-
+
/**
* @swagger
* /api/connections/{connectionId}/ad-groups:
@@ -1419,7 +1426,7 @@ export async function registerRoutes(app: Express): Promise {
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" });
@@ -1430,15 +1437,15 @@ export async function registerRoutes(app: Express): Promise {
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) |
+ const groupTypeValue =
+ (groupCategory === 'Security' ? 0x80000000 : 0) |
(groupType === 'Global' ? 0x2 : groupType === 'Universal' ? 0x8 : 0x4);
-
+
// Attributes for the new group
const groupAttributes = {
objectClass: ['top', 'group'],
@@ -1447,21 +1454,21 @@ export async function registerRoutes(app: Express): Promise {
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,
@@ -1475,7 +1482,7 @@ export async function registerRoutes(app: Express): Promise {
members: [], // No members initially
adProperties: adGroup // Store all attributes
});
-
+
// Add audit log
await storage.createAuditLogEntry({
userId: req.user.id,
@@ -1489,7 +1496,7 @@ export async function registerRoutes(app: Express): Promise {
groupType: groupCategory + ' ' + groupType
}
});
-
+
res.status(201).json(storedGroup);
} catch (err) {
console.error("Error creating AD group:", err);
@@ -1554,18 +1561,18 @@ export async function registerRoutes(app: Express): Promise {
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:
@@ -1614,19 +1621,19 @@ export async function registerRoutes(app: Express): Promise {
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);
@@ -1634,14 +1641,14 @@ export async function registerRoutes(app: Express): Promise {
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",
@@ -1654,7 +1661,7 @@ export async function registerRoutes(app: Express): Promise {
userId: req.user?.id,
connectionId
});
-
+
return res.status(200).json(updatedOU);
} catch (error) {
console.error("Error updating managedBy attribute:", error);
@@ -1696,23 +1703,23 @@ export async function registerRoutes(app: Express): Promise {
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
@@ -1759,21 +1766,21 @@ export async function registerRoutes(app: Express): Promise {
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);
@@ -1782,16 +1789,16 @@ export async function registerRoutes(app: Express): Promise {
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(
@@ -1800,7 +1807,7 @@ export async function registerRoutes(app: Express): Promise {
offset,
`${req.protocol}://${req.get('host')}${req.originalUrl}`
);
-
+
// Return data with pagination metadata
res.json({
data: orgUnitsWithObjectType,
@@ -1871,18 +1878,18 @@ export async function registerRoutes(app: Express): Promise {
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:
@@ -1931,19 +1938,19 @@ export async function registerRoutes(app: Express): Promise {
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);
@@ -1951,14 +1958,14 @@ export async function registerRoutes(app: Express): Promise {
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",
@@ -1971,7 +1978,7 @@ export async function registerRoutes(app: Express): Promise {
userId: req.user?.id,
connectionId
});
-
+
return res.status(200).json(updatedComputer);
} catch (error) {
console.error("Error updating managedBy attribute:", error);
@@ -2010,7 +2017,7 @@ export async function registerRoutes(app: Express): Promise {
* 404:
* $ref: '#/components/responses/NotFoundError'
*/
-
+
/**
* @swagger
* /api/connections/{connectionId}/ad-ous:
@@ -2057,7 +2064,7 @@ export async function registerRoutes(app: Express): Promise {
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" });
@@ -2068,31 +2075,31 @@ export async function registerRoutes(app: Express): Promise {
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,
@@ -2103,7 +2110,7 @@ export async function registerRoutes(app: Express): Promise {
description: adOU.description,
adProperties: adOU // Store all attributes
});
-
+
// Add audit log
await storage.createAuditLogEntry({
userId: req.user.id,
@@ -2117,7 +2124,7 @@ export async function registerRoutes(app: Express): Promise {
description: description
}
});
-
+
res.status(201).json(storedOU);
} catch (err) {
console.error("Error creating AD organizational unit:", err);
@@ -2130,23 +2137,23 @@ export async function registerRoutes(app: Express): Promise {
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
@@ -2193,21 +2200,21 @@ export async function registerRoutes(app: Express): Promise {
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);
@@ -2216,16 +2223,16 @@ export async function registerRoutes(app: Express): Promise {
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(
@@ -2234,7 +2241,7 @@ export async function registerRoutes(app: Express): Promise {
offset,
`${req.protocol}://${req.get('host')}${req.originalUrl}`
);
-
+
// Return data with pagination metadata
res.json({
data: computersWithObjectType,
@@ -2244,7 +2251,7 @@ export async function registerRoutes(app: Express): Promise {
next(error);
}
});
-
+
/**
* @swagger
* /api/connections/{connectionId}/ad-computers:
@@ -2308,16 +2315,16 @@ export async function registerRoutes(app: Express): Promise {
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
+ 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" });
@@ -2328,21 +2335,21 @@ export async function registerRoutes(app: Express): Promise {
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[];
@@ -2361,31 +2368,31 @@ export async function registerRoutes(app: Express): Promise {
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,
@@ -2401,7 +2408,7 @@ export async function registerRoutes(app: Express): Promise {
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,
@@ -2414,7 +2421,7 @@ export async function registerRoutes(app: Express): Promise {
name: name
}
});
-
+
res.status(201).json(storedComputer);
} catch (err) {
console.error("Error creating AD computer:", err);
@@ -2481,11 +2488,11 @@ export async function registerRoutes(app: Express): Promise {
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) {
@@ -2524,23 +2531,23 @@ export async function registerRoutes(app: Express): Promise {
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
@@ -2582,7 +2589,7 @@ export async function registerRoutes(app: Express): Promise {
* 404:
* $ref: '#/components/responses/NotFoundError'
*/
-
+
/**
* @swagger
* /api/connections/{connectionId}/ad-domains:
@@ -2638,21 +2645,21 @@ export async function registerRoutes(app: Express): Promise {
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);
@@ -2661,16 +2668,16 @@ export async function registerRoutes(app: Express): Promise {
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(
@@ -2679,7 +2686,7 @@ export async function registerRoutes(app: Express): Promise {
offset,
`${req.protocol}://${req.get('host')}${req.originalUrl}`
);
-
+
// Return data with pagination metadata
res.json({
data: domainsWithObjectType,
@@ -2689,7 +2696,7 @@ export async function registerRoutes(app: Express): Promise {
next(error);
}
});
-
+
/**
* @swagger
* /api/connections/{connectionId}/ad-domains/{objectGUID}:
@@ -2739,7 +2746,7 @@ export async function registerRoutes(app: Express): Promise {
* 500:
* description: Server error
*/
-
+
/**
* @swagger
* /api/connections/{connectionId}/ad-domains/{objectGUID}:
@@ -2774,7 +2781,7 @@ export async function registerRoutes(app: Express): Promise {
* 500:
* description: Server error
*/
-
+
// Domain PATCH endpoint
app.patch("/api/connections/:connectionId/ad-domains/:objectGUID", authenticateApiToken, async (req, res, next) => {
try {
@@ -2783,51 +2790,51 @@ export async function registerRoutes(app: Express): Promise {
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",
@@ -2840,7 +2847,7 @@ export async function registerRoutes(app: Express): Promise {
changes: changes
}
});
-
+
res.json({
...updatedDomain,
objectType: 'domain'
@@ -2862,44 +2869,44 @@ export async function registerRoutes(app: Express): Promise {
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."
+ 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",
@@ -2911,7 +2918,7 @@ export async function registerRoutes(app: Express): Promise {
distinguishedName: domain.distinguishedName
}
});
-
+
res.json({ success: true });
} catch (err) {
console.error("Error deleting domain:", err);
@@ -2929,52 +2936,52 @@ export async function registerRoutes(app: Express): Promise {
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,
@@ -2986,9 +2993,9 @@ export async function registerRoutes(app: Express): Promise {
forestName: req.body.forestName,
adProperties: domainEntry
};
-
+
const createdDomain = await storage.createAdDomain(domainData);
-
+
// Add audit log
await storage.createAuditLogEntry({
action: "create_domain",
@@ -3000,7 +3007,7 @@ export async function registerRoutes(app: Express): Promise {
distinguishedName: req.body.distinguishedName
}
});
-
+
res.status(201).json({
...createdDomain,
objectType: 'domain'
@@ -3011,7 +3018,7 @@ export async function registerRoutes(app: Express): Promise {
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) {
@@ -3057,24 +3064,24 @@ export async function registerRoutes(app: Express): Promise {
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:
@@ -3129,24 +3136,24 @@ export async function registerRoutes(app: Express): Promise {
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}:
@@ -3196,17 +3203,17 @@ export async function registerRoutes(app: Express): Promise {
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}:
@@ -3233,17 +3240,17 @@ export async function registerRoutes(app: Express): Promise {
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:
@@ -3321,17 +3328,17 @@ export async function registerRoutes(app: Express): Promise {
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);
@@ -3343,20 +3350,20 @@ export async function registerRoutes(app: Express): Promise {
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:
@@ -3409,26 +3416,26 @@ export async function registerRoutes(app: Express): Promise {
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}:
@@ -3459,17 +3466,17 @@ export async function registerRoutes(app: Express): Promise {
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}:
@@ -3518,25 +3525,25 @@ export async function registerRoutes(app: Express): Promise {
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}:
@@ -3562,13 +3569,13 @@ export async function registerRoutes(app: Express): Promise {
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!);
@@ -3576,14 +3583,14 @@ export async function registerRoutes(app: Express): Promise {
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:
@@ -3615,20 +3622,20 @@ export async function registerRoutes(app: Express): Promise {
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}:
@@ -3664,13 +3671,13 @@ export async function registerRoutes(app: Express): Promise {
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!);
@@ -3678,23 +3685,23 @@ export async function registerRoutes(app: Express): Promise {
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:
@@ -3742,17 +3749,17 @@ export async function registerRoutes(app: Express): Promise {
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) {
@@ -3812,32 +3819,51 @@ export async function registerRoutes(app: Express): Promise {
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
-
+
+ // Connect to LDAP if not already connected
+ if (!ldapClient.isConnectionActive(connectionId)) {
+ await ldapClient.connect(connection);
+ }
+
+ // Find the computer by GUID to get the distinguishedName
+ const computer = await storage.getAdComputerByObjectGUID(connectionId, data.computerObjectGUID);
+ if (!computer) {
+ return res.status(404).json({ message: "Computer not found" });
+ }
+
+ // Move the computer to the new OU
+ const success = await ldapClient.moveObject(
+ connectionId,
+ computer.distinguishedName,
+ data.targetOUDistinguishedName
+ );
+
+ if (!success) {
+ return res.status(500).json({ message: "Failed to move computer" });
+ }
+
// Record the action in the audit log
const auditEntry = {
action: "MOVE_COMPUTER",
targetId: data.computerObjectGUID,
details: {
- targetOU: data.targetOUDistinguishedName
+ targetOU: data.targetOUDistinguishedName,
+ sourceDN: computer.distinguishedName
},
- userId: req.user?.id || null,
+ 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}`
@@ -3905,32 +3931,51 @@ export async function registerRoutes(app: Express): Promise {
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
-
+
+ // Connect to LDAP if not already connected
+ if (!ldapClient.isConnectionActive(connectionId)) {
+ await ldapClient.connect(connection);
+ }
+
+ // Find the user by GUID to get the distinguishedName
+ const users = await storage.getAdUserByObjectGUID(connectionId, data.userObjectGUID);
+ if (!users) {
+ return res.status(404).json({ message: "User not found" });
+ }
+
+ // Move the user to the new OU
+ const success = await ldapClient.moveObject(
+ connectionId,
+ users.distinguishedName,
+ data.targetOUDistinguishedName
+ );
+
+ if (!success) {
+ return res.status(500).json({ message: "Failed to move user" });
+ }
+
// Record the action in the audit log
const auditEntry = {
action: "MOVE_USER",
targetId: data.userObjectGUID,
details: {
- targetOU: data.targetOUDistinguishedName
+ targetOU: data.targetOUDistinguishedName,
+ sourceDN: users.distinguishedName
},
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}`
@@ -4002,33 +4047,71 @@ export async function registerRoutes(app: Express): Promise {
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
-
+
+ // Connect to LDAP if not already connected
+ if (!ldapClient.isConnectionActive(connectionId)) {
+ await ldapClient.connect(connection);
+ }
+
+ // Find the group by GUID to get the distinguishedName
+ const group = await storage.getAdGroupByObjectGUID(connectionId, data.groupObjectGUID);
+ if (!group) {
+ return res.status(404).json({ message: "Group not found" });
+ }
+
+ // Find the object by GUID to get the distinguishedName
+ let objectDN = '';
+ if (data.objectType === 'user') {
+ const user = await storage.getAdUserByObjectGUID(connectionId, data.objectGUID);
+ if (!user) {
+ return res.status(404).json({ message: "User not found" });
+ }
+ objectDN = user.distinguishedName;
+ } else if (data.objectType === 'computer') {
+ const computer = await storage.getAdComputerByObjectGUID(connectionId, data.objectGUID);
+ if (!computer) {
+ return res.status(404).json({ message: "Computer not found" });
+ }
+ objectDN = computer.distinguishedName;
+ } else {
+ return res.status(400).json({ message: "Invalid object type" });
+ }
+
+ // Add the object to the group
+ const success = await ldapClient.addToGroup(
+ connectionId,
+ group.distinguishedName,
+ objectDN
+ );
+
+ if (!success) {
+ return res.status(500).json({ message: `Failed to add ${data.objectType} to group` });
+ }
+
// Record the action in the audit log
const auditEntry = {
action: "ADD_TO_GROUP",
targetId: data.objectGUID,
details: {
groupGUID: data.groupObjectGUID,
- objectType: data.objectType
+ objectType: data.objectType,
+ objectDN: objectDN,
+ groupDN: group.distinguishedName
},
- userId: req.user?.id || null,
+ 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}`
@@ -4136,21 +4219,21 @@ export async function registerRoutes(app: Express): Promise {
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);
@@ -4159,16 +4242,16 @@ export async function registerRoutes(app: Express): Promise {
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(
@@ -4177,7 +4260,7 @@ export async function registerRoutes(app: Express): Promise {
offset,
`${req.protocol}://${req.get('host')}${req.originalUrl}`
);
-
+
res.json({
data: sitesWithObjectType,
metadata: paginationMetadata
@@ -4224,22 +4307,22 @@ export async function registerRoutes(app: Express): Promise {
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);
@@ -4291,32 +4374,32 @@ export async function registerRoutes(app: Express): Promise {
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'],
@@ -4324,23 +4407,23 @@ export async function registerRoutes(app: Express): Promise {
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,
@@ -4354,7 +4437,7 @@ export async function registerRoutes(app: Express): Promise {
managedBy: null,
adProperties: siteData
});
-
+
// Create audit log entry
await storage.createAuditLogEntry({
action: 'create',
@@ -4363,7 +4446,7 @@ export async function registerRoutes(app: Express): Promise {
userId: req.user?.id,
connectionId
});
-
+
res.status(201).json(newSite);
} catch (err) {
console.error("Error creating AD site:", err);
@@ -4424,36 +4507,36 @@ export async function registerRoutes(app: Express): Promise {
if (!req.isAuthenticated() && !req.headers.authorization) {
return res.status(401).json({ message: "Authentication required" });
}
-
+
const connectionId = parseInt(req.params.connectionId);
const objectGUID = req.params.objectGUID;
-
+
const site = await storage.getAdSiteByObjectGUID(connectionId, objectGUID);
-
+
if (!site) {
return res.status(404).json({ message: "AD site not found" });
}
-
+
const connection = await storage.getLdapConnection(connectionId);
-
+
if (!connection) {
return res.status(404).json({ message: "LDAP connection not found" });
}
-
+
// Connect to LDAP if needed
const client = ldapClient.getClient(connectionId);
-
+
if (!client) {
const connected = await ldapClient.connect(connection);
if (!connected) {
return res.status(500).json({ message: "Failed to connect to LDAP server" });
}
}
-
+
// Prepare changes
const changes = [];
const updateData: Partial = {};
-
+
if (req.body.description !== undefined) {
changes.push({
operation: 'replace',
@@ -4463,7 +4546,7 @@ export async function registerRoutes(app: Express): Promise {
});
updateData.description = req.body.description;
}
-
+
if (req.body.location !== undefined) {
changes.push({
operation: 'replace',
@@ -4473,24 +4556,24 @@ export async function registerRoutes(app: Express): Promise {
});
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" });
}
@@ -4499,10 +4582,10 @@ export async function registerRoutes(app: Express): Promise {
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',
@@ -4511,7 +4594,7 @@ export async function registerRoutes(app: Express): Promise {
userId: req.user?.id,
connectionId
});
-
+
res.json(updatedSite);
} catch (error) {
next(error);
@@ -4558,43 +4641,43 @@ export async function registerRoutes(app: Express): Promise {
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',
@@ -4603,7 +4686,7 @@ export async function registerRoutes(app: Express): Promise {
userId: req.user?.id,
connectionId
});
-
+
res.json({ success: true });
} catch (err) {
console.error("Error deleting AD site:", err);
@@ -4660,21 +4743,21 @@ export async function registerRoutes(app: Express): Promise {
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);
@@ -4683,16 +4766,16 @@ export async function registerRoutes(app: Express): Promise {
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(
@@ -4701,7 +4784,7 @@ export async function registerRoutes(app: Express): Promise {
offset,
`${req.protocol}://${req.get('host')}${req.originalUrl}`
);
-
+
res.json({
data: subnetsWithObjectType,
metadata: paginationMetadata
@@ -4748,22 +4831,22 @@ export async function registerRoutes(app: Express): Promise {
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);
@@ -4822,38 +4905,38 @@ export async function registerRoutes(app: Express): Promise {
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'],
@@ -4861,28 +4944,28 @@ export async function registerRoutes(app: Express): Promise {
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,
@@ -4898,7 +4981,7 @@ export async function registerRoutes(app: Express): Promise {
managedBy: null,
adProperties: subnetData
});
-
+
// Create audit log entry
await storage.createAuditLogEntry({
action: 'create',
@@ -4907,7 +4990,7 @@ export async function registerRoutes(app: Express): Promise {
userId: req.user?.id,
connectionId
});
-
+
res.status(201).json(newSubnet);
} catch (err) {
console.error("Error creating AD subnet:", err);
@@ -4974,36 +5057,36 @@ export async function registerRoutes(app: Express): Promise {
if (!req.isAuthenticated() && !req.headers.authorization) {
return res.status(401).json({ message: "Authentication required" });
}
-
+
const connectionId = parseInt(req.params.connectionId);
const objectGUID = req.params.objectGUID;
-
+
const subnet = await storage.getAdSubnetByObjectGUID(connectionId, objectGUID);
-
+
if (!subnet) {
return res.status(404).json({ message: "AD subnet not found" });
}
-
+
const connection = await storage.getLdapConnection(connectionId);
-
+
if (!connection) {
return res.status(404).json({ message: "LDAP connection not found" });
}
-
+
// Connect to LDAP if needed
const client = ldapClient.getClient(connectionId);
-
+
if (!client) {
const connected = await ldapClient.connect(connection);
if (!connected) {
return res.status(500).json({ message: "Failed to connect to LDAP server" });
}
}
-
+
// Prepare changes
const changes = [];
const updateData: Partial = {};
-
+
if (req.body.description !== undefined) {
changes.push({
operation: 'replace',
@@ -5013,7 +5096,7 @@ export async function registerRoutes(app: Express): Promise {
});
updateData.description = req.body.description;
}
-
+
if (req.body.location !== undefined) {
changes.push({
operation: 'replace',
@@ -5023,7 +5106,7 @@ export async function registerRoutes(app: Express): Promise {
});
updateData.location = req.body.location;
}
-
+
if (req.body.siteObject !== undefined) {
changes.push({
operation: 'replace',
@@ -5033,28 +5116,28 @@ export async function registerRoutes(app: Express): Promise {
});
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" });
}
@@ -5063,10 +5146,10 @@ export async function registerRoutes(app: Express): Promise {
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',
@@ -5075,7 +5158,7 @@ export async function registerRoutes(app: Express): Promise {
userId: req.user?.id,
connectionId
});
-
+
res.json(updatedSubnet);
} catch (error) {
next(error);
@@ -5122,43 +5205,43 @@ export async function registerRoutes(app: Express): Promise {
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',
@@ -5167,7 +5250,7 @@ export async function registerRoutes(app: Express): Promise {
userId: req.user?.id,
connectionId
});
-
+
res.json({ success: true });
} catch (err) {
console.error("Error deleting AD subnet:", err);
@@ -5182,18 +5265,18 @@ export async function registerRoutes(app: Express): Promise {
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:
@@ -5240,7 +5323,7 @@ export async function registerRoutes(app: Express): Promise {
next(error);
}
});
-
+
/**
* @swagger
* /api/connections/{connectionId}/remove-from-group:
@@ -5297,33 +5380,71 @@ export async function registerRoutes(app: Express): Promise {
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
-
+
+ // Connect to LDAP if not already connected
+ if (!ldapClient.isConnectionActive(connectionId)) {
+ await ldapClient.connect(connection);
+ }
+
+ // Find the group by GUID to get the distinguishedName
+ const group = await storage.getAdGroupByObjectGUID(connectionId, data.groupObjectGUID);
+ if (!group) {
+ return res.status(404).json({ message: "Group not found" });
+ }
+
+ // Find the object by GUID to get the distinguishedName
+ let objectDN = '';
+ if (data.objectType === 'user') {
+ const user = await storage.getAdUserByObjectGUID(connectionId, data.objectGUID);
+ if (!user) {
+ return res.status(404).json({ message: "User not found" });
+ }
+ objectDN = user.distinguishedName;
+ } else if (data.objectType === 'computer') {
+ const computer = await storage.getAdComputerByObjectGUID(connectionId, data.objectGUID);
+ if (!computer) {
+ return res.status(404).json({ message: "Computer not found" });
+ }
+ objectDN = computer.distinguishedName;
+ } else {
+ return res.status(400).json({ message: "Invalid object type" });
+ }
+
+ // Remove the object from the group
+ const success = await ldapClient.removeFromGroup(
+ connectionId,
+ group.distinguishedName,
+ objectDN
+ );
+
+ if (!success) {
+ return res.status(500).json({ message: `Failed to remove ${data.objectType} from group` });
+ }
+
// Record the action in the audit log
const auditEntry = {
action: "REMOVE_FROM_GROUP",
targetId: data.objectGUID,
details: {
groupGUID: data.groupObjectGUID,
- objectType: data.objectType
+ objectType: data.objectType,
+ objectDN: objectDN,
+ groupDN: group.distinguishedName
},
- userId: req.user?.id || null,
+ 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}`
@@ -5340,7 +5461,7 @@ export async function registerRoutes(app: Express): Promise {
});
// This dedicated API endpoint has been replaced by object-specific endpoints using the PATCH method
- // See: /api/connections/:connectionId/ad-users/:id/managed-by,
+ // 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
@@ -5351,6 +5472,979 @@ export async function registerRoutes(app: Express): Promise {
// /api/connections/:connectionId/ad-computers/:objectGuid,
// /api/connections/:connectionId/ad-org-units/:objectGuid
+ /**
+ * @swagger
+ * /api/connections/{connectionId}/reset-password:
+ * post:
+ * summary: Reset a user's password
+ * tags: [Active Directory]
+ * security:
+ * - cookieAuth: []
+ * - apiTokenAuth: []
+ * parameters:
+ * - name: connectionId
+ * in: path
+ * required: true
+ * schema:
+ * type: integer
+ * requestBody:
+ * required: true
+ * content:
+ * application/json:
+ * schema:
+ * $ref: '#/components/schemas/PasswordReset'
+ * responses:
+ * 200:
+ * description: Password reset successful
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * message:
+ * type: string
+ * 400:
+ * description: Invalid request
+ * 401:
+ * $ref: '#/components/responses/UnauthorizedError'
+ * 404:
+ * description: User not found
+ * 500:
+ * description: Server error
+ */
+ app.post("/api/connections/:connectionId/reset-password", requirePermission(PERMISSIONS.UPDATE_AD_USERS, { allowApiToken: true }), async (req: Request, res: Response, next: NextFunction) => {
+ 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 = resetPasswordSchema.parse(req.body);
+
+ // Connect to LDAP if not already connected
+ if (!ldapClient.isConnectionActive(connectionId)) {
+ await ldapClient.connect(connection);
+ }
+
+ // Find the user by GUID to get the distinguishedName
+ const user = await storage.getAdUserByObjectGUID(connectionId, data.userObjectGUID);
+ if (!user) {
+ return res.status(404).json({ message: "User not found" });
+ }
+
+ // Reset the user's password
+ const success = await ldapClient.resetUserPassword(
+ connectionId,
+ user.distinguishedName,
+ data.newPassword,
+ data.skipValidation
+ );
+
+ if (!success) {
+ return res.status(500).json({ message: "Failed to reset password" });
+ }
+
+ // If requirePasswordChangeAtNextLogon is true, set the pwdLastSet attribute to 0
+ if (data.requirePasswordChangeAtNextLogon) {
+ await ldapClient.updateAttribute(
+ connectionId,
+ user.distinguishedName,
+ 'pwdLastSet',
+ '0',
+ 'replace'
+ );
+ }
+
+ // Record the action in the audit log
+ const auditEntry = {
+ action: "RESET_PASSWORD",
+ targetId: data.userObjectGUID,
+ details: {
+ userDN: user.distinguishedName,
+ requirePasswordChangeAtNextLogon: data.requirePasswordChangeAtNextLogon
+ },
+ userId: req.user?.id ?? null,
+ connectionId: connectionId
+ };
+
+ // Save the audit entry to storage
+ await storage.createAuditLogEntry(auditEntry);
+
+ res.json({
+ success: true,
+ message: `Password reset successful for user with GUID ${data.userObjectGUID}`
+ });
+ } catch (error) {
+ if (error instanceof ZodError) {
+ return handleZodError(error, res);
+ }
+ throw error;
+ }
+ } catch (error) {
+ next(error);
+ }
+ });
+
+ /**
+ * @swagger
+ * /api/connections/{connectionId}/enable-user-account:
+ * post:
+ * summary: Enable or disable a user account
+ * tags: [Active Directory]
+ * security:
+ * - cookieAuth: []
+ * - apiTokenAuth: []
+ * parameters:
+ * - name: connectionId
+ * in: path
+ * required: true
+ * schema:
+ * type: integer
+ * requestBody:
+ * required: true
+ * content:
+ * application/json:
+ * schema:
+ * $ref: '#/components/schemas/EnableUserAccount'
+ * responses:
+ * 200:
+ * description: Account status updated successfully
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * message:
+ * type: string
+ * 400:
+ * description: Invalid request
+ * 401:
+ * $ref: '#/components/responses/UnauthorizedError'
+ * 404:
+ * description: User not found
+ * 500:
+ * description: Server error
+ */
+ app.post("/api/connections/:connectionId/enable-user-account", requirePermission(PERMISSIONS.UPDATE_AD_USERS, { allowApiToken: true }), async (req: Request, res: Response, next: NextFunction) => {
+ 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 = enableUserAccountSchema.parse(req.body);
+
+ // Connect to LDAP if not already connected
+ if (!ldapClient.isConnectionActive(connectionId)) {
+ await ldapClient.connect(connection);
+ }
+
+ // Find the user by GUID to get the distinguishedName
+ const user = await storage.getAdUserByObjectGUID(connectionId, data.userObjectGUID);
+ if (!user) {
+ return res.status(404).json({ message: "User not found" });
+ }
+
+ // Enable or disable the user account
+ const success = await ldapClient.setUserAccountStatus(
+ connectionId,
+ user.distinguishedName,
+ data.enabled
+ );
+
+ if (!success) {
+ return res.status(500).json({ message: `Failed to ${data.enabled ? 'enable' : 'disable'} user account` });
+ }
+
+ // Record the action in the audit log
+ const auditEntry = {
+ action: data.enabled ? "ENABLE_USER_ACCOUNT" : "DISABLE_USER_ACCOUNT",
+ targetId: data.userObjectGUID,
+ details: {
+ userDN: user.distinguishedName
+ },
+ userId: req.user?.id ?? null,
+ connectionId: connectionId
+ };
+
+ // Save the audit entry to storage
+ await storage.createAuditLogEntry(auditEntry);
+
+ // Update the user in the database
+ await storage.updateAdUserByObjectGUID(connectionId, data.userObjectGUID, { enabled: data.enabled });
+
+ res.json({
+ success: true,
+ message: `User account ${data.enabled ? 'enabled' : 'disabled'} successfully for user with GUID ${data.userObjectGUID}`
+ });
+ } catch (error) {
+ if (error instanceof ZodError) {
+ return handleZodError(error, res);
+ }
+ throw error;
+ }
+ } catch (error) {
+ next(error);
+ }
+ });
+
+ /**
+ * @swagger
+ * /api/connections/{connectionId}/bulk-enable-user-accounts:
+ * post:
+ * summary: Enable or disable multiple user accounts
+ * tags: [Active Directory]
+ * security:
+ * - cookieAuth: []
+ * - apiTokenAuth: []
+ * parameters:
+ * - name: connectionId
+ * in: path
+ * required: true
+ * schema:
+ * type: integer
+ * requestBody:
+ * required: true
+ * content:
+ * application/json:
+ * schema:
+ * $ref: '#/components/schemas/BulkEnableUserAccounts'
+ * responses:
+ * 200:
+ * description: Bulk account status update results
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: array
+ * items:
+ * type: string
+ * failed:
+ * type: array
+ * items:
+ * type: object
+ * properties:
+ * dn:
+ * type: string
+ * error:
+ * type: string
+ * 400:
+ * description: Invalid request
+ * 401:
+ * $ref: '#/components/responses/UnauthorizedError'
+ * 500:
+ * description: Server error
+ */
+ app.post("/api/connections/:connectionId/bulk-enable-user-accounts", requirePermission(PERMISSIONS.UPDATE_AD_USERS, { allowApiToken: true }), async (req: Request, res: Response, next: NextFunction) => {
+ 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 = bulkEnableUserAccountsSchema.parse(req.body);
+
+ // Connect to LDAP if not already connected
+ if (!ldapClient.isConnectionActive(connectionId)) {
+ await ldapClient.connect(connection);
+ }
+
+ // Enable or disable the user accounts
+ const results = await ldapClient.bulkSetUserAccountStatus(
+ connectionId,
+ data.userDNs,
+ data.enabled
+ );
+
+ // Record the action in the audit log
+ const auditEntry = {
+ action: data.enabled ? "BULK_ENABLE_USER_ACCOUNTS" : "BULK_DISABLE_USER_ACCOUNTS",
+ targetId: "multiple",
+ details: {
+ userDNs: data.userDNs,
+ results: results
+ },
+ userId: req.user?.id ?? null,
+ connectionId: connectionId
+ };
+
+ // Save the audit entry to storage
+ await storage.createAuditLogEntry(auditEntry);
+
+ // Update the users in the database
+ for (const dn of results.success) {
+ try {
+ // Find the user by DN
+ const users = await ldapClient.searchUsers(connectionId, `(distinguishedName=${dn})`, ['objectGUID']);
+ if (users && users.length > 0) {
+ const objectGUID = users[0].objectGUID;
+ await storage.updateAdUserByObjectGUID(connectionId, objectGUID, { enabled: data.enabled });
+ }
+ } catch (error) {
+ console.error(`Failed to update user in database: ${error}`);
+ }
+ }
+
+ res.json(results);
+ } catch (error) {
+ if (error instanceof ZodError) {
+ return handleZodError(error, res);
+ }
+ throw error;
+ }
+ } catch (error) {
+ next(error);
+ }
+ });
+
+ /**
+ * @swagger
+ * /api/connections/{connectionId}/reset-password:
+ * post:
+ * summary: Reset a user's password
+ * tags: [Active Directory]
+ * security:
+ * - cookieAuth: []
+ * - apiTokenAuth: []
+ * parameters:
+ * - name: connectionId
+ * in: path
+ * required: true
+ * schema:
+ * type: integer
+ * requestBody:
+ * required: true
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * required:
+ * - userObjectGUID
+ * - newPassword
+ * properties:
+ * userObjectGUID:
+ * type: string
+ * description: The ObjectGUID of the user
+ * newPassword:
+ * type: string
+ * description: The new password for the user
+ * skipValidation:
+ * type: boolean
+ * description: Whether to skip password policy validation
+ * default: false
+ * requirePasswordChangeAtNextLogon:
+ * type: boolean
+ * description: Whether to require the user to change their password at next logon
+ * default: true
+ * responses:
+ * 200:
+ * description: Password reset successful
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * message:
+ * type: string
+ * 400:
+ * description: Invalid request
+ * 401:
+ * $ref: '#/components/responses/UnauthorizedError'
+ * 404:
+ * description: User not found
+ * 500:
+ * description: Server error
+ */
+ app.post("/api/connections/:connectionId/reset-password", requirePermission(PERMISSIONS.UPDATE_AD_USERS, { allowApiToken: true }), async (req: any, res: any, next: any) => {
+ 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 = resetPasswordSchema.parse(req.body);
+
+ // Connect to LDAP if not already connected
+ if (!ldapClient.isConnectionActive(connectionId)) {
+ await ldapClient.connect(connection);
+ }
+
+ // Find the user by GUID to get the distinguishedName
+ const user = await storage.getAdUserByObjectGUID(connectionId, data.userObjectGUID);
+ if (!user) {
+ return res.status(404).json({ message: "User not found" });
+ }
+
+ // Reset the user's password
+ const success = await ldapClient.resetUserPassword(
+ connectionId,
+ user.distinguishedName,
+ data.newPassword,
+ data.skipValidation
+ );
+
+ if (!success) {
+ return res.status(500).json({ message: "Failed to reset password" });
+ }
+
+ // If requirePasswordChangeAtNextLogon is true, set the pwdLastSet attribute to 0
+ if (data.requirePasswordChangeAtNextLogon) {
+ await ldapClient.updateAttribute(
+ connectionId,
+ user.distinguishedName,
+ 'pwdLastSet',
+ '0',
+ 'replace'
+ );
+ }
+
+ // Record the action in the audit log
+ const auditEntry = {
+ action: "RESET_PASSWORD",
+ targetId: data.userObjectGUID,
+ details: {
+ userDN: user.distinguishedName,
+ requirePasswordChangeAtNextLogon: data.requirePasswordChangeAtNextLogon
+ },
+ userId: req.user?.id ?? null,
+ connectionId: connectionId
+ };
+
+ // Save the audit entry to storage
+ await storage.createAuditLogEntry(auditEntry);
+
+ res.json({
+ success: true,
+ message: `Password reset successful for user with GUID ${data.userObjectGUID}`
+ });
+ } catch (error) {
+ if (error instanceof ZodError) {
+ return handleZodError(error, res);
+ }
+ throw error;
+ }
+ } catch (error) {
+ next(error);
+ }
+ });
+
+ /**
+ * @swagger
+ * /api/connections/{connectionId}/enable-user-account:
+ * post:
+ * summary: Enable or disable a user account
+ * tags: [Active Directory]
+ * security:
+ * - cookieAuth: []
+ * - apiTokenAuth: []
+ * parameters:
+ * - name: connectionId
+ * in: path
+ * required: true
+ * schema:
+ * type: integer
+ * requestBody:
+ * required: true
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * required:
+ * - userObjectGUID
+ * - enabled
+ * properties:
+ * userObjectGUID:
+ * type: string
+ * description: The ObjectGUID of the user
+ * enabled:
+ * type: boolean
+ * description: Whether to enable or disable the account
+ * responses:
+ * 200:
+ * description: Account status updated successfully
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * message:
+ * type: string
+ * 400:
+ * description: Invalid request
+ * 401:
+ * $ref: '#/components/responses/UnauthorizedError'
+ * 404:
+ * description: User not found
+ * 500:
+ * description: Server error
+ */
+ app.post("/api/connections/:connectionId/enable-user-account", requirePermission(PERMISSIONS.UPDATE_AD_USERS, { allowApiToken: true }), async (req: any, res: any, next: any) => {
+ 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 = enableUserAccountSchema.parse(req.body);
+
+ // Connect to LDAP if not already connected
+ if (!ldapClient.isConnectionActive(connectionId)) {
+ await ldapClient.connect(connection);
+ }
+
+ // Find the user by GUID to get the distinguishedName
+ const user = await storage.getAdUserByObjectGUID(connectionId, data.userObjectGUID);
+ if (!user) {
+ return res.status(404).json({ message: "User not found" });
+ }
+
+ // Enable or disable the user account
+ const success = await ldapClient.setUserAccountStatus(
+ connectionId,
+ user.distinguishedName,
+ data.enabled
+ );
+
+ if (!success) {
+ return res.status(500).json({ message: `Failed to ${data.enabled ? 'enable' : 'disable'} user account` });
+ }
+
+ // Record the action in the audit log
+ const auditEntry = {
+ action: data.enabled ? "ENABLE_USER_ACCOUNT" : "DISABLE_USER_ACCOUNT",
+ targetId: data.userObjectGUID,
+ details: {
+ userDN: user.distinguishedName
+ },
+ userId: req.user?.id ?? null,
+ connectionId: connectionId
+ };
+
+ // Save the audit entry to storage
+ await storage.createAuditLogEntry(auditEntry);
+
+ // Update the user in the database
+ await storage.updateAdUser(connectionId, data.userObjectGUID, { enabled: data.enabled });
+
+ res.json({
+ success: true,
+ message: `User account ${data.enabled ? 'enabled' : 'disabled'} successfully for user with GUID ${data.userObjectGUID}`
+ });
+ } catch (error) {
+ if (error instanceof ZodError) {
+ return handleZodError(error, res);
+ }
+ throw error;
+ }
+ } catch (error) {
+ next(error);
+ }
+ });
+
+ /**
+ * @swagger
+ * /api/connections/{connectionId}/bulk-enable-user-accounts:
+ * post:
+ * summary: Enable or disable multiple user accounts
+ * tags: [Active Directory]
+ * security:
+ * - cookieAuth: []
+ * - apiTokenAuth: []
+ * parameters:
+ * - name: connectionId
+ * in: path
+ * required: true
+ * schema:
+ * type: integer
+ * requestBody:
+ * required: true
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * required:
+ * - userDNs
+ * - enabled
+ * properties:
+ * userDNs:
+ * type: array
+ * items:
+ * type: string
+ * description: Array of user distinguished names
+ * enabled:
+ * type: boolean
+ * description: Whether to enable or disable the accounts
+ * responses:
+ * 200:
+ * description: Bulk account status update results
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: array
+ * items:
+ * type: string
+ * failed:
+ * type: array
+ * items:
+ * type: object
+ * properties:
+ * dn:
+ * type: string
+ * error:
+ * type: string
+ * 400:
+ * description: Invalid request
+ * 401:
+ * $ref: '#/components/responses/UnauthorizedError'
+ * 500:
+ * description: Server error
+ */
+ app.post("/api/connections/:connectionId/bulk-enable-user-accounts", requirePermission(PERMISSIONS.UPDATE_AD_USERS, { allowApiToken: true }), async (req: any, res: any, next: any) => {
+ 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 = bulkEnableUserAccountsSchema.parse(req.body);
+
+ // Connect to LDAP if not already connected
+ if (!ldapClient.isConnectionActive(connectionId)) {
+ await ldapClient.connect(connection);
+ }
+
+ // Enable or disable the user accounts
+ const results = await ldapClient.bulkSetUserAccountStatus(
+ connectionId,
+ data.userDNs,
+ data.enabled
+ );
+
+ // Record the action in the audit log
+ const auditEntry = {
+ action: data.enabled ? "BULK_ENABLE_USER_ACCOUNTS" : "BULK_DISABLE_USER_ACCOUNTS",
+ targetId: "multiple",
+ details: {
+ userDNs: data.userDNs,
+ results: results
+ },
+ userId: req.user?.id ?? null,
+ connectionId: connectionId
+ };
+
+ // Save the audit entry to storage
+ await storage.createAuditLogEntry(auditEntry);
+
+ // Update the users in the database
+ for (const dn of results.success) {
+ try {
+ // Find the user by DN
+ const user = await storage.getAdUserByDN(connectionId, dn);
+ if (user) {
+ await storage.updateAdUser(connectionId, user.objectGUID, { enabled: data.enabled });
+ }
+ } catch (error) {
+ console.error(`Failed to update user in database: ${error}`);
+ }
+ }
+
+ res.json(results);
+ } catch (error) {
+ if (error instanceof ZodError) {
+ return handleZodError(error, res);
+ }
+ throw error;
+ }
+ } catch (error) {
+ next(error);
+ }
+ });
+
+ /**
+ * @swagger
+ * /api/connections/{connectionId}/reset-password:
+ * post:
+ * summary: Reset a user's password
+ * tags: [Active Directory]
+ * security:
+ * - cookieAuth: []
+ * - apiTokenAuth: []
+ * parameters:
+ * - name: connectionId
+ * in: path
+ * required: true
+ * schema:
+ * type: integer
+ * requestBody:
+ * required: true
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * required:
+ * - userObjectGUID
+ * - newPassword
+ * properties:
+ * userObjectGUID:
+ * type: string
+ * description: The ObjectGUID of the user
+ * newPassword:
+ * type: string
+ * description: The new password for the user
+ * skipValidation:
+ * type: boolean
+ * description: Whether to skip password policy validation
+ * default: false
+ * requirePasswordChangeAtNextLogon:
+ * type: boolean
+ * description: Whether to require the user to change their password at next logon
+ * default: true
+ * responses:
+ * 200:
+ * description: Password reset successful
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * message:
+ * type: string
+ * 400:
+ * description: Invalid request
+ * 401:
+ * $ref: '#/components/responses/UnauthorizedError'
+ * 404:
+ * description: User not found
+ * 500:
+ * description: Server error
+ */
+ app.post("/api/connections/:connectionId/reset-password", requirePermission(PERMISSIONS.UPDATE_AD_USERS, { allowApiToken: true }), 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 = resetPasswordSchema.parse(req.body);
+
+ // Connect to LDAP if not already connected
+ if (!ldapClient.isConnectionActive(connectionId)) {
+ await ldapClient.connect(connection);
+ }
+
+ // Find the user by GUID to get the distinguishedName
+ const user = await storage.getAdUserByObjectGUID(connectionId, data.userObjectGUID);
+ if (!user) {
+ return res.status(404).json({ message: "User not found" });
+ }
+
+ // Reset the user's password
+ const success = await ldapClient.resetUserPassword(
+ connectionId,
+ user.distinguishedName,
+ data.newPassword,
+ data.skipValidation
+ );
+
+ if (!success) {
+ return res.status(500).json({ message: "Failed to reset password" });
+ }
+
+ // If requirePasswordChangeAtNextLogon is true, set the pwdLastSet attribute to 0
+ if (data.requirePasswordChangeAtNextLogon) {
+ await ldapClient.updateAttribute(
+ connectionId,
+ user.distinguishedName,
+ 'pwdLastSet',
+ '0',
+ 'replace'
+ );
+ }
+
+ // Record the action in the audit log
+ const auditEntry = {
+ action: "RESET_PASSWORD",
+ targetId: data.userObjectGUID,
+ details: {
+ userDN: user.distinguishedName,
+ requirePasswordChangeAtNextLogon: data.requirePasswordChangeAtNextLogon
+ },
+ userId: req.user?.id ?? null,
+ connectionId: connectionId
+ };
+
+ // Save the audit entry to storage
+ await storage.createAuditLogEntry(auditEntry);
+
+ res.json({
+ success: true,
+ message: `Password reset successful for user with GUID ${data.userObjectGUID}`
+ });
+ } catch (error) {
+ if (error instanceof ZodError) {
+ return handleZodError(error, res);
+ }
+ throw error;
+ }
+ } catch (error) {
+ next(error);
+ }
+ });
+
+ /**
+ * @swagger
+ * /api/connections/{connectionId}/enable-user-account:
+ * post:
+ * summary: Enable or disable a user account
+ * tags: [Active Directory]
+ * security:
+ * - cookieAuth: []
+ * - apiTokenAuth: []
+ * parameters:
+ * - name: connectionId
+ * in: path
+ * required: true
+ * schema:
+ * type: integer
+ * requestBody:
+ * required: true
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * required:
+ * - userObjectGUID
+ * - enabled
+ * properties:
+ * userObjectGUID:
+ * type: string
+ * description: The ObjectGUID of the user
+ * enabled:
+ * type: boolean
+ * description: Whether to enable or disable the account
+ * responses:
+ * 200:
+ * description: Account status updated successfully
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * success:
+ * type: boolean
+ * message:
+ * type: string
+ * 400:
+ * description: Invalid request
+ * 401:
+ * $ref: '#/components/responses/UnauthorizedError'
+ * 404:
+ * description: User not found
+ * 500:
+ * description: Server error
+ */
+ app.post("/api/connections/:connectionId/enable-user-account", requirePermission(PERMISSIONS.UPDATE_AD_USERS, { allowApiToken: true }), 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 = enableUserAccountSchema.parse(req.body);
+
+ // Connect to LDAP if not already connected
+ if (!ldapClient.isConnectionActive(connectionId)) {
+ await ldapClient.connect(connection);
+ }
+
+ // Find the user by GUID to get the distinguishedName
+ const user = await storage.getAdUserByObjectGUID(connectionId, data.userObjectGUID);
+ if (!user) {
+ return res.status(404).json({ message: "User not found" });
+ }
+
+ // Enable or disable the user account
+ const success = await ldapClient.setUserAccountStatus(
+ connectionId,
+ user.distinguishedName,
+ data.enabled
+ );
+
+ if (!success) {
+ return res.status(500).json({ message: `Failed to ${data.enabled ? 'enable' : 'disable'} user account` });
+ }
+
+ // Record the action in the audit log
+ const auditEntry = {
+ action: data.enabled ? "ENABLE_USER_ACCOUNT" : "DISABLE_USER_ACCOUNT",
+ targetId: data.userObjectGUID,
+ details: {
+ userDN: user.distinguishedName
+ },
+ userId: req.user?.id ?? null,
+ connectionId: connectionId
+ };
+
+ // Save the audit entry to storage
+ await storage.createAuditLogEntry(auditEntry);
+
+ // Update the user in the database
+ await storage.updateAdUser(connectionId, data.userObjectGUID, { enabled: data.enabled });
+
+ res.json({
+ success: true,
+ message: `User account ${data.enabled ? 'enabled' : 'disabled'} successfully for user with GUID ${data.userObjectGUID}`
+ });
+ } catch (error) {
+ if (error instanceof ZodError) {
+ return handleZodError(error, res);
+ }
+ throw error;
+ }
+ } catch (error) {
+ next(error);
+ }
+ });
+
/**
* @swagger
* /api/audit-logs:
@@ -5408,7 +6502,7 @@ export async function registerRoutes(app: Express): Promise {
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) {
@@ -5481,13 +6575,13 @@ export async function registerRoutes(app: Express): Promise {
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) {
@@ -5500,20 +6594,20 @@ export async function registerRoutes(app: Express): Promise {
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);
@@ -5525,20 +6619,20 @@ export async function registerRoutes(app: Express): Promise {
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);
@@ -5550,20 +6644,20 @@ export async function registerRoutes(app: Express): Promise {
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);
@@ -5575,20 +6669,20 @@ export async function registerRoutes(app: Express): Promise {
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);
@@ -5600,20 +6694,20 @@ export async function registerRoutes(app: Express): Promise {
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);
@@ -5625,20 +6719,20 @@ export async function registerRoutes(app: Express): Promise {
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);
@@ -5650,20 +6744,20 @@ export async function registerRoutes(app: Express): Promise {
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);
@@ -5675,7 +6769,7 @@ export async function registerRoutes(app: Express): Promise {
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,
@@ -5686,25 +6780,25 @@ export async function registerRoutes(app: Express): Promise {
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),
@@ -5733,11 +6827,11 @@ export async function registerRoutes(app: Express): Promise {
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);
@@ -5759,11 +6853,11 @@ export async function registerRoutes(app: Express): Promise {
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);
@@ -5775,11 +6869,11 @@ export async function registerRoutes(app: Express): Promise {
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);
@@ -5850,11 +6944,11 @@ export async function registerRoutes(app: Express): Promise {
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);
@@ -5899,11 +6993,11 @@ export async function registerRoutes(app: Express): Promise {
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);
@@ -5915,11 +7009,11 @@ export async function registerRoutes(app: Express): Promise {
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);
diff --git a/server/swagger.ts b/server/swagger.ts
index cc0c1ff..6a4a92c 100644
--- a/server/swagger.ts
+++ b/server/swagger.ts
@@ -35,6 +35,32 @@ const swaggerOptions = {
description: "API base URL",
},
],
+ tags: [
+ {
+ name: "LDAP Connections",
+ description: "Operations for managing LDAP connections"
+ },
+ {
+ name: "AD Users",
+ description: "Operations for managing Active Directory users"
+ },
+ {
+ name: "AD Groups",
+ description: "Operations for managing Active Directory groups"
+ },
+ {
+ name: "AD Computers",
+ description: "Operations for managing Active Directory computers"
+ },
+ {
+ name: "AD Organizational Units",
+ description: "Operations for managing Active Directory organizational units"
+ },
+ {
+ name: "Active Directory",
+ description: "General Active Directory operations including password management and account status"
+ }
+ ],
components: {
securitySchemes: {
bearerAuth: {
@@ -299,7 +325,7 @@ const swaggerOptions = {
id: { type: "integer" },
action: { type: "string" },
targetId: { type: "string" },
- details: {
+ details: {
type: "object",
additionalProperties: true
},
@@ -308,6 +334,59 @@ const swaggerOptions = {
connectionId: { type: "integer" }
},
},
+ PasswordReset: {
+ type: "object",
+ required: ["userObjectGUID", "newPassword"],
+ properties: {
+ userObjectGUID: {
+ type: "string",
+ description: "The ObjectGUID of the user"
+ },
+ newPassword: {
+ type: "string",
+ description: "The new password for the user"
+ },
+ skipValidation: {
+ type: "boolean",
+ description: "Whether to skip password policy validation",
+ default: false
+ },
+ requirePasswordChangeAtNextLogon: {
+ type: "boolean",
+ description: "Whether to require the user to change their password at next logon",
+ default: true
+ }
+ }
+ },
+ EnableUserAccount: {
+ type: "object",
+ required: ["userObjectGUID", "enabled"],
+ properties: {
+ userObjectGUID: {
+ type: "string",
+ description: "The ObjectGUID of the user"
+ },
+ enabled: {
+ type: "boolean",
+ description: "Whether to enable or disable the account"
+ }
+ }
+ },
+ BulkEnableUserAccounts: {
+ type: "object",
+ required: ["userDNs", "enabled"],
+ properties: {
+ userDNs: {
+ type: "array",
+ items: { type: "string" },
+ description: "Array of user distinguished names"
+ },
+ enabled: {
+ type: "boolean",
+ description: "Whether to enable or disable the accounts"
+ }
+ }
+ },
},
responses: {
UnauthorizedError: {
@@ -394,13 +473,13 @@ export function setupSwagger(app: Express) {
// Mount at both paths for backward compatibility
app.use("/api/docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec, swaggerUiOptions));
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec, swaggerUiOptions));
-
+
// Provide the JSON spec at multiple paths
app.get("/api/swagger.json", (req, res) => {
res.setHeader("Content-Type", "application/json");
res.send(swaggerSpec);
});
-
+
app.get("/api-docs/swagger.json", (req, res) => {
res.setHeader("Content-Type", "application/json");
res.send(swaggerSpec);
@@ -408,8 +487,8 @@ export function setupSwagger(app: Express) {
// Add download endpoint for the OpenAPI specification
app.get("/api/docs/download", (req, res) => {
- const format = req.query.format?.toString().toLowerCase() || 'json';
-
+ const format = req.query.format?.toString().toLowerCase() ?? 'json';
+
if (format === 'json') {
res.setHeader('Content-Disposition', 'attachment; filename=openapi-spec.json');
res.setHeader('Content-Type', 'application/json');
@@ -455,7 +534,7 @@ export function setupSwagger(app: Express) {
Active Directory Management API - Advanced Usage Guide
-
+
API Documentation
The complete API documentation is available at /api/docs.
You can also download the OpenAPI specification:
@@ -463,28 +542,28 @@ export function setupSwagger(app: Express) {
Download OpenAPI Spec (JSON)
Download OpenAPI Spec (YAML)
-
+
Query Parameters and Filtering
${queryParamsInfo}
-
+
Authentication
This API supports two authentication methods:
- Session-based authentication: Used when accessing the API from the web interface.
- API Token authentication: Used when accessing the API programmatically.
-
+
API Token Authentication
To authenticate using an API token, include the token in the Authorization header:
Authorization: Bearer YOUR_API_TOKEN
-
+
Rate Limiting
The API has rate limiting in place to prevent abuse. The current limits are:
- 100 requests per minute for authenticated users
- 20 requests per minute for unauthenticated users
-
+
Error Handling
The API returns consistent error responses with the following structure:
{
@@ -496,20 +575,20 @@ export function setupSwagger(app: Express) {
}
]
}
-
+
Example Usage
Filter Users by Name
GET /api/connections/1/ad-users?filter=displayName contains 'John'
-
+
Select Specific Fields
GET /api/connections/1/ad-users?select=id,displayName,email
-
+
Pagination
GET /api/connections/1/ad-users?top=10&skip=20
-
+
Combining Parameters
GET /api/connections/1/ad-users?filter=enabled eq true&select=id,displayName,email&orderBy=displayName asc&top=10
-
+
Return to API Documentation