feat(vpn,target-profiles,migrations): add startup data migrations, support scoped VPN route allow entries, and rename target profile hosts to ips

This commit is contained in:
2026-04-07 21:02:37 +00:00
parent f29ed9757e
commit 7fa6d82e58
24 changed files with 1503 additions and 1563 deletions

70
ts_migrations/index.ts Normal file
View File

@@ -0,0 +1,70 @@
/// <reference types="node" />
/**
* dcrouter migration runner.
*
* Uses @push.rocks/smartmigration via dynamic import so smartmigration's type
* chain (which pulls in mongodb 7.x and related types) doesn't leak into
* compile-time type checking for this folder.
*/
/** Matches the subset of IMigrationRunResult we actually log. */
export interface IMigrationRunResult {
stepsApplied: Array<unknown>;
wasFreshInstall: boolean;
currentVersionBefore: string | null;
currentVersionAfter: string;
totalDurationMs: number;
}
export interface IMigrationRunner {
run(): Promise<IMigrationRunResult>;
}
/**
* Create a configured SmartMigration runner with all dcrouter migration steps registered.
*
* Call `.run()` on the returned instance at startup (after DcRouterDb is ready,
* before any service that reads migrated collections).
*
* @param db - The initialized SmartdataDb instance from DcRouterDb.getDb()
* @param targetVersion - The current app version (from commitinfo.version)
*/
export async function createMigrationRunner(
db: unknown,
targetVersion: string,
): Promise<IMigrationRunner> {
const sm = await import('@push.rocks/smartmigration');
const migration = new sm.SmartMigration({
targetVersion,
db: db as any,
// Brand-new installs skip all migrations and stamp directly to the current version.
freshInstallVersion: targetVersion,
});
// Register steps in execution order. Each step's .from() must match the
// previous step's .to() to form a contiguous chain.
migration
.step('rename-target-profile-host-to-ip')
.from('13.0.11').to('13.1.0')
.description('Rename ITargetProfileTarget.host → ip on all target profiles')
.up(async (ctx) => {
const collection = ctx.mongo!.collection('targetprofiledoc');
const cursor = collection.find({ 'targets.host': { $exists: true } });
let migrated = 0;
for await (const doc of cursor) {
const targets = ((doc as any).targets || []).map((t: any) => {
if (t && typeof t === 'object' && 'host' in t && !('ip' in t)) {
const { host, ...rest } = t;
return { ...rest, ip: host };
}
return t;
});
await collection.updateOne({ _id: (doc as any)._id }, { $set: { targets } });
migrated++;
}
ctx.log.log('info', `rename-target-profile-host-to-ip: migrated ${migrated} profile(s)`);
});
return migration;
}

View File

@@ -0,0 +1,3 @@
{
"order": 2
}