288 lines
10 KiB
TypeScript
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' })
|
|
}
|
|
}
|
|
}
|