import type { HttpContext } from '@adonisjs/core/http' import logger from '@adonisjs/core/services/logger' import DeviceModel from '#models/device' import { Tr069TraceService } from '#services/tr069_trace_service' import { readFileSync, existsSync, mkdirSync } from 'node:fs' import { writeFile } from 'node:fs/promises' import { join } from 'node:path' const APP_ROOT = process.cwd() const CONFIG_PATH = join(APP_ROOT, 'data', 'tr069-trace.json') const LOG_DIR = join(APP_ROOT, 'logs', 'tr069') const MAX_TAIL_LINES = 1000 export default class Tr069TraceController { private get service(): Tr069TraceService { return Tr069TraceService.getInstance() } async index({ response }: HttpContext) { return response.status(200).json({ entries: this.service.list() }) } async store({ request, response }: HttpContext) { const deviceId = request.input('deviceId') as string | undefined if (!deviceId) { return response.status(422).json({ error: 'deviceId is required' }) } if (this.service.isTraced(deviceId)) { return response.status(409).json({ error: 'Device is already being traced' }) } const record = await DeviceModel.findByIdentifier(deviceId) if (!record) { return response.status(404).json({ error: `Device ${deviceId} not found` }) } await this.service.addDevice({ deviceId, manufacturer: record.deviceId?.manufacturer ?? '', oui: record.deviceId?.oui ?? '', productClass: record.deviceId?.productClass ?? '', serialNumber: record.deviceId?.serialNumber ?? '', }) logger.info({ deviceId }, 'TR-069 trace enabled for device') const entry = this.service.list().find((traceEntry) => traceEntry.deviceId === deviceId) return response.status(201).json({ entry }) } async destroy({ params, response }: HttpContext) { const removed = await this.service.removeDevice(params.id) if (!removed) { return response.status(404).json({ error: `Device ${params.id} is not being traced` }) } logger.info({ deviceId: params.id }, 'TR-069 trace disabled for device') return response.status(204).send('') } async showLog({ params, request, response }: HttpContext) { const deviceId = params.id const lines = Math.min(Number(request.qs().lines ?? 200), MAX_TAIL_LINES) if (!this.service.isTraced(deviceId)) { return response.status(404).json({ error: `Device ${deviceId} is not being traced` }) } const filePath = join(LOG_DIR, `${deviceId}.log`) if (!existsSync(filePath)) { return response.status(200).json({ deviceId, content: '', lines: 0 }) } const raw = readFileSync(filePath, 'utf-8') const allLines = raw.split('\n') const tail = allLines.slice(-lines).join('\n') return response.status(200).json({ deviceId, content: tail, lines: allLines.length }) } async rawConfig({ response }: HttpContext) { try { if (!existsSync(CONFIG_PATH)) { return response.status(200).json({ raw: JSON.stringify({ entries: [] }, null, 2) }) } const raw = readFileSync(CONFIG_PATH, 'utf-8') return response.status(200).json({ raw }) } catch (error) { return response.status(500).json({ error: 'Failed to read config file' }) } } async saveRawConfig({ request, response }: HttpContext) { const raw = request.input('raw') as string | undefined if (!raw) { return response.status(422).json({ error: 'raw JSON content is required' }) } try { const parsed = JSON.parse(raw) if (!parsed.entries || !Array.isArray(parsed.entries)) { return response.status(422).json({ error: 'JSON must contain an "entries" array' }) } mkdirSync(join(APP_ROOT, 'data'), { recursive: true }) await writeFile(CONFIG_PATH, JSON.stringify(parsed, null, 2), 'utf-8') // Reload the service in-memory state this.service.reload() logger.info('TR-069 trace config updated via raw editor') return response.status(200).json({ entries: this.service.list() }) } catch (error) { if (error instanceof SyntaxError) { return response.status(422).json({ error: `Invalid JSON: ${error.message}` }) } return response.status(500).json({ error: 'Failed to save config file' }) } } }