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;
|
||||
if (ticket.assignedResourceID) {
|
||||
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;
|
||||
} catch (err) {
|
||||
console.error('Error fetching resource:', err);
|
||||
|
|
|
|||
|
|
@ -42,46 +42,45 @@ export async function GET(
|
|||
const rmmClient = getDattoRMMClient();
|
||||
|
||||
// First priority: Match by RMM Device UID if available
|
||||
if (autotaskDevice.rmmDeviceUID) {
|
||||
if (autotaskDevice?.rmmDeviceUID) {
|
||||
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) {
|
||||
console.log('Matched by RMM UID:', rmmDevice.uid);
|
||||
}
|
||||
}
|
||||
|
||||
// Second priority: Match by RMM Device ID if available
|
||||
if (!rmmDevice && autotaskDevice.rmmDeviceID) {
|
||||
if (!rmmDevice && autotaskDevice?.rmmDeviceID) {
|
||||
try {
|
||||
rmmDevice = await rmmClient.getDeviceById(autotaskDevice.rmmDeviceID);
|
||||
if (rmmDevice) {
|
||||
console.log('Matched by RMM Device ID:', autotaskDevice.rmmDeviceID);
|
||||
console.log('Matched by RMM ID:', rmmDevice.id);
|
||||
}
|
||||
} 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
|
||||
if (!rmmDevice && companyName) {
|
||||
const companyDevices = await rmmClient.getDevicesByCompanyName(companyName);
|
||||
console.log(`Found ${companyDevices.length} RMM devices for company: ${companyName}`);
|
||||
|
||||
// Try to match within company devices only
|
||||
if (autotaskDevice.serialNumber) {
|
||||
rmmDevice = companyDevices.find(d =>
|
||||
d.serialNumber === autotaskDevice.serialNumber
|
||||
// Third priority: Match by serial number
|
||||
if (!rmmDevice && autotaskDevice?.serialNumber) {
|
||||
const devices = await rmmClient.getAllDevices();
|
||||
rmmDevice = devices.find(d =>
|
||||
d.serialNumber?.toLowerCase() === autotaskDevice?.serialNumber?.toLowerCase()
|
||||
) || null;
|
||||
if (rmmDevice) console.log('Matched by serial number within company');
|
||||
if (rmmDevice) {
|
||||
console.log('Matched by serial number:', rmmDevice.serialNumber);
|
||||
}
|
||||
}
|
||||
|
||||
if (!rmmDevice && autotaskDevice.rmmDeviceAuditHostname) {
|
||||
rmmDevice = companyDevices.find(d =>
|
||||
d.hostname?.toLowerCase() === autotaskDevice.rmmDeviceAuditHostname?.toLowerCase()
|
||||
// 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) {
|
||||
console.log('No RMM device match found');
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ export async function GET(request: NextRequest) {
|
|||
matchedBy: 'RMM UID'
|
||||
});
|
||||
matchedAutotaskIds.add(autotaskMatch.id);
|
||||
matchedRmmIds.add(rmmDevice.id);
|
||||
matchedRmmIds.add(String(rmmDevice.id));
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -121,7 +121,7 @@ export async function GET(request: NextRequest) {
|
|||
matchedBy: 'Serial Number'
|
||||
});
|
||||
matchedAutotaskIds.add(autotaskMatch.id);
|
||||
matchedRmmIds.add(rmmDevice.id);
|
||||
matchedRmmIds.add(String(rmmDevice.id));
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -143,7 +143,7 @@ export async function GET(request: NextRequest) {
|
|||
matchedBy: 'Hostname'
|
||||
});
|
||||
matchedAutotaskIds.add(autotaskMatch.id);
|
||||
matchedRmmIds.add(rmmDevice.id);
|
||||
matchedRmmIds.add(String(rmmDevice.id));
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -166,7 +166,7 @@ export async function GET(request: NextRequest) {
|
|||
matchedBy: 'IP Address'
|
||||
});
|
||||
matchedAutotaskIds.add(autotaskMatch.id);
|
||||
matchedRmmIds.add(rmmDevice.id);
|
||||
matchedRmmIds.add(String(rmmDevice.id));
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,9 +21,10 @@ export async function GET(
|
|||
|
||||
// Get assigned resource name if available
|
||||
let assignedResourceName = null;
|
||||
if (ticket.assignedResourceID) {
|
||||
const ticketData = ticket as any;
|
||||
if (ticketData.assignedResourceID) {
|
||||
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;
|
||||
} catch (err) {
|
||||
console.error('Error fetching resource:', err);
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export async function GET(
|
|||
let resourceName = null;
|
||||
if (entry.resourceID) {
|
||||
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;
|
||||
} catch (err) {
|
||||
console.error('Error fetching resource:', err);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getAutotaskClient } from '@/lib/services/autotask-factory';
|
||||
import { Ticket } from '@/lib/types/autotask';
|
||||
import { getCachedData, setCachedData } from '@/lib/services/redis-client';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, Suspense } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
|
@ -69,7 +69,7 @@ interface DeviceComparison {
|
|||
matchedBy?: string;
|
||||
}
|
||||
|
||||
export default function ConfigurationItemsPage() {
|
||||
function ConfigurationItemsContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const [selectedCompany, setSelectedCompany] = useState<number | undefined>();
|
||||
const [selectedCompanyName, setSelectedCompanyName] = useState<string>('');
|
||||
|
|
@ -160,9 +160,9 @@ export default function ConfigurationItemsPage() {
|
|||
});
|
||||
|
||||
// Handle company selection
|
||||
const handleCompanyChange = (companyId: number | undefined, companyName: string) => {
|
||||
const handleCompanyChange = (companyId: number | undefined, companyName?: string) => {
|
||||
setSelectedCompany(companyId);
|
||||
setSelectedCompanyName(companyName);
|
||||
setSelectedCompanyName(companyName || '');
|
||||
setSelectedItems(new Set()); // Clear selections when company changes
|
||||
};
|
||||
|
||||
|
|
@ -756,3 +756,11 @@ export default function ConfigurationItemsPage() {
|
|||
</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 &&
|
||||
!device.macAddresses.some(mac =>
|
||||
!device.macAddresses?.some(mac =>
|
||||
mac.toLowerCase() === criteria.macAddress!.toLowerCase()
|
||||
)) {
|
||||
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 = {
|
||||
/* config options here */
|
||||
output: 'standalone',
|
||||
reactCompiler: true,
|
||||
// Allow external images if needed
|
||||
images: {
|
||||
domains: [],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"ioredis": "^5.8.2",
|
||||
"lucide-react": "^0.548.0",
|
||||
"next": "16.0.0",
|
||||
"next-themes": "^0.4.6",
|
||||
|
|
@ -32,6 +33,7 @@
|
|||
"react-day-picker": "^9.11.1",
|
||||
"react-dom": "19.2.0",
|
||||
"react-hook-form": "^7.65.0",
|
||||
"redis": "^5.9.0",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"zod": "^4.1.12"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue