Files
adam-acs/app/controllers/configurations_v1_controller.ts
T

288 lines
10 KiB
TypeScript

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 { createConfigurationValidator, updateConfigurationValidator } from '#validators/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 = await request.validateUsing(createConfigurationValidator) 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 = await request.validateUsing(updateConfigurationValidator) 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' })
}
}
}