import type { HttpContext } from '@adonisjs/core/http' import ConfigurationService, { type ConfigurationFilters, type CreateConfigurationDto, type UpdateConfigurationDto, } from '#services/configuration_service' import ConfigurationEngine from '#services/configuration_engine' import ConfigurationModel from '#models/configuration' import logger from '@adonisjs/core/services/logger' /** * Configurations API Controller * * Handles REST endpoints for Configuration management: * - GET /api/v1/configurations (list with pagination and filters) * - GET /api/v1/configurations/:id (get single with compliance stats) * - POST /api/v1/configurations (create with validation and overlap detection) * - PUT /api/v1/configurations/:id (update with validation and overlap detection) * - DELETE /api/v1/configurations/:id (delete) * - PATCH /api/v1/configurations/:id/toggle (toggle enabled status) * - PATCH /api/v1/configurations/:id/tags (add/remove tags) * - GET /api/v1/configurations/:id/history (version history) * - POST /api/v1/configurations/:id/dry-run (dry-run evaluation) */ export default class ConfigurationsController { private configService: ConfigurationService private engine: ConfigurationEngine constructor() { this.configService = new ConfigurationService() this.engine = new ConfigurationEngine() } /** * GET /api/v1/configurations * List configurations with pagination and filters */ async index({ request, response }: HttpContext) { try { const filters: ConfigurationFilters = { page: Number(request.input('page', 1)), perPage: Number(request.input('perPage', 20)), tag: request.input('tag'), name: request.input('name') || undefined, enabled: request.input('enabled') !== undefined ? request.input('enabled') === 'true' : undefined, productClass: request.input('productClass'), } const result = await this.configService.findAll(filters) return response.ok(result) } catch (error: any) { logger.error({ error: error.message }, '[ConfigAPI] List configurations failed') return response.status(500).json({ message: 'Failed to list configurations' }) } } /** * GET /api/v1/configurations/tags * Return all distinct tags across configurations */ async listTags({ response }: HttpContext) { try { const tags = await ConfigurationModel.distinctTags() return response.ok(tags) } catch (error: any) { return response.status(500).json({ message: 'Failed to list tags' }) } } /** * GET /api/v1/configurations/:id * Get single configuration with compliance statistics */ async show({ params, response }: HttpContext) { try { const config = await this.configService.findById(params.id) if (!config) { return response.status(404).json({ message: 'Configuration not found' }) } // TODO: Add compliance statistics from device compliance info return response.ok(config) } catch (error: any) { logger.error({ error: error.message, id: params.id }, '[ConfigAPI] Get configuration failed') return response.status(500).json({ message: 'Failed to retrieve configuration' }) } } /** * POST /api/v1/configurations * Create new configuration with validation and overlap detection */ async store({ request, response }: HttpContext) { try { const data = request.only([ 'identifier', 'name', 'version', 'priority', 'policy', 'enabled', 'tags', 'criteria', 'inventory', 'desiredParameters', 'firmwareTarget', 'schedule', ]) as CreateConfigurationDto // Check for overlaps const overlapResult = await this.configService.detectOverlap(data.criteria || {}, data.priority) if (overlapResult && overlapResult.overlappingConfigurations && overlapResult.overlappingConfigurations.length > 0 && !request.input('ignoreOverlap')) { return response.status(409).json({ message: 'Configuration overlaps with existing ones', overlaps: overlapResult.overlappingConfigurations, }) } // Create configuration const config = await this.configService.create(data) return response.status(201).json(config) } catch (error: any) { logger.error({ error: error.message }, '[ConfigAPI] Create configuration failed') if (error.field) { return response.status(422).json({ message: error.message, field: error.field }) } return response.status(500).json({ message: 'Failed to create configuration' }) } } /** * PUT /api/v1/configurations/:id * Update configuration with validation and overlap detection */ async update({ params, request, response }: HttpContext) { try { const data = request.only([ 'name', 'priority', 'policy', 'enabled', 'tags', 'criteria', 'inventory', 'desiredParameters', 'firmwareTarget', 'schedule', 'bumpType', ]) as UpdateConfigurationDto // Check for overlaps if priority changed if (data.priority) { const overlapResult = await this.configService.detectOverlap( data.criteria || {}, data.priority, params.id ) if (overlapResult && overlapResult.overlappingConfigurations && overlapResult.overlappingConfigurations.length > 0 && !request.input('ignoreOverlap')) { return response.status(409).json({ message: 'Configuration overlaps with existing ones', overlaps: overlapResult.overlappingConfigurations, }) } } // Update configuration const config = await this.configService.update(params.id, data) if (!config) { return response.status(404).json({ message: 'Configuration not found' }) } return response.ok(config) } catch (error: any) { logger.error({ error: error.message, id: params.id }, '[ConfigAPI] Update configuration failed') if (error.field) { return response.status(422).json({ message: error.message, field: error.field }) } return response.status(500).json({ message: 'Failed to update configuration' }) } } /** * DELETE /api/v1/configurations/:id * Delete configuration */ async destroy({ params, response }: HttpContext) { try { await this.configService.delete(params.id) return response.status(204).send(null) } catch (error: any) { logger.error({ error: error.message, id: params.id }, '[ConfigAPI] Delete configuration failed') return response.status(500).json({ message: 'Failed to delete configuration' }) } } /** * PATCH /api/v1/configurations/:id/toggle * Toggle enabled status */ async toggleEnabled({ params, request, response }: HttpContext) { try { const enabled = request.input('enabled') as boolean const config = await this.configService.toggleEnabled(params.id, enabled) if (!config) { return response.status(404).json({ message: 'Configuration not found' }) } return response.ok(config) } catch (error: any) { logger.error({ error: error.message, id: params.id }, '[ConfigAPI] Toggle configuration failed') return response.status(500).json({ message: 'Failed to toggle configuration' }) } } /** * PATCH /api/v1/configurations/:id/tags * Add or remove tags */ async manageTags({ params, request, response }: HttpContext) { try { const action = request.input('action') as 'add' | 'remove' const tag = request.input('tag') as string if (!action || !['add', 'remove'].includes(action)) { return response.status(422).json({ message: 'Invalid action: must be "add" or "remove"' }) } if (!tag) { return response.status(422).json({ message: 'Tag is required' }) } const config = await this.configService.findById(params.id) if (!config) { return response.status(404).json({ message: 'Configuration not found' }) } if (action === 'add') { if (!config.tags.includes(tag)) { config.tags.push(tag) } } else { const index = config.tags.indexOf(tag) if (index > -1) { config.tags.splice(index, 1) } } const updated = await this.configService.update(params.id, { tags: config.tags }) return response.ok(updated) } catch (error: any) { logger.error({ error: error.message, id: params.id }, '[ConfigAPI] Manage tags failed') return response.status(500).json({ message: 'Failed to manage tags' }) } } /** * GET /api/v1/configurations/:id/history * Get configuration version history */ async history({ params, request, response }: HttpContext) { try { const page = Number(request.input('page', 1)) const limit = Number(request.input('limit', 10)) const result = await this.configService.getHistory(params.id, page, limit) return response.ok(result) } catch (error: any) { logger.error({ error: error.message, id: params.id }, '[ConfigAPI] Get history failed') return response.status(500).json({ message: 'Failed to retrieve configuration history' }) } } /** * GET /api/v1/configurations/:id/compliance * Compliance report: all devices matching criteria + their compliance status */ async compliance({ params, response }: HttpContext) { try { const report = await this.engine.complianceReport(params.id) return response.ok(report) } catch (error: any) { logger.error({ error: error.message, id: params.id }, '[ConfigAPI] Compliance report failed') if (error.message.includes('not found')) { return response.status(404).json({ message: error.message }) } return response.status(500).json({ message: 'Compliance report failed' }) } } /** * POST /api/v1/configurations/:id/dry-run * Perform dry-run evaluation for a device */ async dryRun({ params, request, response }: HttpContext) { try { const deviceIdentifier = request.input('deviceIdentifier') as string if (!deviceIdentifier) { return response.status(422).json({ message: 'deviceIdentifier is required' }) } const result = await this.engine.dryRun(params.id, deviceIdentifier) return response.ok(result) } catch (error: any) { logger.error({ error: error.message, id: params.id }, '[ConfigAPI] Dry-run failed') if (error.message.includes('not found')) { return response.status(404).json({ message: error.message }) } return response.status(500).json({ message: 'Dry-run failed' }) } } }