Add Addigy API integration and Docker deployment with Redis caching
- Implemented complete Addigy API v2 client with authentication via x-api-key - Added device and policy endpoints with automatic org ID resolution - Created field mapping from snake_case to Title Case for UI compatibility - Handles nested 'facts' response structure from Addigy devices API - Added comprehensive API documentation in ADDIGY_API_GUIDE.md - Multi-stage Dockerfile with optimized production build - Custom ports: App on 3100, Redis on 6380 (avoids conflicts) - Docker Compose orchestration with health checks - Standalone Next.js output for smaller container images - Non-root user execution for security - Implemented Redis caching layer for API responses - 5-minute TTL with graceful fallback if Redis unavailable - Cache key structure: service:entity:filter1:filter2 - Applied to Addigy devices endpoint with cache hit/miss logging - Fixed TypeScript strict mode errors for production builds - Added null safety checks with optional chaining throughout API routes - Wrapped useSearchParams in Suspense boundary for Next.js 15+ compatibility - Fixed type assertions for dynamic API responses - Corrected Set<string> type mismatches in device comparison logic - Created DOCKER_README.md with complete deployment guide - Updated ADDIGY_API_GUIDE.md with real-world API patterns - Documented response structures, field mappings, and troubleshooting - Next.js 16.0.0 with Turbopack - Redis 7 with AOF persistence - Podman/Docker compatible - TypeScript strict mode compliant
This commit is contained in:
parent
cf5fabe306
commit
f429f3af54
23 changed files with 1931 additions and 36 deletions
16
.env.docker
Normal file
16
.env.docker
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Autotask API Configuration
|
||||||
|
AUTOTASK_API_URL=https://webservices1.autotask.net/atservicesrest/v1.0
|
||||||
|
AUTOTASK_USERNAME=heabv32jr3yzoke@WULFCONSULTING.COM
|
||||||
|
AUTOTASK_SECRET='7g*Zf@K0Ns3#q$E4A1~n2#mW$'
|
||||||
|
AUTOTASK_API_INTEGRATION_CODE=FJCJDU3YQ6GUIYUMZ36AL2O4XCP
|
||||||
|
|
||||||
|
# Datto RMM API Configuration
|
||||||
|
DATTO_RMM_API_URL=https://concord-api.centrastage.net
|
||||||
|
DATTO_RMM_API_KEY=21F8F7ATN71JOUHESU14MM89FB1MNHI7
|
||||||
|
DATTO_RMM_API_SECRET=4N4E397S6HUI5TJD6QGMU43VE8S0SDLJ
|
||||||
|
|
||||||
|
# Addigy API Configuration
|
||||||
|
ADDIGY_API_URL=https://api.addigy.com/api/v2
|
||||||
|
ADDIGY_API_TOKEN=d9138a6561cb96b74d917ba81560cfd8
|
||||||
|
# Optional: If you need to specify a parent organization ID
|
||||||
|
# ADDIGY_ORG_ID=your_organization_id_here
|
||||||
345
ADDIGY_API_GUIDE.md
Normal file
345
ADDIGY_API_GUIDE.md
Normal file
|
|
@ -0,0 +1,345 @@
|
||||||
|
# Addigy API Integration Guide
|
||||||
|
|
||||||
|
This guide explains how to use the Addigy API integration in the PSA-Utils application.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### 1. Get Your Addigy API Token
|
||||||
|
|
||||||
|
1. Log into your Addigy account
|
||||||
|
2. Navigate to **Account > Integrations**
|
||||||
|
3. Click on the **V2** tab under "Addigy API"
|
||||||
|
4. Click **New API Token**
|
||||||
|
5. Enter a name for your token (e.g., "PSA-Utils Integration")
|
||||||
|
6. Select the appropriate permissions:
|
||||||
|
- **View Devices** - Required to fetch device information
|
||||||
|
- **View Policies** - Required to fetch policy information
|
||||||
|
- **View Organizations** - Optional, for organization data
|
||||||
|
- Additional permissions as needed for your use case
|
||||||
|
7. Click **Save**
|
||||||
|
8. **Copy the API token immediately** - you won't be able to see it again!
|
||||||
|
|
||||||
|
**Note**: The Addigy v2 API uses the `x-api-key` header for authentication, not Bearer tokens.
|
||||||
|
|
||||||
|
### 2. Configure Environment Variables
|
||||||
|
|
||||||
|
Add your Addigy API token to `.env.local`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Addigy API Configuration
|
||||||
|
ADDIGY_API_URL=https://api.addigy.com/api/v2
|
||||||
|
ADDIGY_API_TOKEN=your_actual_token_here
|
||||||
|
|
||||||
|
# Optional: Parent Organization ID (if needed)
|
||||||
|
# ADDIGY_ORG_ID=your_org_id_here
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Restart Your Development Server
|
||||||
|
|
||||||
|
After updating the `.env.local` file, restart your Next.js development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Import the Addigy Client
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { getAddigyClient } from '@/lib/services/addigy-factory';
|
||||||
|
|
||||||
|
const addigyClient = getAddigyClient();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Operations
|
||||||
|
|
||||||
|
#### Get All Devices
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const devices = await addigyClient.getAllDevices();
|
||||||
|
console.log(`Found ${devices.length} devices`);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Get Online Devices Only
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const onlineDevices = await addigyClient.getOnlineDevices();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Get Devices by Policy
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const policyId = '003f01fa-9bb3-421a-be29-409adf7a1xxx';
|
||||||
|
const devices = await addigyClient.getDevicesByPolicy(policyId);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Get Device Applications
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const deviceId = '114x11c3-b5dy-4n38-92fb-34th6ju782se';
|
||||||
|
const applications = await addigyClient.getDeviceApplications(deviceId);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Get All Policies
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const policies = await addigyClient.getAllPolicies();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Get Policy by ID
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const policyId = '76yh84t0-ju74-bh45-jd6b-ok87y4gc83gt';
|
||||||
|
const policy = await addigyClient.getPolicyById(policyId);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Get Active Alerts
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const alerts = await addigyClient.getActiveAlerts();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Get Maintenance Items
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const maintenanceItems = await addigyClient.getMaintenanceItems();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Get Custom Facts for a Device
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const deviceId = '114x11c3-b5dy-4n38-92fb-34th6ju782se';
|
||||||
|
const facts = await addigyClient.getCustomFacts(deviceId);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Assign Device to Policy
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const deviceId = '114x11c3-b5dy-4n38-92fb-34th6ju782se';
|
||||||
|
const newPolicyId = '76yh84t0-ju74-bh45-jd6b-ok87y4gc83gt';
|
||||||
|
await addigyClient.assignDeviceToPolicy(deviceId, newPolicyId);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced Queries with Filters
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Get devices with custom filters
|
||||||
|
const devices = await addigyClient.getAllDevices({
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
audit_field: 'Free Disk Percentage',
|
||||||
|
type: 'number',
|
||||||
|
operation: 'less_than',
|
||||||
|
value: 20,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
audit_field: 'Firewall Enabled',
|
||||||
|
type: 'boolean',
|
||||||
|
operation: 'equals',
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
limit: 100,
|
||||||
|
page: 1,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pagination for Large Datasets
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Get all devices with automatic pagination
|
||||||
|
const allDevices = await addigyClient.getAllDevicesPaginated(100);
|
||||||
|
console.log(`Total devices: ${allDevices.length}`);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Connection
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const isConnected = await addigyClient.testConnection();
|
||||||
|
if (isConnected) {
|
||||||
|
console.log('Successfully connected to Addigy API');
|
||||||
|
} else {
|
||||||
|
console.error('Failed to connect to Addigy API');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
The integration includes the following pre-built API endpoints:
|
||||||
|
|
||||||
|
### GET `/api/addigy-devices`
|
||||||
|
|
||||||
|
Fetch devices from Addigy. Automatically retrieves the organization ID from policies if not configured.
|
||||||
|
|
||||||
|
**Query Parameters:**
|
||||||
|
- `policyId` (optional) - Filter devices by policy ID
|
||||||
|
- `online` (optional) - Set to 'true' to get only online devices
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
# Get all devices
|
||||||
|
curl http://localhost:3000/api/addigy-devices
|
||||||
|
|
||||||
|
# Get devices for a specific policy
|
||||||
|
curl http://localhost:3000/api/addigy-devices?policyId=76yh84t0-ju74-bh45-jd6b-ok87y4gc83gt
|
||||||
|
|
||||||
|
# Get only online devices
|
||||||
|
curl http://localhost:3000/api/addigy-devices?online=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### GET `/api/addigy-policies`
|
||||||
|
|
||||||
|
Fetch all policies from Addigy.
|
||||||
|
|
||||||
|
## Important API Details
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
- Uses `x-api-key` header (not `Authorization: Bearer`)
|
||||||
|
- Single API token for authentication
|
||||||
|
|
||||||
|
### Endpoint Structure
|
||||||
|
- **Devices**: `POST /o/{orgid}/devices` - Requires organization ID
|
||||||
|
- **Policies**: `POST /oa/policies/query` - Returns flat array
|
||||||
|
- All endpoints use POST with JSON body, not GET
|
||||||
|
|
||||||
|
### Request Body Format
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"page": 1,
|
||||||
|
"per_page": 500,
|
||||||
|
"query": {
|
||||||
|
"filters": [...]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response Structure
|
||||||
|
|
||||||
|
#### Devices Response
|
||||||
|
Devices are returned with nested `facts` structure:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"facts": {
|
||||||
|
"device_name": { "value": "MacBook-Pro", "type": "string" },
|
||||||
|
"serial_number": { "value": "C02XX...", "type": "string" },
|
||||||
|
"online": { "value": true, "type": "boolean" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Policies Response
|
||||||
|
Policies return as a flat array:
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"policyId": "69ae51e4-...",
|
||||||
|
"orgid": "4200f347-...",
|
||||||
|
"name": "Policy Name"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3000/api/addigy-policies
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rate Limiting
|
||||||
|
|
||||||
|
The Addigy API has a rate limit of **1,000 requests per 10 seconds**. The client includes built-in rate limiting to prevent exceeding this limit. If you exceed the rate limit, further requests will be rejected for 24 hours.
|
||||||
|
|
||||||
|
## Type Definitions
|
||||||
|
|
||||||
|
All Addigy types are defined in `/lib/types/addigy.ts`. Key types include:
|
||||||
|
|
||||||
|
- `AddigyDevice` - Device information
|
||||||
|
- `AddigyPolicy` - Policy information
|
||||||
|
- `AddigyApplication` - Installed application details
|
||||||
|
- `AddigyAlert` - Alert information
|
||||||
|
- `AddigyOrganization` - Organization details
|
||||||
|
- `AddigyMaintenanceItem` - Maintenance task information
|
||||||
|
- `AddigyMonitoringItem` - Monitoring rule information
|
||||||
|
- `AddigyCustomFact` - Custom facts/variables
|
||||||
|
- `AddigySoftwareItem` - Software catalog items
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
Always wrap API calls in try-catch blocks:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
try {
|
||||||
|
const devices = await addigyClient.getAllDevices();
|
||||||
|
// Process devices
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching devices:', error);
|
||||||
|
// Handle error appropriately
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Device Fields
|
||||||
|
|
||||||
|
Addigy returns device fields in snake_case format. The integration automatically maps these to Title Case for UI compatibility:
|
||||||
|
|
||||||
|
| Addigy Field | Mapped To | Description |
|
||||||
|
|--------------|-----------|-------------|
|
||||||
|
| `agentid` | `agentid` | Unique device identifier |
|
||||||
|
| `device_name` | `Device Name` | Device hostname |
|
||||||
|
| `device_model_name` | `Device Model Name` | Device model (e.g., "MacBook Pro") |
|
||||||
|
| `mac_os_x_version` | `MAC OS X Version` | macOS version |
|
||||||
|
| `ios_version` | `iOS Version` | iOS version |
|
||||||
|
| `online` | `online` | Boolean indicating if device is online |
|
||||||
|
| `policy_id` | `policy_id` | Current policy ID |
|
||||||
|
| `current_user` | `Current User` | Currently logged in user |
|
||||||
|
| `serial_number` | `Serial Number` | Device serial number |
|
||||||
|
| `free_disk_percentage` | `Free Disk Percentage` | Available disk space percentage |
|
||||||
|
| `battery_percentage` | `Battery Percentage` | Current battery level |
|
||||||
|
| `firewall_enabled` | `Firewall Enabled` | Boolean |
|
||||||
|
| `filevault_enabled` | `FileVault Enabled` | Boolean |
|
||||||
|
| `agent_version` | `Agent Version` | Addigy agent version |
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [Addigy API v2 Documentation](https://support.addigy.com/hc/en-us/articles/16938210315411-API-Documentation-v2)
|
||||||
|
- [Addigy API v2 Interactive Docs](https://api.addigy.com/api/v2/documentation/)
|
||||||
|
- [Addigy Support Center](https://support.addigy.com/)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "ADDIGY_API_TOKEN environment variable is required"
|
||||||
|
|
||||||
|
Make sure you've added your API token to the `.env.local` file and restarted your development server.
|
||||||
|
|
||||||
|
### "API Error: Unauthorized" or 401 Error
|
||||||
|
|
||||||
|
Check that your API token is valid and has the necessary permissions. You may need to create a new token with the correct permissions in Addigy.
|
||||||
|
|
||||||
|
### "404 page not found" Error
|
||||||
|
|
||||||
|
This usually means:
|
||||||
|
1. Wrong endpoint path - check you're using `/o/{orgid}/devices` for devices
|
||||||
|
2. Missing organization ID - the integration will auto-fetch from policies if not configured
|
||||||
|
3. Using GET instead of POST - all Addigy v2 endpoints require POST
|
||||||
|
|
||||||
|
### Empty Device Names
|
||||||
|
|
||||||
|
Addigy uses snake_case field names (e.g., `device_name`). The integration automatically maps these to the UI-expected format (e.g., `Device Name`).
|
||||||
|
|
||||||
|
### Rate Limit Exceeded
|
||||||
|
|
||||||
|
If you receive a rate limit error, wait 24 hours before making more API requests. Consider implementing caching or reducing the frequency of API calls.
|
||||||
|
|
||||||
|
### "Invalid JSON response from Addigy API"
|
||||||
|
|
||||||
|
This may indicate an issue with the API endpoint or your token permissions. Check the Addigy API documentation for the specific endpoint you're trying to access.
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
- Explore the Addigy API documentation for additional endpoints
|
||||||
|
- Create custom API routes for your specific use cases
|
||||||
|
- Integrate Addigy data with your Autotask or other systems
|
||||||
|
- Build dashboards to visualize device status, alerts, and compliance
|
||||||
21
autotask-app/.dockerignore
Normal file
21
autotask-app/.dockerignore
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
.next
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
README.md
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.production
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
coverage
|
||||||
|
.nyc_output
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.dockerignore
|
||||||
|
Dockerfile
|
||||||
|
docker-compose.yml
|
||||||
199
autotask-app/DOCKER_README.md
Normal file
199
autotask-app/DOCKER_README.md
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
# Docker Setup for PSA-Utils
|
||||||
|
|
||||||
|
This application is configured to run in Docker with custom ports to avoid conflicts with other services.
|
||||||
|
|
||||||
|
## Port Configuration
|
||||||
|
|
||||||
|
- **Frontend/Backend (Next.js)**: Port `3100` (instead of default 3000)
|
||||||
|
- **Redis Cache**: Port `6380` (instead of default 6379)
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Copy Environment Variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.local .env.docker
|
||||||
|
# Edit .env.docker with your actual API credentials
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Build and Run with Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build and start all services
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Stop all services
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
# Stop and remove volumes (clears Redis cache)
|
||||||
|
docker-compose down -v
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Access the Application
|
||||||
|
|
||||||
|
Open your browser and navigate to: `http://localhost:3100`
|
||||||
|
|
||||||
|
## Docker Services
|
||||||
|
|
||||||
|
### Application Service
|
||||||
|
- **Container Name**: `psa-utils-app`
|
||||||
|
- **Port**: 3100
|
||||||
|
- **Features**:
|
||||||
|
- Multi-stage build for optimized image size
|
||||||
|
- Runs as non-root user for security
|
||||||
|
- Automatic restart on failure
|
||||||
|
- Environment variables passed from docker-compose
|
||||||
|
|
||||||
|
### Redis Cache Service
|
||||||
|
- **Container Name**: `psa-utils-redis`
|
||||||
|
- **Port**: 6380
|
||||||
|
- **Features**:
|
||||||
|
- Persistent data storage
|
||||||
|
- Append-only file for durability
|
||||||
|
- Health checks
|
||||||
|
- Automatic restart on failure
|
||||||
|
|
||||||
|
## Caching Strategy
|
||||||
|
|
||||||
|
The application implements Redis caching for API responses:
|
||||||
|
|
||||||
|
- **Addigy Devices**: Cached for 5 minutes
|
||||||
|
- **Addigy Policies**: Cached for 5 minutes
|
||||||
|
- **Autotask Tickets**: Can be cached (add to route)
|
||||||
|
- **Autotask Companies**: Can be cached (add to route)
|
||||||
|
|
||||||
|
Cache keys are structured as: `service:entity:filter1:filter2`
|
||||||
|
|
||||||
|
Example: `addigy:devices:all:online`
|
||||||
|
|
||||||
|
## Development vs Production
|
||||||
|
|
||||||
|
### Development Mode
|
||||||
|
```bash
|
||||||
|
# Run with mounted .env.local for easy configuration changes
|
||||||
|
docker-compose up
|
||||||
|
```
|
||||||
|
|
||||||
|
### Production Mode
|
||||||
|
```bash
|
||||||
|
# Build with embedded environment variables
|
||||||
|
docker build --build-arg NODE_ENV=production -t psa-utils:latest .
|
||||||
|
|
||||||
|
# Run with environment file
|
||||||
|
docker run -d \
|
||||||
|
--name psa-utils \
|
||||||
|
-p 3100:3100 \
|
||||||
|
--env-file .env.production \
|
||||||
|
psa-utils:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
### Check Service Health
|
||||||
|
```bash
|
||||||
|
# Check if services are running
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
# Check Redis connection
|
||||||
|
docker exec psa-utils-redis redis-cli ping
|
||||||
|
|
||||||
|
# Monitor Redis cache
|
||||||
|
docker exec psa-utils-redis redis-cli monitor
|
||||||
|
|
||||||
|
# View cache keys
|
||||||
|
docker exec psa-utils-redis redis-cli keys "*"
|
||||||
|
```
|
||||||
|
|
||||||
|
### View Logs
|
||||||
|
```bash
|
||||||
|
# All services
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
# Specific service
|
||||||
|
docker-compose logs -f app
|
||||||
|
docker-compose logs -f redis
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Port Already in Use
|
||||||
|
If ports 3100 or 6380 are already in use, modify the port mappings in `docker-compose.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
ports:
|
||||||
|
- "3200:3100" # Change 3200 to your desired port
|
||||||
|
redis:
|
||||||
|
ports:
|
||||||
|
- "6381:6379" # Change 6381 to your desired port
|
||||||
|
```
|
||||||
|
|
||||||
|
### Clear Redis Cache
|
||||||
|
```bash
|
||||||
|
# Connect to Redis and flush
|
||||||
|
docker exec psa-utils-redis redis-cli FLUSHDB
|
||||||
|
|
||||||
|
# Or restart with volume removal
|
||||||
|
docker-compose down -v
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables Not Loading
|
||||||
|
Ensure your `.env.local` file exists and contains all required variables:
|
||||||
|
- `AUTOTASK_*` credentials
|
||||||
|
- `DATTO_RMM_*` credentials
|
||||||
|
- `ADDIGY_*` credentials
|
||||||
|
- `REDIS_URL` (set automatically in Docker)
|
||||||
|
|
||||||
|
### Build Errors
|
||||||
|
```bash
|
||||||
|
# Clean build
|
||||||
|
docker-compose build --no-cache
|
||||||
|
|
||||||
|
# Remove all containers and images
|
||||||
|
docker-compose down
|
||||||
|
docker system prune -a
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Optimization
|
||||||
|
|
||||||
|
### Redis Configuration
|
||||||
|
The Redis cache is configured with:
|
||||||
|
- AOF persistence for durability
|
||||||
|
- 5-minute TTL for most cached data
|
||||||
|
- Automatic retry on connection failure
|
||||||
|
- Health checks every 5 seconds
|
||||||
|
|
||||||
|
### Next.js Optimization
|
||||||
|
- Standalone output mode for smaller Docker images
|
||||||
|
- Multi-stage build reduces final image size
|
||||||
|
- Static assets served efficiently
|
||||||
|
- Production optimizations enabled
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
1. **Non-root User**: Application runs as `nextjs` user (UID 1001)
|
||||||
|
2. **Environment Variables**: Sensitive data kept in `.env` files, not in images
|
||||||
|
3. **Network Isolation**: Services communicate via Docker network
|
||||||
|
4. **Port Mapping**: Only necessary ports exposed to host
|
||||||
|
5. **Redis Security**: Redis only accessible within Docker network
|
||||||
|
|
||||||
|
## Backup and Restore
|
||||||
|
|
||||||
|
### Backup Redis Data
|
||||||
|
```bash
|
||||||
|
# Create backup
|
||||||
|
docker exec psa-utils-redis redis-cli BGSAVE
|
||||||
|
docker cp psa-utils-redis:/data/dump.rdb ./redis-backup.rdb
|
||||||
|
```
|
||||||
|
|
||||||
|
### Restore Redis Data
|
||||||
|
```bash
|
||||||
|
# Restore backup
|
||||||
|
docker cp ./redis-backup.rdb psa-utils-redis:/data/dump.rdb
|
||||||
|
docker-compose restart redis
|
||||||
|
```
|
||||||
55
autotask-app/Dockerfile
Normal file
55
autotask-app/Dockerfile
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
# Multi-stage build for Next.js application
|
||||||
|
FROM node:20-alpine AS base
|
||||||
|
|
||||||
|
# Install dependencies only when needed
|
||||||
|
FROM base AS deps
|
||||||
|
RUN apk add --no-cache libc6-compat
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Rebuild the source code only when needed
|
||||||
|
FROM base AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Next.js collects completely anonymous telemetry data about general usage.
|
||||||
|
# Learn more here: https://nextjs.org/telemetry
|
||||||
|
# Uncomment the following line in case you want to disable telemetry during the build.
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED 1
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Production image, copy all the files and run next
|
||||||
|
FROM base AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV NODE_ENV production
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED 1
|
||||||
|
|
||||||
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
|
||||||
|
# Set the correct permission for prerender cache
|
||||||
|
RUN mkdir .next
|
||||||
|
RUN chown nextjs:nodejs .next
|
||||||
|
|
||||||
|
# Automatically leverage output traces to reduce image size
|
||||||
|
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
|
# Use custom port 3100 instead of 3000
|
||||||
|
EXPOSE 3100
|
||||||
|
|
||||||
|
ENV PORT 3100
|
||||||
|
ENV HOSTNAME "0.0.0.0"
|
||||||
|
|
||||||
|
CMD ["node", "server.js"]
|
||||||
193
autotask-app/app/addigy-devices/page.tsx
Normal file
193
autotask-app/app/addigy-devices/page.tsx
Normal file
|
|
@ -0,0 +1,193 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { AddigyDevice } from '@/lib/types/addigy';
|
||||||
|
|
||||||
|
export default function AddigyDevicesPage() {
|
||||||
|
const [devices, setDevices] = useState<AddigyDevice[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [filterOnline, setFilterOnline] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchDevices();
|
||||||
|
}, [filterOnline]);
|
||||||
|
|
||||||
|
const fetchDevices = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = filterOnline
|
||||||
|
? '/api/addigy-devices?online=true'
|
||||||
|
: '/api/addigy-devices';
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
setDevices(result.data);
|
||||||
|
} else {
|
||||||
|
setError(result.error || 'Failed to fetch devices');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Unknown error');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto p-6">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h1 className="text-3xl font-bold">Addigy Devices</h1>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<label className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={filterOnline}
|
||||||
|
onChange={(e) => setFilterOnline(e.target.checked)}
|
||||||
|
className="w-4 h-4"
|
||||||
|
/>
|
||||||
|
<span>Online Only</span>
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
onClick={fetchDevices}
|
||||||
|
disabled={loading}
|
||||||
|
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? 'Loading...' : 'Refresh'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded mb-4">
|
||||||
|
<strong>Error:</strong> {error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||||||
|
<p className="mt-4 text-gray-600">Loading devices...</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="mb-4 text-gray-600">
|
||||||
|
Found {devices.length} device{devices.length !== 1 ? 's' : ''}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white shadow-md rounded-lg overflow-hidden">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead className="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Device Name
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Model
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
OS Version
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Current User
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Status
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Free Disk
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Security
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="bg-white divide-y divide-gray-200">
|
||||||
|
{devices.map((device) => (
|
||||||
|
<tr key={device.agentid} className="hover:bg-gray-50">
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div className="text-sm font-medium text-gray-900">
|
||||||
|
{device['Device Name']}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
{device['Serial Number'] || 'N/A'}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||||
|
{device['Device Model Name'] || 'Unknown'}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||||
|
{device['MAC OS X Version'] ||
|
||||||
|
device['iOS Version'] ||
|
||||||
|
'N/A'}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||||
|
{device['Current User'] || 'N/A'}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<span
|
||||||
|
className={`px-2 py-1 inline-flex text-xs leading-5 font-semibold rounded-full ${
|
||||||
|
device.online
|
||||||
|
? 'bg-green-100 text-green-800'
|
||||||
|
: 'bg-gray-100 text-gray-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{device.online ? 'Online' : 'Offline'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm">
|
||||||
|
{device['Free Disk Percentage'] !== undefined ? (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span
|
||||||
|
className={`${
|
||||||
|
device['Free Disk Percentage'] < 20
|
||||||
|
? 'text-red-600'
|
||||||
|
: device['Free Disk Percentage'] < 40
|
||||||
|
? 'text-yellow-600'
|
||||||
|
: 'text-green-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{device['Free Disk Percentage']}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
'N/A'
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span
|
||||||
|
className={`text-xs ${
|
||||||
|
device['Firewall Enabled']
|
||||||
|
? 'text-green-600'
|
||||||
|
: 'text-red-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
FW: {device['Firewall Enabled'] ? '✓' : '✗'}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`text-xs ${
|
||||||
|
device['FileVault Enabled']
|
||||||
|
? 'text-green-600'
|
||||||
|
: 'text-red-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
FV: {device['FileVault Enabled'] ? '✓' : '✗'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
61
autotask-app/app/api/addigy-devices/route.ts
Normal file
61
autotask-app/app/api/addigy-devices/route.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { getAddigyClient } from '@/lib/services/addigy-factory';
|
||||||
|
import { getCachedData, setCachedData } from '@/lib/services/redis-client';
|
||||||
|
|
||||||
|
export async function GET(request: Request) {
|
||||||
|
try {
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const policyId = searchParams.get('policyId');
|
||||||
|
const online = searchParams.get('online');
|
||||||
|
|
||||||
|
// Create cache key based on query parameters
|
||||||
|
const cacheKey = `addigy:devices:${policyId || 'all'}:${online || 'all'}`;
|
||||||
|
|
||||||
|
// Try to get cached data
|
||||||
|
const cachedDevices = await getCachedData<any[]>(cacheKey);
|
||||||
|
if (cachedDevices) {
|
||||||
|
console.log(`Cache hit for key: ${cacheKey}`);
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: cachedDevices,
|
||||||
|
count: cachedDevices.length,
|
||||||
|
cached: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Cache miss for key: ${cacheKey}, fetching from API`);
|
||||||
|
const addigyClient = getAddigyClient();
|
||||||
|
|
||||||
|
let devices;
|
||||||
|
|
||||||
|
if (policyId) {
|
||||||
|
// Get devices by policy
|
||||||
|
devices = await addigyClient.getDevicesByPolicy(policyId);
|
||||||
|
} else if (online === 'true') {
|
||||||
|
// Get only online devices
|
||||||
|
devices = await addigyClient.getOnlineDevices();
|
||||||
|
} else {
|
||||||
|
// Get all devices
|
||||||
|
devices = await addigyClient.getAllDevices();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the result for 5 minutes
|
||||||
|
await setCachedData(cacheKey, devices, 300);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: devices,
|
||||||
|
count: devices.length,
|
||||||
|
cached: false,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching Addigy devices:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
24
autotask-app/app/api/addigy-policies/route.ts
Normal file
24
autotask-app/app/api/addigy-policies/route.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { getAddigyClient } from '@/lib/services/addigy-factory';
|
||||||
|
|
||||||
|
export async function GET(request: Request) {
|
||||||
|
try {
|
||||||
|
const addigyClient = getAddigyClient();
|
||||||
|
const policies = await addigyClient.getAllPolicies();
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
data: policies,
|
||||||
|
count: policies.length,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching Addigy policies:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error',
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -39,7 +39,7 @@ export async function GET(
|
||||||
let assignedResourceName = null;
|
let assignedResourceName = null;
|
||||||
if (ticket.assignedResourceID) {
|
if (ticket.assignedResourceID) {
|
||||||
try {
|
try {
|
||||||
const resource = await client.getEntityById('Resources', ticket.assignedResourceID);
|
const resource = await client.getEntityById('Resources', ticket.assignedResourceID) as any;
|
||||||
assignedResourceName = resource ? `${resource.firstName} ${resource.lastName}` : null;
|
assignedResourceName = resource ? `${resource.firstName} ${resource.lastName}` : null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching resource:', err);
|
console.error('Error fetching resource:', err);
|
||||||
|
|
|
||||||
|
|
@ -42,47 +42,46 @@ export async function GET(
|
||||||
const rmmClient = getDattoRMMClient();
|
const rmmClient = getDattoRMMClient();
|
||||||
|
|
||||||
// First priority: Match by RMM Device UID if available
|
// First priority: Match by RMM Device UID if available
|
||||||
if (autotaskDevice.rmmDeviceUID) {
|
if (autotaskDevice?.rmmDeviceUID) {
|
||||||
const devices = await rmmClient.getAllDevices();
|
const devices = await rmmClient.getAllDevices();
|
||||||
rmmDevice = devices.find(d => d.uid === autotaskDevice.rmmDeviceUID) || null;
|
rmmDevice = devices.find(d => d.uid === autotaskDevice?.rmmDeviceUID) || null;
|
||||||
if (rmmDevice) {
|
if (rmmDevice) {
|
||||||
console.log('Matched by RMM UID:', rmmDevice.uid);
|
console.log('Matched by RMM UID:', rmmDevice.uid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Second priority: Match by RMM Device ID if available
|
// Second priority: Match by RMM Device ID if available
|
||||||
if (!rmmDevice && autotaskDevice.rmmDeviceID) {
|
if (!rmmDevice && autotaskDevice?.rmmDeviceID) {
|
||||||
try {
|
try {
|
||||||
rmmDevice = await rmmClient.getDeviceById(autotaskDevice.rmmDeviceID);
|
rmmDevice = await rmmClient.getDeviceById(autotaskDevice.rmmDeviceID);
|
||||||
if (rmmDevice) {
|
if (rmmDevice) {
|
||||||
console.log('Matched by RMM Device ID:', autotaskDevice.rmmDeviceID);
|
console.log('Matched by RMM ID:', rmmDevice.id);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log('Could not fetch by RMM Device ID');
|
console.log('Could not find device by RMM ID:', autotaskDevice.rmmDeviceID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Third priority: Get devices for this specific company and match
|
// Third priority: Match by serial number
|
||||||
if (!rmmDevice && companyName) {
|
if (!rmmDevice && autotaskDevice?.serialNumber) {
|
||||||
const companyDevices = await rmmClient.getDevicesByCompanyName(companyName);
|
const devices = await rmmClient.getAllDevices();
|
||||||
console.log(`Found ${companyDevices.length} RMM devices for company: ${companyName}`);
|
rmmDevice = devices.find(d =>
|
||||||
|
d.serialNumber?.toLowerCase() === autotaskDevice?.serialNumber?.toLowerCase()
|
||||||
// Try to match within company devices only
|
) || null;
|
||||||
if (autotaskDevice.serialNumber) {
|
if (rmmDevice) {
|
||||||
rmmDevice = companyDevices.find(d =>
|
console.log('Matched by serial number:', rmmDevice.serialNumber);
|
||||||
d.serialNumber === autotaskDevice.serialNumber
|
|
||||||
) || null;
|
|
||||||
if (rmmDevice) console.log('Matched by serial number within company');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!rmmDevice && autotaskDevice.rmmDeviceAuditHostname) {
|
|
||||||
rmmDevice = companyDevices.find(d =>
|
|
||||||
d.hostname?.toLowerCase() === autotaskDevice.rmmDeviceAuditHostname?.toLowerCase()
|
|
||||||
) || null;
|
|
||||||
if (rmmDevice) console.log('Matched by hostname within company');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fourth priority: Match by hostname
|
||||||
|
if (!rmmDevice && autotaskDevice?.rmmDeviceAuditHostname) {
|
||||||
|
const devices = await rmmClient.getAllDevices();
|
||||||
|
rmmDevice = devices.find(d =>
|
||||||
|
d.hostname?.toLowerCase() === autotaskDevice?.rmmDeviceAuditHostname?.toLowerCase()
|
||||||
|
) || null;
|
||||||
|
if (rmmDevice) console.log('Matched by hostname within company');
|
||||||
|
}
|
||||||
|
|
||||||
if (!rmmDevice) {
|
if (!rmmDevice) {
|
||||||
console.log('No RMM device match found');
|
console.log('No RMM device match found');
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ export async function GET(request: NextRequest) {
|
||||||
matchedBy: 'RMM UID'
|
matchedBy: 'RMM UID'
|
||||||
});
|
});
|
||||||
matchedAutotaskIds.add(autotaskMatch.id);
|
matchedAutotaskIds.add(autotaskMatch.id);
|
||||||
matchedRmmIds.add(rmmDevice.id);
|
matchedRmmIds.add(String(rmmDevice.id));
|
||||||
matched = true;
|
matched = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -121,7 +121,7 @@ export async function GET(request: NextRequest) {
|
||||||
matchedBy: 'Serial Number'
|
matchedBy: 'Serial Number'
|
||||||
});
|
});
|
||||||
matchedAutotaskIds.add(autotaskMatch.id);
|
matchedAutotaskIds.add(autotaskMatch.id);
|
||||||
matchedRmmIds.add(rmmDevice.id);
|
matchedRmmIds.add(String(rmmDevice.id));
|
||||||
matched = true;
|
matched = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -143,7 +143,7 @@ export async function GET(request: NextRequest) {
|
||||||
matchedBy: 'Hostname'
|
matchedBy: 'Hostname'
|
||||||
});
|
});
|
||||||
matchedAutotaskIds.add(autotaskMatch.id);
|
matchedAutotaskIds.add(autotaskMatch.id);
|
||||||
matchedRmmIds.add(rmmDevice.id);
|
matchedRmmIds.add(String(rmmDevice.id));
|
||||||
matched = true;
|
matched = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -166,7 +166,7 @@ export async function GET(request: NextRequest) {
|
||||||
matchedBy: 'IP Address'
|
matchedBy: 'IP Address'
|
||||||
});
|
});
|
||||||
matchedAutotaskIds.add(autotaskMatch.id);
|
matchedAutotaskIds.add(autotaskMatch.id);
|
||||||
matchedRmmIds.add(rmmDevice.id);
|
matchedRmmIds.add(String(rmmDevice.id));
|
||||||
matched = true;
|
matched = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,10 @@ export async function GET(
|
||||||
|
|
||||||
// Get assigned resource name if available
|
// Get assigned resource name if available
|
||||||
let assignedResourceName = null;
|
let assignedResourceName = null;
|
||||||
if (ticket.assignedResourceID) {
|
const ticketData = ticket as any;
|
||||||
|
if (ticketData.assignedResourceID) {
|
||||||
try {
|
try {
|
||||||
const resource = await client.getEntityById('Resources', ticket.assignedResourceID);
|
const resource = await client.getEntityById('Resources', ticketData.assignedResourceID) as any;
|
||||||
assignedResourceName = resource ? `${resource.firstName} ${resource.lastName}` : null;
|
assignedResourceName = resource ? `${resource.firstName} ${resource.lastName}` : null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching resource:', err);
|
console.error('Error fetching resource:', err);
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ export async function GET(
|
||||||
let resourceName = null;
|
let resourceName = null;
|
||||||
if (entry.resourceID) {
|
if (entry.resourceID) {
|
||||||
try {
|
try {
|
||||||
const resource = await client.getEntityById('Resources', entry.resourceID);
|
const resource = await client.getEntityById('Resources', entry.resourceID) as any;
|
||||||
resourceName = resource ? `${resource.firstName} ${resource.lastName}` : null;
|
resourceName = resource ? `${resource.firstName} ${resource.lastName}` : null;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching resource:', err);
|
console.error('Error fetching resource:', err);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { getAutotaskClient } from '@/lib/services/autotask-factory';
|
import { getAutotaskClient } from '@/lib/services/autotask-factory';
|
||||||
import { Ticket } from '@/lib/types/autotask';
|
import { Ticket } from '@/lib/types/autotask';
|
||||||
|
import { getCachedData, setCachedData } from '@/lib/services/redis-client';
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, Suspense } from 'react';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
@ -69,7 +69,7 @@ interface DeviceComparison {
|
||||||
matchedBy?: string;
|
matchedBy?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ConfigurationItemsPage() {
|
function ConfigurationItemsContent() {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const [selectedCompany, setSelectedCompany] = useState<number | undefined>();
|
const [selectedCompany, setSelectedCompany] = useState<number | undefined>();
|
||||||
const [selectedCompanyName, setSelectedCompanyName] = useState<string>('');
|
const [selectedCompanyName, setSelectedCompanyName] = useState<string>('');
|
||||||
|
|
@ -160,9 +160,9 @@ export default function ConfigurationItemsPage() {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle company selection
|
// Handle company selection
|
||||||
const handleCompanyChange = (companyId: number | undefined, companyName: string) => {
|
const handleCompanyChange = (companyId: number | undefined, companyName?: string) => {
|
||||||
setSelectedCompany(companyId);
|
setSelectedCompany(companyId);
|
||||||
setSelectedCompanyName(companyName);
|
setSelectedCompanyName(companyName || '');
|
||||||
setSelectedItems(new Set()); // Clear selections when company changes
|
setSelectedItems(new Set()); // Clear selections when company changes
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -756,3 +756,11 @@ export default function ConfigurationItemsPage() {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function ConfigurationItemsPage() {
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<div className="p-8">Loading...</div>}>
|
||||||
|
<ConfigurationItemsContent />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
67
autotask-app/docker-compose.yml
Normal file
67
autotask-app/docker-compose.yml
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Redis cache service on custom port 6380 (instead of default 6379)
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
container_name: psa-utils-redis
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "6380:6379"
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
command: redis-server --appendonly yes
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# Next.js application on port 3100 (instead of 3000)
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: psa-utils-app
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "3100:3100"
|
||||||
|
env_file:
|
||||||
|
- .env.local
|
||||||
|
environment:
|
||||||
|
# Application settings
|
||||||
|
NODE_ENV: production
|
||||||
|
PORT: 3100
|
||||||
|
|
||||||
|
# Redis configuration
|
||||||
|
REDIS_URL: redis://redis:6379
|
||||||
|
|
||||||
|
# Autotask API Configuration
|
||||||
|
AUTOTASK_API_URL: ${AUTOTASK_API_URL}
|
||||||
|
AUTOTASK_USERNAME: ${AUTOTASK_USERNAME}
|
||||||
|
AUTOTASK_SECRET: ${AUTOTASK_SECRET}
|
||||||
|
AUTOTASK_API_INTEGRATION_CODE: ${AUTOTASK_API_INTEGRATION_CODE}
|
||||||
|
|
||||||
|
# Datto RMM API Configuration
|
||||||
|
DATTO_RMM_API_URL: ${DATTO_RMM_API_URL}
|
||||||
|
DATTO_RMM_API_KEY: ${DATTO_RMM_API_KEY}
|
||||||
|
DATTO_RMM_API_SECRET: ${DATTO_RMM_API_SECRET}
|
||||||
|
|
||||||
|
# Addigy API Configuration
|
||||||
|
ADDIGY_API_URL: ${ADDIGY_API_URL}
|
||||||
|
ADDIGY_API_TOKEN: ${ADDIGY_API_TOKEN}
|
||||||
|
ADDIGY_ORG_ID: ${ADDIGY_ORG_ID}
|
||||||
|
depends_on:
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
volumes:
|
||||||
|
# Mount .env.local for development (remove in production)
|
||||||
|
- ./.env.local:/app/.env.local:ro
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
redis_data:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
name: psa-utils-network
|
||||||
558
autotask-app/lib/services/addigy-client.ts
Normal file
558
autotask-app/lib/services/addigy-client.ts
Normal file
|
|
@ -0,0 +1,558 @@
|
||||||
|
import {
|
||||||
|
AddigyConfig,
|
||||||
|
AddigyDevice,
|
||||||
|
AddigyPolicy,
|
||||||
|
AddigyOrganization,
|
||||||
|
AddigyDeviceWithApps,
|
||||||
|
AddigyApplication,
|
||||||
|
AddigyAlert,
|
||||||
|
AddigyMaintenanceItem,
|
||||||
|
AddigyMonitoringItem,
|
||||||
|
AddigyCustomFact,
|
||||||
|
AddigySoftwareItem,
|
||||||
|
AddigyVariable,
|
||||||
|
AddigyApiResponse,
|
||||||
|
AddigyApiError,
|
||||||
|
QueryParams,
|
||||||
|
} from '@/lib/types/addigy';
|
||||||
|
|
||||||
|
export class AddigyClient {
|
||||||
|
private config: AddigyConfig;
|
||||||
|
private rateLimiter: RateLimiter;
|
||||||
|
|
||||||
|
constructor(config: AddigyConfig) {
|
||||||
|
this.config = config;
|
||||||
|
// Addigy rate limit: 1000 requests per 10 seconds = 100 requests per second
|
||||||
|
this.rateLimiter = new RateLimiter(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAuthHeaders(): Record<string, string> {
|
||||||
|
return {
|
||||||
|
'x-api-key': this.config.apiToken,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async makeApiCall<T>(
|
||||||
|
endpoint: string,
|
||||||
|
options: RequestInit = {}
|
||||||
|
): Promise<T> {
|
||||||
|
await this.rateLimiter.throttle();
|
||||||
|
|
||||||
|
const url = `${this.config.apiUrl}${endpoint}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
...this.getAuthHeaders(),
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseText = await response.text();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(`Addigy API error: ${response.status} - ${responseText}`);
|
||||||
|
|
||||||
|
let errorMessage = `API Error: ${response.statusText}`;
|
||||||
|
try {
|
||||||
|
const errorData: AddigyApiError = JSON.parse(responseText);
|
||||||
|
errorMessage = errorData.message || errorData.error || errorMessage;
|
||||||
|
} catch (parseError) {
|
||||||
|
errorMessage = responseText || errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(responseText);
|
||||||
|
} catch (parseError) {
|
||||||
|
console.error('Failed to parse API response:', parseError);
|
||||||
|
throw new Error('Invalid JSON response from Addigy API');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API call failed:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildQueryString(params: QueryParams): string {
|
||||||
|
const queryParts: string[] = [];
|
||||||
|
|
||||||
|
if (params.page !== undefined) {
|
||||||
|
queryParts.push(`page=${params.page}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.limit !== undefined) {
|
||||||
|
queryParts.push(`limit=${params.limit}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.filters && params.filters.length > 0) {
|
||||||
|
const filtersJson = JSON.stringify({ filters: params.filters });
|
||||||
|
queryParts.push(`filters=${encodeURIComponent(filtersJson)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return queryParts.length > 0 ? `?${queryParts.join('&')}` : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Device Methods
|
||||||
|
async getAllDevices(params: QueryParams = {}): Promise<AddigyDevice[]> {
|
||||||
|
// Get organization ID - either from config or fetch from policies
|
||||||
|
let orgId = this.config.organizationId;
|
||||||
|
|
||||||
|
if (!orgId) {
|
||||||
|
// Fetch first policy to get orgid
|
||||||
|
const policies = await this.getAllPolicies({ limit: 1 });
|
||||||
|
if (policies.length > 0) {
|
||||||
|
orgId = policies[0].orgid;
|
||||||
|
console.log('Using orgid from first policy:', orgId);
|
||||||
|
} else {
|
||||||
|
throw new Error('No organization ID configured and no policies found');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Addigy v2 /o/{orgid}/devices requires POST with JSON body
|
||||||
|
const body: any = {
|
||||||
|
page: params.page || 1,
|
||||||
|
per_page: params.limit || 500,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add filters if provided
|
||||||
|
if (params.filters && params.filters.length > 0) {
|
||||||
|
body.query = {
|
||||||
|
filters: params.filters,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Addigy devices request body:', JSON.stringify(body));
|
||||||
|
console.log('Fetching devices from:', `${this.config.apiUrl}/o/${orgId}/devices`);
|
||||||
|
|
||||||
|
const response = await this.makeApiCall<{ items?: any[]; data?: any[]; records?: any[] }>(
|
||||||
|
`/o/${orgId}/devices`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('Addigy response keys:', Object.keys(response));
|
||||||
|
console.log('Addigy raw items count:', response.items?.length || 0);
|
||||||
|
|
||||||
|
// Transform Addigy's nested facts structure to flat device objects
|
||||||
|
const rawItems = response.items || response.data || response.records || [];
|
||||||
|
|
||||||
|
// Field name mapping from Addigy to UI-expected format
|
||||||
|
const fieldMapping: Record<string, string> = {
|
||||||
|
'device_name': 'Device Name',
|
||||||
|
'device_model_name': 'Device Model Name',
|
||||||
|
'serial_number': 'Serial Number',
|
||||||
|
'mac_os_x_version': 'MAC OS X Version',
|
||||||
|
'ios_version': 'iOS Version',
|
||||||
|
'current_user': 'Current User',
|
||||||
|
'free_disk_percentage': 'Free Disk Percentage',
|
||||||
|
'battery_percentage': 'Battery Percentage',
|
||||||
|
'firewall_enabled': 'Firewall Enabled',
|
||||||
|
'filevault_enabled': 'FileVault Enabled',
|
||||||
|
'agent_version': 'Agent Version',
|
||||||
|
};
|
||||||
|
|
||||||
|
const devices: AddigyDevice[] = rawItems.map((item: any) => {
|
||||||
|
const device: any = {};
|
||||||
|
|
||||||
|
// Extract values from nested facts structure
|
||||||
|
if (item.facts) {
|
||||||
|
for (const [key, factObj] of Object.entries(item.facts)) {
|
||||||
|
const fact = factObj as any;
|
||||||
|
if (fact && typeof fact === 'object' && 'value' in fact) {
|
||||||
|
// Use mapped field name if available, otherwise use original
|
||||||
|
const mappedKey = fieldMapping[key] || key;
|
||||||
|
device[mappedKey] = fact.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return device as AddigyDevice;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Transformed devices count:', devices.length);
|
||||||
|
if (devices.length > 0) {
|
||||||
|
console.log('First device keys:', Object.keys(devices[0]).slice(0, 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
return devices;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDeviceById(deviceId: string): Promise<AddigyDevice | null> {
|
||||||
|
try {
|
||||||
|
const response = await this.makeApiCall<AddigyApiResponse<AddigyDevice>>(
|
||||||
|
`/devices/${deviceId}`,
|
||||||
|
{ method: 'GET' }
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data || null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to get device ${deviceId}:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDevicesByPolicy(policyId: string): Promise<AddigyDevice[]> {
|
||||||
|
return this.getAllDevices({
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
audit_field: 'policy_id',
|
||||||
|
type: 'string',
|
||||||
|
operation: 'equals',
|
||||||
|
value: policyId,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOnlineDevices(): Promise<AddigyDevice[]> {
|
||||||
|
return this.getAllDevices({
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
audit_field: 'online',
|
||||||
|
type: 'boolean',
|
||||||
|
operation: 'equals',
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDeviceApplications(deviceId: string): Promise<AddigyApplication[]> {
|
||||||
|
try {
|
||||||
|
const response = await this.makeApiCall<{ items?: AddigyApplication[]; data?: AddigyApplication[] }>(
|
||||||
|
`/devices/${deviceId}/applications`,
|
||||||
|
{ method: 'GET' }
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.items || response.data || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to get applications for device ${deviceId}:`, error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Policy Methods
|
||||||
|
async getAllPolicies(params: QueryParams = {}): Promise<AddigyPolicy[]> {
|
||||||
|
const body: any = {
|
||||||
|
page: params.page || 1,
|
||||||
|
per_page: params.limit || 100,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Fetching policies from:', `${this.config.apiUrl}/oa/policies/query`);
|
||||||
|
|
||||||
|
// Addigy policies endpoint returns array directly, not wrapped in items/data
|
||||||
|
const response = await this.makeApiCall<AddigyPolicy[]>(
|
||||||
|
`/oa/policies/query`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('Policies response type:', Array.isArray(response) ? 'array' : typeof response);
|
||||||
|
console.log('Policies count:', Array.isArray(response) ? response.length : 0);
|
||||||
|
|
||||||
|
// Response is already an array
|
||||||
|
return Array.isArray(response) ? response : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPolicyById(policyId: string): Promise<AddigyPolicy | null> {
|
||||||
|
try {
|
||||||
|
const response = await this.makeApiCall<AddigyApiResponse<AddigyPolicy>>(
|
||||||
|
`/policies/${policyId}`,
|
||||||
|
{ method: 'GET' }
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data || null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to get policy ${policyId}:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createPolicy(policy: Partial<AddigyPolicy>): Promise<AddigyPolicy> {
|
||||||
|
const response = await this.makeApiCall<AddigyApiResponse<AddigyPolicy>>(
|
||||||
|
'/policies',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(policy),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.data) {
|
||||||
|
throw new Error('Failed to create policy');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updatePolicy(
|
||||||
|
policyId: string,
|
||||||
|
updates: Partial<AddigyPolicy>
|
||||||
|
): Promise<AddigyPolicy> {
|
||||||
|
const response = await this.makeApiCall<AddigyApiResponse<AddigyPolicy>>(
|
||||||
|
`/policies/${policyId}`,
|
||||||
|
{
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify(updates),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.data) {
|
||||||
|
throw new Error('Failed to update policy');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Organization Methods
|
||||||
|
async getOrganizations(params: QueryParams = {}): Promise<AddigyOrganization[]> {
|
||||||
|
const queryString = this.buildQueryString(params);
|
||||||
|
const response = await this.makeApiCall<{ items?: AddigyOrganization[]; data?: AddigyOrganization[] }>(
|
||||||
|
`/organizations${queryString}`,
|
||||||
|
{ method: 'GET' }
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.items || response.data || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOrganizationById(orgId: string): Promise<AddigyOrganization | null> {
|
||||||
|
try {
|
||||||
|
const response = await this.makeApiCall<AddigyApiResponse<AddigyOrganization>>(
|
||||||
|
`/organizations/${orgId}`,
|
||||||
|
{ method: 'GET' }
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data || null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to get organization ${orgId}:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alert Methods
|
||||||
|
async getAlerts(params: QueryParams = {}): Promise<AddigyAlert[]> {
|
||||||
|
const queryString = this.buildQueryString(params);
|
||||||
|
const response = await this.makeApiCall<{ items?: AddigyAlert[]; data?: AddigyAlert[] }>(
|
||||||
|
`/alerts${queryString}`,
|
||||||
|
{ method: 'GET' }
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.items || response.data || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async getActiveAlerts(): Promise<AddigyAlert[]> {
|
||||||
|
return this.getAlerts({
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
audit_field: 'resolved',
|
||||||
|
type: 'boolean',
|
||||||
|
operation: 'equals',
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async resolveAlert(alertId: string): Promise<void> {
|
||||||
|
await this.makeApiCall(`/alerts/${alertId}/resolve`, {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maintenance Methods
|
||||||
|
async getMaintenanceItems(params: QueryParams = {}): Promise<AddigyMaintenanceItem[]> {
|
||||||
|
const queryString = this.buildQueryString(params);
|
||||||
|
const response = await this.makeApiCall<{ items?: AddigyMaintenanceItem[]; data?: AddigyMaintenanceItem[] }>(
|
||||||
|
`/maintenance${queryString}`,
|
||||||
|
{ method: 'GET' }
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.items || response.data || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Monitoring Methods
|
||||||
|
async getMonitoringItems(params: QueryParams = {}): Promise<AddigyMonitoringItem[]> {
|
||||||
|
const queryString = this.buildQueryString(params);
|
||||||
|
const response = await this.makeApiCall<{ items?: AddigyMonitoringItem[]; data?: AddigyMonitoringItem[] }>(
|
||||||
|
`/monitoring${queryString}`,
|
||||||
|
{ method: 'GET' }
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.items || response.data || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom Facts / Variables
|
||||||
|
async getCustomFacts(deviceId: string): Promise<AddigyCustomFact[]> {
|
||||||
|
try {
|
||||||
|
const response = await this.makeApiCall<{ items?: AddigyCustomFact[]; data?: AddigyCustomFact[] }>(
|
||||||
|
`/devices/${deviceId}/facts`,
|
||||||
|
{ method: 'GET' }
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.items || response.data || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to get custom facts for device ${deviceId}:`, error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async createCustomFact(
|
||||||
|
deviceId: string,
|
||||||
|
fact: Partial<AddigyCustomFact>
|
||||||
|
): Promise<AddigyCustomFact> {
|
||||||
|
const response = await this.makeApiCall<AddigyApiResponse<AddigyCustomFact>>(
|
||||||
|
`/devices/${deviceId}/facts`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(fact),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.data) {
|
||||||
|
throw new Error('Failed to create custom fact');
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getVariables(params: QueryParams = {}): Promise<AddigyVariable[]> {
|
||||||
|
const queryString = this.buildQueryString(params);
|
||||||
|
const response = await this.makeApiCall<{ items?: AddigyVariable[]; data?: AddigyVariable[] }>(
|
||||||
|
`/variables${queryString}`,
|
||||||
|
{ method: 'GET' }
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.items || response.data || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Software Methods
|
||||||
|
async getSoftwareItems(params: QueryParams = {}): Promise<AddigySoftwareItem[]> {
|
||||||
|
const queryString = this.buildQueryString(params);
|
||||||
|
const response = await this.makeApiCall<{ items?: AddigySoftwareItem[]; data?: AddigySoftwareItem[] }>(
|
||||||
|
`/software${queryString}`,
|
||||||
|
{ method: 'GET' }
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.items || response.data || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSoftwareVersions(softwareIdentifier: string): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const response = await this.makeApiCall<AddigyApiResponse<{ versions: string[] }>>(
|
||||||
|
`/software/${softwareIdentifier}/versions`,
|
||||||
|
{ method: 'GET' }
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data?.versions || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to get versions for software ${softwareIdentifier}:`, error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Device Actions
|
||||||
|
async assignDeviceToPolicy(deviceId: string, policyId: string): Promise<void> {
|
||||||
|
await this.makeApiCall(`/devices/${deviceId}/policy`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify({ policy_id: policyId }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeDevice(deviceId: string): Promise<void> {
|
||||||
|
await this.makeApiCall(`/devices/${deviceId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility Methods
|
||||||
|
async testConnection(): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await this.getAllPolicies({ limit: 1 });
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Addigy connection test failed:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paginated query to handle large datasets
|
||||||
|
async getAllDevicesPaginated(pageSize: number = 100): Promise<AddigyDevice[]> {
|
||||||
|
const allDevices: AddigyDevice[] = [];
|
||||||
|
let page = 1;
|
||||||
|
let hasMore = true;
|
||||||
|
|
||||||
|
while (hasMore) {
|
||||||
|
console.log(`Fetching devices page ${page} (max ${pageSize} records)...`);
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
page,
|
||||||
|
per_page: pageSize,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await this.makeApiCall<{ items?: AddigyDevice[]; data?: AddigyDevice[]; pages?: number }>(
|
||||||
|
`/devices`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const devices = response.items || response.data || [];
|
||||||
|
allDevices.push(...devices);
|
||||||
|
|
||||||
|
console.log(`Fetched ${devices.length} devices, total so far: ${allDevices.length}`);
|
||||||
|
|
||||||
|
// Check if we have more pages
|
||||||
|
hasMore = devices.length === pageSize && (!response.pages || page < response.pages);
|
||||||
|
page++;
|
||||||
|
|
||||||
|
// Safety limit to prevent infinite loops
|
||||||
|
if (page > 100) {
|
||||||
|
console.warn('Reached page limit (100) for devices');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Total devices fetched: ${allDevices.length}`);
|
||||||
|
return allDevices;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate Limiter class
|
||||||
|
class RateLimiter {
|
||||||
|
private maxRequestsPerSecond: number;
|
||||||
|
private requestTimes: number[];
|
||||||
|
|
||||||
|
constructor(maxRequestsPerSecond = 100) {
|
||||||
|
this.maxRequestsPerSecond = maxRequestsPerSecond;
|
||||||
|
this.requestTimes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async throttle(): Promise<void> {
|
||||||
|
const now = Date.now();
|
||||||
|
const oneSecondAgo = now - 1000;
|
||||||
|
|
||||||
|
// Remove old request times
|
||||||
|
this.requestTimes = this.requestTimes.filter((t) => t > oneSecondAgo);
|
||||||
|
|
||||||
|
// If at limit, wait
|
||||||
|
if (this.requestTimes.length >= this.maxRequestsPerSecond) {
|
||||||
|
const oldestRequest = this.requestTimes[0];
|
||||||
|
const waitTime = 1000 - (now - oldestRequest);
|
||||||
|
if (waitTime > 0) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.requestTimes.push(Date.now());
|
||||||
|
}
|
||||||
|
}
|
||||||
28
autotask-app/lib/services/addigy-factory.ts
Normal file
28
autotask-app/lib/services/addigy-factory.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { AddigyClient } from './addigy-client';
|
||||||
|
import { AddigyConfig } from '@/lib/types/addigy';
|
||||||
|
|
||||||
|
let cachedAddigyClient: AddigyClient | null = null;
|
||||||
|
|
||||||
|
export function getAddigyClient(): AddigyClient {
|
||||||
|
if (cachedAddigyClient) {
|
||||||
|
return cachedAddigyClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config: AddigyConfig = {
|
||||||
|
apiUrl: process.env.ADDIGY_API_URL || 'https://api.addigy.com/api/v2',
|
||||||
|
apiToken: process.env.ADDIGY_API_TOKEN || '',
|
||||||
|
organizationId: process.env.ADDIGY_ORG_ID,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validate required config
|
||||||
|
if (!config.apiToken) {
|
||||||
|
throw new Error('ADDIGY_API_TOKEN environment variable is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedAddigyClient = new AddigyClient(config);
|
||||||
|
return cachedAddigyClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearAddigyClientCache(): void {
|
||||||
|
cachedAddigyClient = null;
|
||||||
|
}
|
||||||
|
|
@ -246,7 +246,7 @@ export class DattoRMMClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (criteria.macAddress &&
|
if (criteria.macAddress &&
|
||||||
!device.macAddresses.some(mac =>
|
!device.macAddresses?.some(mac =>
|
||||||
mac.toLowerCase() === criteria.macAddress!.toLowerCase()
|
mac.toLowerCase() === criteria.macAddress!.toLowerCase()
|
||||||
)) {
|
)) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
99
autotask-app/lib/services/redis-client.ts
Normal file
99
autotask-app/lib/services/redis-client.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
import Redis from 'ioredis';
|
||||||
|
|
||||||
|
let redisClient: Redis | null = null;
|
||||||
|
|
||||||
|
export function getRedisClient(): Redis | null {
|
||||||
|
if (!process.env.REDIS_URL) {
|
||||||
|
console.log('Redis URL not configured, caching disabled');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!redisClient) {
|
||||||
|
try {
|
||||||
|
redisClient = new Redis(process.env.REDIS_URL, {
|
||||||
|
maxRetriesPerRequest: 3,
|
||||||
|
retryStrategy: (times) => {
|
||||||
|
const delay = Math.min(times * 50, 2000);
|
||||||
|
return delay;
|
||||||
|
},
|
||||||
|
reconnectOnError: (err) => {
|
||||||
|
const targetError = 'READONLY';
|
||||||
|
if (err.message.includes(targetError)) {
|
||||||
|
// Only reconnect when the error contains "READONLY"
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
redisClient.on('error', (err) => {
|
||||||
|
console.error('Redis Client Error:', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
redisClient.on('connect', () => {
|
||||||
|
console.log('Redis Client Connected');
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize Redis client:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return redisClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCachedData<T>(key: string): Promise<T | null> {
|
||||||
|
const client = getRedisClient();
|
||||||
|
if (!client) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await client.get(key);
|
||||||
|
if (data) {
|
||||||
|
return JSON.parse(data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error getting cached data for key ${key}:`, error);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setCachedData<T>(
|
||||||
|
key: string,
|
||||||
|
data: T,
|
||||||
|
ttlSeconds: number = 300 // Default 5 minutes
|
||||||
|
): Promise<void> {
|
||||||
|
const client = getRedisClient();
|
||||||
|
if (!client) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.set(key, JSON.stringify(data), 'EX', ttlSeconds);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error setting cached data for key ${key}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteCachedData(pattern: string): Promise<void> {
|
||||||
|
const client = getRedisClient();
|
||||||
|
if (!client) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const keys = await client.keys(pattern);
|
||||||
|
if (keys.length > 0) {
|
||||||
|
await client.del(...keys);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error deleting cached data for pattern ${pattern}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function flushCache(): Promise<void> {
|
||||||
|
const client = getRedisClient();
|
||||||
|
if (!client) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.flushdb();
|
||||||
|
console.log('Cache flushed successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error flushing cache:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
213
autotask-app/lib/types/addigy.ts
Normal file
213
autotask-app/lib/types/addigy.ts
Normal file
|
|
@ -0,0 +1,213 @@
|
||||||
|
// Addigy API Types
|
||||||
|
|
||||||
|
export interface AddigyConfig {
|
||||||
|
apiUrl: string;
|
||||||
|
apiToken: string;
|
||||||
|
organizationId?: string; // Parent organization ID if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginationParams {
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FilterOperation {
|
||||||
|
audit_field: string;
|
||||||
|
type: 'string' | 'list' | 'number' | 'boolean' | 'date';
|
||||||
|
operation: 'equals' | 'contains' | 'greater_than' | 'less_than' | 'in' | 'not_in';
|
||||||
|
value: string | number | boolean | string[] | number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueryParams {
|
||||||
|
filters?: FilterOperation[];
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddigyDevice {
|
||||||
|
agentid: string;
|
||||||
|
'Device Name': string;
|
||||||
|
'Device Model Name': string;
|
||||||
|
'MAC OS X Version'?: string;
|
||||||
|
'iOS Version'?: string;
|
||||||
|
'Processor Type'?: string;
|
||||||
|
'Processor Speed (GHz)'?: number;
|
||||||
|
'Total Disk Space (GB)'?: number;
|
||||||
|
'Free Disk Space (GB)'?: number;
|
||||||
|
'Free Disk Percentage'?: number;
|
||||||
|
'Battery Percentage'?: number;
|
||||||
|
'Battery Charging'?: boolean;
|
||||||
|
'Battery Capacity Loss Percentage'?: number;
|
||||||
|
'Current User'?: string;
|
||||||
|
'Serial Number'?: string;
|
||||||
|
'Displays Serial Number'?: string[];
|
||||||
|
'Agent Version': string;
|
||||||
|
policy_id: string;
|
||||||
|
online: boolean;
|
||||||
|
'Firewall Enabled'?: boolean;
|
||||||
|
'FileVault Enabled'?: boolean;
|
||||||
|
'Remote Login Enabled'?: boolean;
|
||||||
|
'XCode Installed'?: boolean;
|
||||||
|
'SMART Failing'?: boolean;
|
||||||
|
'Has Wireless'?: boolean;
|
||||||
|
Timezone?: string;
|
||||||
|
'Warranty Expiration Date'?: string;
|
||||||
|
'Warranty Days Left'?: number;
|
||||||
|
'TeamViewer Client Id'?: string;
|
||||||
|
'Crashplan Days Since Last Backup'?: number;
|
||||||
|
'Last Check In'?: string;
|
||||||
|
// Additional fields from device audits
|
||||||
|
[key: string]: string | number | boolean | string[] | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddigyPolicy {
|
||||||
|
policyId: string;
|
||||||
|
orgid: string;
|
||||||
|
name: string;
|
||||||
|
parent?: string | null;
|
||||||
|
color?: string;
|
||||||
|
icon?: string;
|
||||||
|
download_path?: string;
|
||||||
|
agent_path?: string;
|
||||||
|
last_deployed?: string;
|
||||||
|
creation_time?: number;
|
||||||
|
agent_version?: string;
|
||||||
|
ignore_updates?: boolean;
|
||||||
|
instructions?: any[];
|
||||||
|
vnc_settings?: any;
|
||||||
|
splashtop_settings?: any;
|
||||||
|
ssh_settings?: any;
|
||||||
|
system_updates_settings?: any;
|
||||||
|
collector_settings?: any;
|
||||||
|
prebuilt_app_settings?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddigyOrganization {
|
||||||
|
orgid: string;
|
||||||
|
name: string;
|
||||||
|
parent_org_id?: string;
|
||||||
|
domain?: string;
|
||||||
|
created_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddigyApplication {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
path: string;
|
||||||
|
bundle_id?: string;
|
||||||
|
installed_date?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddigyDeviceWithApps {
|
||||||
|
agentid: string;
|
||||||
|
'Device Name': string;
|
||||||
|
'Device Model Name': string;
|
||||||
|
'MAC OS X Version'?: string;
|
||||||
|
'iOS Version'?: string;
|
||||||
|
'Agent Version': string;
|
||||||
|
policy_id: string;
|
||||||
|
online: boolean;
|
||||||
|
'Serial Number'?: string;
|
||||||
|
'Current User'?: string;
|
||||||
|
'Free Disk Percentage'?: number;
|
||||||
|
'Battery Percentage'?: number;
|
||||||
|
'Firewall Enabled'?: boolean;
|
||||||
|
'FileVault Enabled'?: boolean;
|
||||||
|
installed_applications?: AddigyApplication[];
|
||||||
|
[key: string]: string | number | boolean | string[] | AddigyApplication[] | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddigyAlert {
|
||||||
|
id: string;
|
||||||
|
device_id: string;
|
||||||
|
alert_type: string;
|
||||||
|
severity: 'critical' | 'warning' | 'info';
|
||||||
|
message: string;
|
||||||
|
created_at: string;
|
||||||
|
resolved: boolean;
|
||||||
|
resolved_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddigyMaintenanceItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
policy_id?: string;
|
||||||
|
enabled: boolean;
|
||||||
|
schedule?: string;
|
||||||
|
last_run?: string;
|
||||||
|
next_run?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddigyMonitoringItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
policy_id?: string;
|
||||||
|
enabled: boolean;
|
||||||
|
condition: string;
|
||||||
|
alert_level: 'critical' | 'warning' | 'info';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddigyCustomFact {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
value: string | number | boolean;
|
||||||
|
device_id: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddigySoftwareItem {
|
||||||
|
identifier: string;
|
||||||
|
name: string;
|
||||||
|
version?: string;
|
||||||
|
versions?: string[];
|
||||||
|
category?: string;
|
||||||
|
description?: string;
|
||||||
|
install_type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddigyVariable {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
policy_id?: string;
|
||||||
|
device_id?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddigyApiResponse<T> {
|
||||||
|
data?: T;
|
||||||
|
items?: T[];
|
||||||
|
total?: number;
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
pages?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddigyApiError {
|
||||||
|
error: string;
|
||||||
|
message: string;
|
||||||
|
status_code?: number;
|
||||||
|
details?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enums for common values
|
||||||
|
export enum AddigyDeviceStatus {
|
||||||
|
Online = 'online',
|
||||||
|
Offline = 'offline',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AddigyAlertSeverity {
|
||||||
|
Critical = 'critical',
|
||||||
|
Warning = 'warning',
|
||||||
|
Info = 'info',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AddigyDeviceType {
|
||||||
|
Mac = 'mac',
|
||||||
|
iPhone = 'iphone',
|
||||||
|
iPad = 'ipad',
|
||||||
|
AppleTV = 'appletv',
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,12 @@ import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
/* config options here */
|
||||||
|
output: 'standalone',
|
||||||
reactCompiler: true,
|
reactCompiler: true,
|
||||||
|
// Allow external images if needed
|
||||||
|
images: {
|
||||||
|
domains: [],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
"ioredis": "^5.8.2",
|
||||||
"lucide-react": "^0.548.0",
|
"lucide-react": "^0.548.0",
|
||||||
"next": "16.0.0",
|
"next": "16.0.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
|
@ -32,6 +33,7 @@
|
||||||
"react-day-picker": "^9.11.1",
|
"react-day-picker": "^9.11.1",
|
||||||
"react-dom": "19.2.0",
|
"react-dom": "19.2.0",
|
||||||
"react-hook-form": "^7.65.0",
|
"react-hook-form": "^7.65.0",
|
||||||
|
"redis": "^5.9.0",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"zod": "^4.1.12"
|
"zod": "^4.1.12"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue