chore: initial commit — AdonisJS v7 TR-069 ACS

- Docker infrastructure (Dockerfile, docker-compose.yml, genieacs-sim)
- AdonisJS v7 app with Inertia.js + Vue 3
- MongoDB integration
- CWMP/TR-069 state machine engine
- Configuration engine with compliance tracking
- Branding: ADAM ACS (favicon, navbar, locales)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Kevin L'HereecandCopilot committed 2026-05-29 16:46:37 +02:00
commit d1c65dbf1e
164 files changed
+29127

No files matched your search

+136
View File
@@ -0,0 +1,136 @@
import { Db, Collection, ObjectId, Filter, UpdateFilter, Sort } from 'mongodb'
import { getMongoDb } from '#providers/mongodb_provider'
export abstract class BaseModel {
static collectionName: string
_id?: ObjectId
createdAt?: Date
updatedAt?: Date
static getDb(): Db {
return getMongoDb()
}
static getCollection(this: { collectionName: string; getDb(): Db }): Collection {
return this.getDb().collection(this.collectionName)
}
static async find(this: { collectionName: string; getDb(): Db }, filter: Filter<any> = {}): Promise<any[]> {
const collection = this.getDb().collection(this.collectionName)
return collection.find(filter).toArray()
}
static async findById(this: { collectionName: string; getDb(): Db }, id: string | ObjectId): Promise<any | null> {
const collection = this.getDb().collection(this.collectionName)
const objectId = typeof id === 'string' ? new ObjectId(id) : id
return collection.findOne({ _id: objectId })
}
static async findOne(this: { collectionName: string; getDb(): Db }, filter: Filter<any>): Promise<any | null> {
const collection = this.getDb().collection(this.collectionName)
return collection.findOne(filter)
}
static async create(this: { collectionName: string; getDb(): Db }, data: Partial<any>): Promise<any> {
const collection = this.getDb().collection(this.collectionName)
const now = new Date()
const doc = { ...data, createdAt: now, updatedAt: now }
const result = await collection.insertOne(doc)
return { ...doc, _id: result.insertedId }
}
static async createMany(this: { collectionName: string; getDb(): Db }, data: Partial<any>[]): Promise<any[]> {
const collection = this.getDb().collection(this.collectionName)
const now = new Date()
const docs = data.map((d) => ({ ...d, createdAt: now, updatedAt: now }))
const result = await collection.insertMany(docs)
return docs.map((doc, i) => ({ ...doc, _id: result.insertedIds[i] }))
}
static async updateOne(
this: { collectionName: string; getDb(): Db },
filter: Filter<any>,
update: UpdateFilter<any> | Partial<any>
): Promise<any> {
const collection = this.getDb().collection(this.collectionName)
const updateDoc = '$set' in update ? update : { $set: { ...update, updatedAt: new Date() } }
return collection.updateOne(filter, updateDoc)
}
static async updateMany(
this: { collectionName: string; getDb(): Db },
filter: Filter<any>,
update: UpdateFilter<any> | Partial<any>
): Promise<any> {
const collection = this.getDb().collection(this.collectionName)
const updateDoc = '$set' in update ? update : { $set: { ...update, updatedAt: new Date() } }
return collection.updateMany(filter, updateDoc)
}
static async deleteOne(this: { collectionName: string; getDb(): Db }, filter: Filter<any>): Promise<boolean> {
const collection = this.getDb().collection(this.collectionName)
const result = await collection.deleteOne(filter)
return result.deletedCount === 1
}
static async deleteMany(this: { collectionName: string; getDb(): Db }, filter: Filter<any>): Promise<number> {
const collection = this.getDb().collection(this.collectionName)
const result = await collection.deleteMany(filter)
return result.deletedCount
}
static async paginate(
this: { collectionName: string; getDb(): Db },
page: number,
perPage: number,
filter: Filter<any> = {},
sort: Sort = {}
): Promise<{ data: any[]; total: number; page: number; perPage: number; lastPage: number }> {
const collection = this.getDb().collection(this.collectionName)
const total = await collection.countDocuments(filter)
const lastPage = Math.max(1, Math.ceil(total / perPage))
const skip = (page - 1) * perPage
const data = await collection.find(filter).sort(sort).skip(skip).limit(perPage).toArray()
return { data, total, page, perPage, lastPage }
}
static query(this: { collectionName: string; getDb(): Db }) {
const collection = this.getDb().collection(this.collectionName)
return collection
}
async save(): Promise<void> {
const constructor = this.constructor as typeof BaseModel
const collection = constructor.getDb().collection(constructor.collectionName)
this.updatedAt = new Date()
if (!this.createdAt) {
this.createdAt = new Date()
}
const { _id, ...data } = this as any
if (_id) {
await collection.updateOne({ _id }, { $set: data })
} else {
const result = await collection.insertOne(data)
this._id = result.insertedId
}
}
async delete(): Promise<boolean> {
const constructor = this.constructor as typeof BaseModel
const collection = constructor.getDb().collection(constructor.collectionName)
if (!this._id) return false
const result = await collection.deleteOne({ _id: this._id })
return result.deletedCount === 1
}
async refresh(): Promise<void> {
const constructor = this.constructor as typeof BaseModel
const collection = constructor.getDb().collection(constructor.collectionName)
if (!this._id) return
const doc = await collection.findOne({ _id: this._id })
if (doc) {
Object.assign(this, doc)
}
}
}
@@ -0,0 +1,21 @@
import { BaseSchema } from '@adonisjs/lucid/schema'
export default class extends BaseSchema {
protected tableName = 'users'
async up() {
this.schema.createTable(this.tableName, (table) => {
table.increments('id').notNullable()
table.string('full_name').nullable()
table.string('email', 254).notNullable().unique()
table.string('password').notNullable()
table.timestamp('created_at').notNullable()
table.timestamp('updated_at').nullable()
})
}
async down() {
this.schema.dropTable(this.tableName)
}
}
@@ -0,0 +1,31 @@
import { BaseSchema } from '@adonisjs/lucid/schema'
export default class extends BaseSchema {
protected tableName = 'auth_access_tokens'
async up() {
this.schema.createTable(this.tableName, (table) => {
table.increments('id')
table
.integer('tokenable_id')
.notNullable()
.unsigned()
.references('id')
.inTable('users')
.onDelete('CASCADE')
table.string('type').notNullable()
table.string('name').nullable()
table.string('hash').notNullable()
table.text('abilities').notNullable()
table.timestamp('created_at')
table.timestamp('updated_at')
table.timestamp('last_used_at').nullable()
table.timestamp('expires_at').nullable()
})
}
async down() {
this.schema.dropTable(this.tableName)
}
}
@@ -0,0 +1,27 @@
import { MongoMigration } from '../mongo_migrator.js'
export default class CreateDevicesCollection extends MongoMigration {
async up(): Promise<void> {
const collection = this.db.collection('devices')
await collection.createIndex(
{
'deviceId.manufacturer': 1,
'deviceId.oui': 1,
'deviceId.productClass': 1,
'deviceId.serialNumber': 1,
},
{ unique: true, name: 'idx_deviceId_compound' }
)
await collection.createIndex({ identifier: 1 }, { unique: true, name: 'idx_identifier' })
await collection.createIndex({ 'deviceId.manufacturer': 1 }, { name: 'idx_manufacturer' })
await collection.createIndex({ 'deviceId.oui': 1 }, { name: 'idx_oui' })
await collection.createIndex({ 'deviceId.productClass': 1 }, { name: 'idx_productClass' })
await collection.createIndex({ 'deviceId.serialNumber': 1 }, { name: 'idx_serialNumber' })
}
async down(): Promise<void> {
await this.db.dropCollection('devices').catch(() => {})
}
}
@@ -0,0 +1,20 @@
import { MongoMigration } from '../mongo_migrator.js'
export default class CreateCommandsCollection extends MongoMigration {
async up(): Promise<void> {
const collection = this.db.collection('commands')
await collection.createIndex(
{ deviceIdentifier: 1 },
{ background: true, name: 'idx_deviceIdentifier' }
)
await collection.createIndex(
{ status: 1, priority: -1 },
{ background: true, name: 'idx_status_priority' }
)
}
async down(): Promise<void> {
await this.db.dropCollection('commands').catch(() => {})
}
}
@@ -0,0 +1,12 @@
import { MongoMigration } from '../mongo_migrator.js'
export default class CreateDiscoveryConfigsCollection extends MongoMigration {
async up(): Promise<void> {
const collection = this.db.collection('discovery_configs')
await collection.createIndex({ identifier: 1 }, { unique: true, name: 'idx_identifier' })
}
async down(): Promise<void> {
await this.db.dropCollection('discovery_configs').catch(() => {})
}
}
@@ -0,0 +1,12 @@
import { MongoMigration } from '../mongo_migrator.js'
export default class CreateProvisioningConfigsCollection extends MongoMigration {
async up(): Promise<void> {
const collection = this.db.collection('provisioning_configs')
await collection.createIndex({ identifier: 1 }, { unique: true, name: 'idx_identifier' })
}
async down(): Promise<void> {
await this.db.dropCollection('provisioning_configs').catch(() => {})
}
}
@@ -0,0 +1,19 @@
import { MongoMigration } from '../mongo_migrator.js'
export default class CreateConfigurationsCollection extends MongoMigration {
async up(): Promise<void> {
const collection = this.db.collection('configurations')
await collection.createIndex({ identifier: 1 }, { unique: true, name: 'idx_identifier' })
await collection.createIndex({ enabled: 1, priority: -1 }, { name: 'idx_enabled_priority' })
await collection.createIndex({ tags: 1 }, { name: 'idx_tags' })
await collection.createIndex({ 'criteria.productClass': 1 }, { name: 'idx_criteria_product_class' })
await collection.createIndex(
{ 'criteria.manufacturer': 1, 'criteria.productClass': 1 },
{ name: 'idx_criteria_manufacturer_product_class' }
)
}
async down(): Promise<void> {
await this.db.dropCollection('configurations').catch(() => {})
}
}
@@ -0,0 +1,19 @@
import { MongoMigration } from '../mongo_migrator.js'
export default class CreateConfigurationVersionsCollection extends MongoMigration {
async up(): Promise<void> {
const collection = this.db.collection('configuration_versions')
await collection.createIndex(
{ configurationIdentifier: 1, versionNumber: -1 },
{ name: 'idx_configuration_identifier_version_number' }
)
await collection.createIndex(
{ configurationIdentifier: 1 },
{ name: 'idx_configuration_identifier' }
)
}
async down(): Promise<void> {
await this.db.dropCollection('configuration_versions').catch(() => {})
}
}
@@ -0,0 +1,13 @@
import { MongoMigration } from '../mongo_migrator.js'
export default class CreateFirmwareEntriesCollection extends MongoMigration {
async up(): Promise<void> {
const collection = this.db.collection('firmware_entries')
await collection.createIndex({ identifier: 1 }, { unique: true, name: 'idx_identifier' })
await collection.createIndex({ productClass: 1 }, { name: 'idx_product_class' })
}
async down(): Promise<void> {
await this.db.dropCollection('firmware_entries').catch(() => {})
}
}
@@ -0,0 +1,17 @@
import { MongoMigration } from '../mongo_migrator.js'
export default class CreateMigrationRoutesCollection extends MongoMigration {
async up(): Promise<void> {
const collection = this.db.collection('migration_routes')
await collection.createIndex({ identifier: 1 }, { unique: true, name: 'idx_identifier' })
await collection.createIndex({ productClass: 1 }, { name: 'idx_product_class' })
await collection.createIndex(
{ productClass: 1, sourceVersionPattern: 1 },
{ name: 'idx_product_class_source_version' }
)
}
async down(): Promise<void> {
await this.db.dropCollection('migration_routes').catch(() => {})
}
}
@@ -0,0 +1,18 @@
import { MongoMigration } from '../mongo_migrator.js'
export default class AddComplianceToDevices extends MongoMigration {
async up(): Promise<void> {
const collection = this.db.collection('devices')
await collection.createIndex({ 'compliance.status': 1 }, { name: 'idx_compliance_status' })
await collection.createIndex(
{ 'compliance.configurationIdentifier': 1 },
{ name: 'idx_compliance_configuration_identifier' }
)
}
async down(): Promise<void> {
const collection = this.db.collection('devices')
await collection.dropIndex('idx_compliance_status').catch(() => {})
await collection.dropIndex('idx_compliance_configuration_identifier').catch(() => {})
}
}
@@ -0,0 +1,20 @@
import { MongoMigration } from '../mongo_migrator.js'
export default class DropLegacyCollections extends MongoMigration {
async up(): Promise<void> {
// Drop legacy Discovery collection
await this.db.dropCollection('discovery_configs').catch(() => {})
// Drop legacy Provisioning collection
await this.db.dropCollection('provisioning_configs').catch(() => {})
}
async down(): Promise<void> {
// Recreate legacy collections (empty — data must be restored from migration rollback)
const discoveryCollection = this.db.collection('discovery_configs')
await discoveryCollection.createIndex({ identifier: 1 }, { unique: true, name: 'idx_identifier' })
const provisioningCollection = this.db.collection('provisioning_configs')
await provisioningCollection.createIndex({ identifier: 1 }, { unique: true, name: 'idx_identifier' })
}
}
+119
View File
@@ -0,0 +1,119 @@
import { Db } from 'mongodb'
import { readdir } from 'node:fs/promises'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
export abstract class MongoMigration {
protected db: Db
constructor(db: Db) {
this.db = db
}
abstract up(): Promise<void>
abstract down(): Promise<void>
}
export class MigratorRunner {
private db: Db
private migrationsPath: string
constructor(db: Db, migrationsPath: string) {
this.db = db
this.migrationsPath = migrationsPath
}
private get migrationsCollection() {
return this.db.collection('_migrations')
}
async run(): Promise<string[]> {
const files = await this.getMigrationFiles()
const executed = await this.getExecutedMigrations()
const pending = files.filter((f) => !executed.includes(f))
if (pending.length === 0) {
return []
}
const batch = await this.getNextBatch()
const ran: string[] = []
for (const file of pending) {
const migration = await this.loadMigration(file)
await migration.up()
await this.migrationsCollection.insertOne({
name: file,
batch,
executed_at: new Date(),
})
ran.push(file)
}
return ran
}
async rollback(): Promise<string[]> {
const lastBatch = await this.getLastBatch()
if (lastBatch === 0) return []
const migrations = await this.migrationsCollection
.find({ batch: lastBatch })
.sort({ executed_at: -1 })
.toArray()
const rolledBack: string[] = []
for (const record of migrations) {
const migration = await this.loadMigration(record.name)
await migration.down()
await this.migrationsCollection.deleteOne({ _id: record._id })
rolledBack.push(record.name)
}
return rolledBack
}
async status(): Promise<{ name: string; batch: number | null; ran: boolean }[]> {
const files = await this.getMigrationFiles()
const executed = await this.migrationsCollection.find({}).toArray()
const executedMap = new Map(executed.map((e) => [e.name, e.batch]))
return files.map((file) => ({
name: file,
batch: executedMap.get(file) ?? null,
ran: executedMap.has(file),
}))
}
private async getMigrationFiles(): Promise<string[]> {
try {
const files = await readdir(this.migrationsPath)
return files.filter((f) => f.endsWith('.ts') || f.endsWith('.js')).sort()
} catch {
return []
}
}
private async getExecutedMigrations(): Promise<string[]> {
const docs = await this.migrationsCollection.find({}).toArray()
return docs.map((d) => d.name)
}
private async getNextBatch(): Promise<number> {
const last = await this.getLastBatch()
return last + 1
}
private async getLastBatch(): Promise<number> {
const doc = await this.migrationsCollection.find({}).sort({ batch: -1 }).limit(1).toArray()
return doc.length > 0 ? doc[0].batch : 0
}
private async loadMigration(file: string): Promise<MongoMigration> {
const filePath = join(this.migrationsPath, file)
const fileUrl = pathToFileURL(filePath).href
const module = await import(fileUrl)
const MigrationClass = module.default
return new MigrationClass(this.db)
}
}