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:
commit
d1c65dbf1e
164 files changed
+29127
No files matched your search
@@ -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' })
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user