BREAKING CHANGE(db): replace StorageManager and CacheDb with a unified smartdata-backed database layer

This commit is contained in:
2026-03-31 15:31:16 +00:00
parent 193a4bb180
commit bb6c26484d
49 changed files with 1475 additions and 1687 deletions

View File

@@ -0,0 +1,166 @@
import * as plugins from '../plugins.js';
import { logger } from '../logger.js';
import { DcRouterDb } from './classes.dcrouter-db.js';
// Import document classes for cleanup
import { CachedEmail } from './documents/classes.cached.email.js';
import { CachedIPReputation } from './documents/classes.cached.ip.reputation.js';
/**
* Configuration for the cache cleaner
*/
export interface ICacheCleanerOptions {
/** Cleanup interval in milliseconds (default: 1 hour) */
intervalMs?: number;
/** Enable verbose logging */
verbose?: boolean;
}
/**
* CacheCleaner - Periodically removes expired documents from the cache
*
* Runs on a configurable interval (default: hourly) and queries each
* collection for documents where expiresAt < now(), then deletes them.
*/
export class CacheCleaner {
private cleanupInterval: ReturnType<typeof setInterval> | null = null;
private isRunning: boolean = false;
private options: Required<ICacheCleanerOptions>;
private dcRouterDb: DcRouterDb;
constructor(dcRouterDb: DcRouterDb, options: ICacheCleanerOptions = {}) {
this.dcRouterDb = dcRouterDb;
this.options = {
intervalMs: options.intervalMs || 60 * 60 * 1000, // 1 hour default
verbose: options.verbose || false,
};
}
/**
* Start the periodic cleanup process
*/
public start(): void {
if (this.isRunning) {
logger.log('warn', 'CacheCleaner already running');
return;
}
this.isRunning = true;
// Run cleanup immediately on start
this.runCleanup().catch((error: unknown) => {
logger.log('error', `Initial cache cleanup failed: ${(error as Error).message}`);
});
// Schedule periodic cleanup
this.cleanupInterval = setInterval(() => {
this.runCleanup().catch((error: unknown) => {
logger.log('error', `Cache cleanup failed: ${(error as Error).message}`);
});
}, this.options.intervalMs);
logger.log(
'info',
`CacheCleaner started with interval: ${this.options.intervalMs / 1000 / 60} minutes`
);
}
/**
* Stop the periodic cleanup process
*/
public stop(): void {
if (!this.isRunning) {
return;
}
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = null;
}
this.isRunning = false;
logger.log('info', 'CacheCleaner stopped');
}
/**
* Run a single cleanup cycle
*/
public async runCleanup(): Promise<void> {
if (!this.dcRouterDb.isReady()) {
logger.log('warn', 'DcRouterDb not ready, skipping cleanup');
return;
}
const now = new Date();
const results: { collection: string; deleted: number }[] = [];
try {
const emailsDeleted = await this.cleanExpiredDocuments(CachedEmail, now);
results.push({ collection: 'CachedEmail', deleted: emailsDeleted });
const ipReputationDeleted = await this.cleanExpiredDocuments(CachedIPReputation, now);
results.push({ collection: 'CachedIPReputation', deleted: ipReputationDeleted });
// Log results
const totalDeleted = results.reduce((sum, r) => sum + r.deleted, 0);
if (totalDeleted > 0 || this.options.verbose) {
const summary = results
.filter((r) => r.deleted > 0)
.map((r) => `${r.collection}: ${r.deleted}`)
.join(', ');
logger.log(
'info',
`Cache cleanup completed. Deleted ${totalDeleted} expired documents. ${summary || 'No deletions.'}`
);
}
} catch (error: unknown) {
logger.log('error', `Cache cleanup error: ${(error as Error).message}`);
throw error;
}
}
/**
* Clean expired documents from a specific collection using smartdata API
*/
private async cleanExpiredDocuments<T extends { delete: () => Promise<void> }>(
documentClass: { getInstances: (filter: any) => Promise<T[]> },
now: Date
): Promise<number> {
try {
// Find all expired documents
const expiredDocs = await documentClass.getInstances({
expiresAt: { $lt: now },
});
// Delete each expired document
let deletedCount = 0;
for (const doc of expiredDocs) {
try {
await doc.delete();
deletedCount++;
} catch (deleteError: unknown) {
logger.log('warn', `Failed to delete expired document: ${(deleteError as Error).message}`);
}
}
return deletedCount;
} catch (error: unknown) {
logger.log('error', `Error cleaning collection: ${(error as Error).message}`);
return 0;
}
}
/**
* Check if the cleaner is running
*/
public isActive(): boolean {
return this.isRunning;
}
/**
* Get the cleanup interval in milliseconds
*/
public getIntervalMs(): number {
return this.options.intervalMs;
}
}

View File

@@ -0,0 +1,111 @@
import * as plugins from '../plugins.js';
/**
* Base class for all cached documents with TTL support
*
* Extends smartdata's SmartDataDbDoc to add:
* - Automatic timestamps (createdAt, lastAccessedAt)
* - TTL/expiration support (expiresAt)
* - Helper methods for TTL management
*
* NOTE: Subclasses MUST add @svDb() decorators to createdAt, expiresAt, and lastAccessedAt
* since decorators on abstract classes don't propagate correctly.
*/
export abstract class CachedDocument<T extends CachedDocument<T>> extends plugins.smartdata.SmartDataDbDoc<T, T> {
/**
* Timestamp when the document was created
* NOTE: Subclasses must add @svDb() decorator
*/
public createdAt: Date = new Date();
/**
* Timestamp when the document expires and should be cleaned up
* NOTE: Subclasses must add @svDb() decorator
*/
public expiresAt!: Date;
/**
* Timestamp of last access (for LRU-style eviction if needed)
* NOTE: Subclasses must add @svDb() decorator
*/
public lastAccessedAt: Date = new Date();
/**
* Set the TTL (time to live) for this document
* @param ttlMs Time to live in milliseconds
*/
public setTTL(ttlMs: number): void {
this.expiresAt = new Date(Date.now() + ttlMs);
}
/**
* Set TTL using days
* @param days Number of days until expiration
*/
public setTTLDays(days: number): void {
this.setTTL(days * 24 * 60 * 60 * 1000);
}
/**
* Set TTL using hours
* @param hours Number of hours until expiration
*/
public setTTLHours(hours: number): void {
this.setTTL(hours * 60 * 60 * 1000);
}
/**
* Check if this document has expired
*/
public isExpired(): boolean {
if (!this.expiresAt) {
return false; // No expiration set
}
return new Date() > this.expiresAt;
}
/**
* Update the lastAccessedAt timestamp
*/
public touch(): void {
this.lastAccessedAt = new Date();
}
/**
* Get remaining TTL in milliseconds
* Returns 0 if expired, -1 if no expiration set
*/
public getRemainingTTL(): number {
if (!this.expiresAt) {
return -1;
}
const remaining = this.expiresAt.getTime() - Date.now();
return remaining > 0 ? remaining : 0;
}
/**
* Extend the TTL by the specified milliseconds from now
* @param ttlMs Additional time to live in milliseconds
*/
public extendTTL(ttlMs: number): void {
this.expiresAt = new Date(Date.now() + ttlMs);
}
/**
* Set the document to never expire (100 years in the future)
*/
public setNeverExpires(): void {
this.expiresAt = new Date(Date.now() + 100 * 365 * 24 * 60 * 60 * 1000);
}
}
/**
* TTL constants in milliseconds
*/
export const TTL = {
HOURS_1: 1 * 60 * 60 * 1000,
HOURS_24: 24 * 60 * 60 * 1000,
DAYS_7: 7 * 24 * 60 * 60 * 1000,
DAYS_30: 30 * 24 * 60 * 60 * 1000,
DAYS_90: 90 * 24 * 60 * 60 * 1000,
} as const;

View File

@@ -0,0 +1,179 @@
import * as plugins from '../plugins.js';
import { logger } from '../logger.js';
import { defaultTsmDbPath } from '../paths.js';
/**
* Configuration options for the unified DCRouter database
*/
export interface IDcRouterDbConfig {
/** External MongoDB connection URL. If absent, uses embedded LocalSmartDb. */
mongoDbUrl?: string;
/** Storage path for embedded LocalSmartDb data (default: ~/.serve.zone/dcrouter/tsmdb) */
storagePath?: string;
/** Database name (default: dcrouter) */
dbName?: string;
/** Enable debug logging */
debug?: boolean;
}
/**
* DcRouterDb - Unified database layer for DCRouter
*
* Replaces both StorageManager (flat-file key-value) and CacheDb (embedded MongoDB).
* All data is stored as smartdata document classes in a single database.
*
* Two modes:
* - **Embedded** (default): Spawns a LocalSmartDb (Rust-based MongoDB-compatible engine)
* - **External**: Connects to a provided MongoDB URL
*/
export class DcRouterDb {
private static instance: DcRouterDb | null = null;
private localSmartDb: plugins.smartdb.LocalSmartDb | null = null;
private smartdataDb!: plugins.smartdata.SmartdataDb;
private options: Required<IDcRouterDbConfig>;
private isStarted: boolean = false;
constructor(options: IDcRouterDbConfig = {}) {
this.options = {
mongoDbUrl: options.mongoDbUrl || '',
storagePath: options.storagePath || defaultTsmDbPath,
dbName: options.dbName || 'dcrouter',
debug: options.debug || false,
};
}
/**
* Get or create the singleton instance
*/
public static getInstance(options?: IDcRouterDbConfig): DcRouterDb {
if (!DcRouterDb.instance) {
DcRouterDb.instance = new DcRouterDb(options);
}
return DcRouterDb.instance;
}
/**
* Reset the singleton instance (useful for testing)
*/
public static resetInstance(): void {
DcRouterDb.instance = null;
}
/**
* Start the database
* - If mongoDbUrl is provided, connects directly to external MongoDB
* - Otherwise, starts an embedded LocalSmartDb instance
*/
public async start(): Promise<void> {
if (this.isStarted) {
logger.log('warn', 'DcRouterDb already started');
return;
}
try {
let connectionUri: string;
if (this.options.mongoDbUrl) {
// External MongoDB mode
connectionUri = this.options.mongoDbUrl;
logger.log('info', `DcRouterDb connecting to external MongoDB`);
} else {
// Embedded LocalSmartDb mode
await plugins.fsUtils.ensureDir(this.options.storagePath);
this.localSmartDb = new plugins.smartdb.LocalSmartDb({
folderPath: this.options.storagePath,
});
const connectionInfo = await this.localSmartDb.start();
connectionUri = connectionInfo.connectionUri;
if (this.options.debug) {
logger.log('debug', `LocalSmartDb started with URI: ${connectionUri}`);
}
logger.log('info', `DcRouterDb started embedded instance at ${this.options.storagePath}`);
}
// Initialize smartdata ORM
this.smartdataDb = new plugins.smartdata.SmartdataDb({
mongoDbUrl: connectionUri,
mongoDbName: this.options.dbName,
});
await this.smartdataDb.init();
this.isStarted = true;
logger.log('info', `DcRouterDb ready (db: ${this.options.dbName})`);
} catch (error: unknown) {
logger.log('error', `Failed to start DcRouterDb: ${(error as Error).message}`);
throw error;
}
}
/**
* Stop the database
*/
public async stop(): Promise<void> {
if (!this.isStarted) {
return;
}
try {
// Close smartdata connection
if (this.smartdataDb) {
await this.smartdataDb.close();
}
// Stop embedded LocalSmartDb if running
if (this.localSmartDb) {
await this.localSmartDb.stop();
this.localSmartDb = null;
}
this.isStarted = false;
logger.log('info', 'DcRouterDb stopped');
} catch (error: unknown) {
logger.log('error', `Error stopping DcRouterDb: ${(error as Error).message}`);
throw error;
}
}
/**
* Get the smartdata database instance for @Collection decorators
*/
public getDb(): plugins.smartdata.SmartdataDb {
if (!this.isStarted) {
throw new Error('DcRouterDb not started. Call start() first.');
}
return this.smartdataDb;
}
/**
* Check if the database is ready
*/
public isReady(): boolean {
return this.isStarted;
}
/**
* Whether running in embedded mode (LocalSmartDb) vs external MongoDB
*/
public isEmbedded(): boolean {
return !this.options.mongoDbUrl;
}
/**
* Get the storage path (only relevant for embedded mode)
*/
public getStoragePath(): string {
return this.options.storagePath;
}
/**
* Get the database name
*/
public getDbName(): string {
return this.options.dbName;
}
}

View File

@@ -0,0 +1,106 @@
import * as plugins from '../../plugins.js';
import { DcRouterDb } from '../classes.dcrouter-db.js';
const getDb = () => DcRouterDb.getInstance().getDb();
@plugins.smartdata.Collection(() => getDb())
export class AccountingSessionDoc extends plugins.smartdata.SmartDataDbDoc<AccountingSessionDoc, AccountingSessionDoc> {
@plugins.smartdata.unI()
@plugins.smartdata.svDb()
public sessionId!: string;
@plugins.smartdata.svDb()
public username!: string;
@plugins.smartdata.svDb()
public macAddress!: string;
@plugins.smartdata.svDb()
public nasIpAddress!: string;
@plugins.smartdata.svDb()
public nasPort!: number;
@plugins.smartdata.svDb()
public nasPortType!: string;
@plugins.smartdata.svDb()
public nasIdentifier!: string;
@plugins.smartdata.svDb()
public vlanId!: number;
@plugins.smartdata.svDb()
public framedIpAddress!: string;
@plugins.smartdata.svDb()
public calledStationId!: string;
@plugins.smartdata.svDb()
public callingStationId!: string;
@plugins.smartdata.svDb()
public startTime!: number;
@plugins.smartdata.svDb()
public endTime!: number;
@plugins.smartdata.svDb()
public lastUpdateTime!: number;
@plugins.smartdata.index()
@plugins.smartdata.svDb()
public status!: 'active' | 'stopped' | 'terminated';
@plugins.smartdata.svDb()
public terminateCause!: string;
@plugins.smartdata.svDb()
public inputOctets!: number;
@plugins.smartdata.svDb()
public outputOctets!: number;
@plugins.smartdata.svDb()
public inputPackets!: number;
@plugins.smartdata.svDb()
public outputPackets!: number;
@plugins.smartdata.svDb()
public sessionTime!: number;
@plugins.smartdata.svDb()
public serviceType!: string;
constructor() {
super();
}
public static async findBySessionId(sessionId: string): Promise<AccountingSessionDoc | null> {
return await AccountingSessionDoc.getInstance({ sessionId });
}
public static async findActive(): Promise<AccountingSessionDoc[]> {
return await AccountingSessionDoc.getInstances({ status: 'active' });
}
public static async findByUsername(username: string): Promise<AccountingSessionDoc[]> {
return await AccountingSessionDoc.getInstances({ username });
}
public static async findByNas(nasIpAddress: string): Promise<AccountingSessionDoc[]> {
return await AccountingSessionDoc.getInstances({ nasIpAddress });
}
public static async findByVlan(vlanId: number): Promise<AccountingSessionDoc[]> {
return await AccountingSessionDoc.getInstances({ vlanId });
}
public static async findStoppedBefore(cutoffTime: number): Promise<AccountingSessionDoc[]> {
return await AccountingSessionDoc.getInstances({
status: { $in: ['stopped', 'terminated'] } as any,
endTime: { $lt: cutoffTime, $gt: 0 } as any,
});
}
}

View File

@@ -0,0 +1,41 @@
import * as plugins from '../../plugins.js';
import { DcRouterDb } from '../classes.dcrouter-db.js';
const getDb = () => DcRouterDb.getInstance().getDb();
@plugins.smartdata.Collection(() => getDb())
export class AcmeCertDoc extends plugins.smartdata.SmartDataDbDoc<AcmeCertDoc, AcmeCertDoc> {
@plugins.smartdata.unI()
@plugins.smartdata.svDb()
public domainName!: string;
@plugins.smartdata.svDb()
public id!: string;
@plugins.smartdata.svDb()
public created!: number;
@plugins.smartdata.svDb()
public privateKey!: string;
@plugins.smartdata.svDb()
public publicKey!: string;
@plugins.smartdata.svDb()
public csr!: string;
@plugins.smartdata.svDb()
public validUntil!: number;
constructor() {
super();
}
public static async findByDomain(domainName: string): Promise<AcmeCertDoc | null> {
return await AcmeCertDoc.getInstance({ domainName });
}
public static async findAll(): Promise<AcmeCertDoc[]> {
return await AcmeCertDoc.getInstances({});
}
}

View File

@@ -0,0 +1,56 @@
import * as plugins from '../../plugins.js';
import { DcRouterDb } from '../classes.dcrouter-db.js';
import type { TApiTokenScope } from '../../../ts_interfaces/data/route-management.js';
const getDb = () => DcRouterDb.getInstance().getDb();
@plugins.smartdata.Collection(() => getDb())
export class ApiTokenDoc extends plugins.smartdata.SmartDataDbDoc<ApiTokenDoc, ApiTokenDoc> {
@plugins.smartdata.unI()
@plugins.smartdata.svDb()
public id!: string;
@plugins.smartdata.svDb()
public name: string = '';
@plugins.smartdata.svDb()
public tokenHash!: string;
@plugins.smartdata.svDb()
public scopes!: TApiTokenScope[];
@plugins.smartdata.svDb()
public createdAt!: number;
@plugins.smartdata.svDb()
public expiresAt!: number | null;
@plugins.smartdata.svDb()
public lastUsedAt!: number | null;
@plugins.smartdata.svDb()
public createdBy!: string;
@plugins.smartdata.svDb()
public enabled!: boolean;
constructor() {
super();
}
public static async findById(id: string): Promise<ApiTokenDoc | null> {
return await ApiTokenDoc.getInstance({ id });
}
public static async findByTokenHash(tokenHash: string): Promise<ApiTokenDoc | null> {
return await ApiTokenDoc.getInstance({ tokenHash });
}
public static async findAll(): Promise<ApiTokenDoc[]> {
return await ApiTokenDoc.getInstances({});
}
public static async findEnabled(): Promise<ApiTokenDoc[]> {
return await ApiTokenDoc.getInstances({ enabled: true });
}
}

View File

@@ -0,0 +1,240 @@
import * as plugins from '../../plugins.js';
import { CachedDocument, TTL } from '../classes.cached.document.js';
import { DcRouterDb } from '../classes.dcrouter-db.js';
/**
* Email status in the cache
*/
export type TCachedEmailStatus = 'pending' | 'processing' | 'delivered' | 'failed' | 'deferred';
/**
* Helper to get the smartdata database instance
*/
const getDb = () => DcRouterDb.getInstance().getDb();
/**
* CachedEmail - Stores email queue items in the cache
*
* Used for persistent email queue storage, tracking delivery status,
* and maintaining email history for the configured TTL period.
*/
@plugins.smartdata.Collection(() => getDb())
export class CachedEmail extends CachedDocument<CachedEmail> {
// TTL fields from base class (decorators required on concrete class)
@plugins.smartdata.svDb()
public createdAt: Date = new Date();
@plugins.smartdata.svDb()
public expiresAt: Date = new Date(Date.now() + TTL.DAYS_30);
@plugins.smartdata.svDb()
public lastAccessedAt: Date = new Date();
/**
* Unique identifier for this email
*/
@plugins.smartdata.unI()
@plugins.smartdata.svDb()
public id!: string;
/**
* Email message ID (RFC 822 Message-ID header)
*/
@plugins.smartdata.svDb()
public messageId!: string;
/**
* Sender email address (envelope from)
*/
@plugins.smartdata.svDb()
public from!: string;
/**
* Recipient email addresses
*/
@plugins.smartdata.svDb()
public to!: string[];
/**
* CC recipients
*/
@plugins.smartdata.svDb()
public cc!: string[];
/**
* BCC recipients
*/
@plugins.smartdata.svDb()
public bcc!: string[];
/**
* Email subject
*/
@plugins.smartdata.svDb()
public subject!: string;
/**
* Raw RFC822 email content
*/
@plugins.smartdata.svDb()
public rawContent!: string;
/**
* Current status of the email
*/
@plugins.smartdata.svDb()
public status!: TCachedEmailStatus;
/**
* Number of delivery attempts
*/
@plugins.smartdata.svDb()
public attempts: number = 0;
/**
* Maximum number of delivery attempts
*/
@plugins.smartdata.svDb()
public maxAttempts: number = 3;
/**
* Timestamp for next delivery attempt
*/
@plugins.smartdata.svDb()
public nextAttempt!: Date;
/**
* Last error message if delivery failed
*/
@plugins.smartdata.svDb()
public lastError!: string;
/**
* Timestamp when the email was successfully delivered
*/
@plugins.smartdata.svDb()
public deliveredAt!: Date;
/**
* Sender domain (for querying/filtering)
*/
@plugins.smartdata.svDb()
public senderDomain!: string;
/**
* Priority level (higher = more important)
*/
@plugins.smartdata.svDb()
public priority: number = 0;
/**
* JSON-serialized route data
*/
@plugins.smartdata.svDb()
public routeData!: string;
/**
* DKIM signature status
*/
@plugins.smartdata.svDb()
public dkimSigned: boolean = false;
constructor() {
super();
this.setTTL(TTL.DAYS_30); // Default 30-day TTL
this.status = 'pending';
this.to = [];
this.cc = [];
this.bcc = [];
}
/**
* Create a new CachedEmail with a unique ID
*/
public static createNew(): CachedEmail {
const email = new CachedEmail();
email.id = plugins.uuid.v4();
return email;
}
/**
* Find an email by ID
*/
public static async findById(id: string): Promise<CachedEmail | null> {
return await CachedEmail.getInstance({
id,
});
}
/**
* Find all emails with a specific status
*/
public static async findByStatus(status: TCachedEmailStatus): Promise<CachedEmail[]> {
return await CachedEmail.getInstances({
status,
});
}
/**
* Find all emails pending delivery (status = pending and nextAttempt <= now)
*/
public static async findPendingForDelivery(): Promise<CachedEmail[]> {
const now = new Date();
return await CachedEmail.getInstances({
status: 'pending',
nextAttempt: { $lte: now },
});
}
/**
* Find emails by sender domain
*/
public static async findBySenderDomain(domain: string): Promise<CachedEmail[]> {
return await CachedEmail.getInstances({
senderDomain: domain,
});
}
/**
* Mark as delivered
*/
public markDelivered(): void {
this.status = 'delivered';
this.deliveredAt = new Date();
}
/**
* Mark as failed with error
*/
public markFailed(error: string): void {
this.status = 'failed';
this.lastError = error;
}
/**
* Increment attempt counter and schedule next attempt
*/
public scheduleRetry(delayMs: number = 5 * 60 * 1000): void {
this.attempts++;
this.status = 'deferred';
this.nextAttempt = new Date(Date.now() + delayMs);
// If max attempts reached, mark as failed
if (this.attempts >= this.maxAttempts) {
this.status = 'failed';
this.lastError = `Max attempts (${this.maxAttempts}) reached`;
}
}
/**
* Extract sender domain from email address
*/
public updateSenderDomain(): void {
if (this.from) {
const match = this.from.match(/@([^>]+)>?$/);
if (match) {
this.senderDomain = match[1].toLowerCase();
}
}
}
}

View File

@@ -0,0 +1,247 @@
import * as plugins from '../../plugins.js';
import { CachedDocument, TTL } from '../classes.cached.document.js';
import { DcRouterDb } from '../classes.dcrouter-db.js';
/**
* Helper to get the smartdata database instance
*/
const getDb = () => DcRouterDb.getInstance().getDb();
/**
* IP reputation result data
*/
export interface IIPReputationData {
score: number;
isSpam: boolean;
isProxy: boolean;
isTor: boolean;
isVPN: boolean;
country?: string;
asn?: string;
org?: string;
blacklists?: string[];
}
/**
* CachedIPReputation - Stores IP reputation lookup results
*
* Caches the results of IP reputation checks to avoid repeated
* external API calls. Default TTL is 24 hours.
*/
@plugins.smartdata.Collection(() => getDb())
export class CachedIPReputation extends CachedDocument<CachedIPReputation> {
// TTL fields from base class (decorators required on concrete class)
@plugins.smartdata.svDb()
public createdAt: Date = new Date();
@plugins.smartdata.svDb()
public expiresAt: Date = new Date(Date.now() + TTL.HOURS_24);
@plugins.smartdata.svDb()
public lastAccessedAt: Date = new Date();
/**
* IP address (unique identifier)
*/
@plugins.smartdata.unI()
@plugins.smartdata.svDb()
public ipAddress!: string;
/**
* Reputation score (0-100, higher = better)
*/
@plugins.smartdata.svDb()
public score!: number;
/**
* Whether the IP is flagged as spam source
*/
@plugins.smartdata.svDb()
public isSpam!: boolean;
/**
* Whether the IP is a known proxy
*/
@plugins.smartdata.svDb()
public isProxy!: boolean;
/**
* Whether the IP is a Tor exit node
*/
@plugins.smartdata.svDb()
public isTor!: boolean;
/**
* Whether the IP is a VPN endpoint
*/
@plugins.smartdata.svDb()
public isVPN!: boolean;
/**
* Country code (ISO 3166-1 alpha-2)
*/
@plugins.smartdata.svDb()
public country!: string;
/**
* Autonomous System Number
*/
@plugins.smartdata.svDb()
public asn!: string;
/**
* Organization name
*/
@plugins.smartdata.svDb()
public org!: string;
/**
* List of blacklists the IP appears on
*/
@plugins.smartdata.svDb()
public blacklists!: string[];
/**
* Number of times this IP has been checked
*/
@plugins.smartdata.svDb()
public checkCount: number = 0;
/**
* Number of connections from this IP
*/
@plugins.smartdata.svDb()
public connectionCount: number = 0;
/**
* Number of emails received from this IP
*/
@plugins.smartdata.svDb()
public emailCount: number = 0;
/**
* Number of spam emails from this IP
*/
@plugins.smartdata.svDb()
public spamCount: number = 0;
constructor() {
super();
this.setTTL(TTL.HOURS_24); // Default 24-hour TTL
this.blacklists = [];
this.score = 50; // Default neutral score
this.isSpam = false;
this.isProxy = false;
this.isTor = false;
this.isVPN = false;
}
/**
* Create from reputation data
*/
public static fromReputationData(ipAddress: string, data: IIPReputationData): CachedIPReputation {
const cached = new CachedIPReputation();
cached.ipAddress = ipAddress;
cached.score = data.score;
cached.isSpam = data.isSpam;
cached.isProxy = data.isProxy;
cached.isTor = data.isTor;
cached.isVPN = data.isVPN;
cached.country = data.country || '';
cached.asn = data.asn || '';
cached.org = data.org || '';
cached.blacklists = data.blacklists || [];
cached.checkCount = 1;
return cached;
}
/**
* Convert to reputation data object
*/
public toReputationData(): IIPReputationData {
this.touch();
return {
score: this.score,
isSpam: this.isSpam,
isProxy: this.isProxy,
isTor: this.isTor,
isVPN: this.isVPN,
country: this.country,
asn: this.asn,
org: this.org,
blacklists: this.blacklists,
};
}
/**
* Find by IP address
*/
public static async findByIP(ipAddress: string): Promise<CachedIPReputation | null> {
return await CachedIPReputation.getInstance({
ipAddress,
});
}
/**
* Find all IPs flagged as spam
*/
public static async findSpamIPs(): Promise<CachedIPReputation[]> {
return await CachedIPReputation.getInstances({
isSpam: true,
});
}
/**
* Find IPs with score below threshold
*/
public static async findLowScoreIPs(threshold: number): Promise<CachedIPReputation[]> {
return await CachedIPReputation.getInstances({
score: { $lt: threshold },
});
}
/**
* Record a connection from this IP
*/
public recordConnection(): void {
this.connectionCount++;
this.touch();
}
/**
* Record an email from this IP
*/
public recordEmail(isSpam: boolean = false): void {
this.emailCount++;
if (isSpam) {
this.spamCount++;
}
this.touch();
}
/**
* Update the reputation data
*/
public updateReputation(data: IIPReputationData): void {
this.score = data.score;
this.isSpam = data.isSpam;
this.isProxy = data.isProxy;
this.isTor = data.isTor;
this.isVPN = data.isVPN;
this.country = data.country || this.country;
this.asn = data.asn || this.asn;
this.org = data.org || this.org;
this.blacklists = data.blacklists || this.blacklists;
this.checkCount++;
this.touch();
// Refresh TTL on update
this.setTTL(TTL.HOURS_24);
}
/**
* Check if this IP should be blocked
*/
public shouldBlock(): boolean {
return this.isSpam || this.score < 20 || this.blacklists.length > 2;
}
}

View File

@@ -0,0 +1,35 @@
import * as plugins from '../../plugins.js';
import { DcRouterDb } from '../classes.dcrouter-db.js';
const getDb = () => DcRouterDb.getInstance().getDb();
@plugins.smartdata.Collection(() => getDb())
export class CertBackoffDoc extends plugins.smartdata.SmartDataDbDoc<CertBackoffDoc, CertBackoffDoc> {
@plugins.smartdata.unI()
@plugins.smartdata.svDb()
public domain!: string;
@plugins.smartdata.svDb()
public failures!: number;
@plugins.smartdata.svDb()
public lastFailure!: string;
@plugins.smartdata.svDb()
public retryAfter!: string;
@plugins.smartdata.svDb()
public lastError!: string;
constructor() {
super();
}
public static async findByDomain(domain: string): Promise<CertBackoffDoc | null> {
return await CertBackoffDoc.getInstance({ domain });
}
public static async findAll(): Promise<CertBackoffDoc[]> {
return await CertBackoffDoc.getInstances({});
}
}

View File

@@ -0,0 +1,38 @@
import * as plugins from '../../plugins.js';
import { DcRouterDb } from '../classes.dcrouter-db.js';
const getDb = () => DcRouterDb.getInstance().getDb();
@plugins.smartdata.Collection(() => getDb())
export class ProxyCertDoc extends plugins.smartdata.SmartDataDbDoc<ProxyCertDoc, ProxyCertDoc> {
@plugins.smartdata.unI()
@plugins.smartdata.svDb()
public domain!: string;
@plugins.smartdata.svDb()
public publicKey!: string;
@plugins.smartdata.svDb()
public privateKey!: string;
@plugins.smartdata.svDb()
public ca!: string;
@plugins.smartdata.svDb()
public validUntil!: number;
@plugins.smartdata.svDb()
public validFrom!: number;
constructor() {
super();
}
public static async findByDomain(domain: string): Promise<ProxyCertDoc | null> {
return await ProxyCertDoc.getInstance({ domain });
}
public static async findAll(): Promise<ProxyCertDoc[]> {
return await ProxyCertDoc.getInstances({});
}
}

View File

@@ -0,0 +1,54 @@
import * as plugins from '../../plugins.js';
import { DcRouterDb } from '../classes.dcrouter-db.js';
const getDb = () => DcRouterDb.getInstance().getDb();
@plugins.smartdata.Collection(() => getDb())
export class RemoteIngressEdgeDoc extends plugins.smartdata.SmartDataDbDoc<RemoteIngressEdgeDoc, RemoteIngressEdgeDoc> {
@plugins.smartdata.unI()
@plugins.smartdata.svDb()
public id!: string;
@plugins.smartdata.svDb()
public name: string = '';
@plugins.smartdata.svDb()
public secret!: string;
@plugins.smartdata.svDb()
public listenPorts!: number[];
@plugins.smartdata.svDb()
public listenPortsUdp!: number[];
@plugins.smartdata.svDb()
public enabled!: boolean;
@plugins.smartdata.svDb()
public autoDerivePorts!: boolean;
@plugins.smartdata.svDb()
public tags!: string[];
@plugins.smartdata.svDb()
public createdAt!: number;
@plugins.smartdata.svDb()
public updatedAt!: number;
constructor() {
super();
}
public static async findById(id: string): Promise<RemoteIngressEdgeDoc | null> {
return await RemoteIngressEdgeDoc.getInstance({ id });
}
public static async findAll(): Promise<RemoteIngressEdgeDoc[]> {
return await RemoteIngressEdgeDoc.getInstances({});
}
public static async findEnabled(): Promise<RemoteIngressEdgeDoc[]> {
return await RemoteIngressEdgeDoc.getInstances({ enabled: true });
}
}

View File

@@ -0,0 +1,32 @@
import * as plugins from '../../plugins.js';
import { DcRouterDb } from '../classes.dcrouter-db.js';
const getDb = () => DcRouterDb.getInstance().getDb();
@plugins.smartdata.Collection(() => getDb())
export class RouteOverrideDoc extends plugins.smartdata.SmartDataDbDoc<RouteOverrideDoc, RouteOverrideDoc> {
@plugins.smartdata.unI()
@plugins.smartdata.svDb()
public routeName!: string;
@plugins.smartdata.svDb()
public enabled!: boolean;
@plugins.smartdata.svDb()
public updatedAt!: number;
@plugins.smartdata.svDb()
public updatedBy!: string;
constructor() {
super();
}
public static async findByRouteName(routeName: string): Promise<RouteOverrideDoc | null> {
return await RouteOverrideDoc.getInstance({ routeName });
}
public static async findAll(): Promise<RouteOverrideDoc[]> {
return await RouteOverrideDoc.getInstances({});
}
}

View File

@@ -0,0 +1,38 @@
import * as plugins from '../../plugins.js';
import { DcRouterDb } from '../classes.dcrouter-db.js';
const getDb = () => DcRouterDb.getInstance().getDb();
@plugins.smartdata.Collection(() => getDb())
export class StoredRouteDoc extends plugins.smartdata.SmartDataDbDoc<StoredRouteDoc, StoredRouteDoc> {
@plugins.smartdata.unI()
@plugins.smartdata.svDb()
public id!: string;
@plugins.smartdata.svDb()
public route!: plugins.smartproxy.IRouteConfig;
@plugins.smartdata.svDb()
public enabled!: boolean;
@plugins.smartdata.svDb()
public createdAt!: number;
@plugins.smartdata.svDb()
public updatedAt!: number;
@plugins.smartdata.svDb()
public createdBy!: string;
constructor() {
super();
}
public static async findById(id: string): Promise<StoredRouteDoc | null> {
return await StoredRouteDoc.getInstance({ id });
}
public static async findAll(): Promise<StoredRouteDoc[]> {
return await StoredRouteDoc.getInstances({});
}
}

View File

@@ -0,0 +1,32 @@
import * as plugins from '../../plugins.js';
import { DcRouterDb } from '../classes.dcrouter-db.js';
const getDb = () => DcRouterDb.getInstance().getDb();
export interface IMacVlanMapping {
mac: string;
vlan: number;
description?: string;
enabled: boolean;
createdAt: number;
updatedAt: number;
}
@plugins.smartdata.Collection(() => getDb())
export class VlanMappingsDoc extends plugins.smartdata.SmartDataDbDoc<VlanMappingsDoc, VlanMappingsDoc> {
@plugins.smartdata.unI()
@plugins.smartdata.svDb()
public configId: string = 'vlan-mappings';
@plugins.smartdata.svDb()
public mappings!: IMacVlanMapping[];
constructor() {
super();
this.mappings = [];
}
public static async load(): Promise<VlanMappingsDoc | null> {
return await VlanMappingsDoc.getInstance({ configId: 'vlan-mappings' });
}
}

View File

@@ -0,0 +1,57 @@
import * as plugins from '../../plugins.js';
import { DcRouterDb } from '../classes.dcrouter-db.js';
const getDb = () => DcRouterDb.getInstance().getDb();
@plugins.smartdata.Collection(() => getDb())
export class VpnClientDoc extends plugins.smartdata.SmartDataDbDoc<VpnClientDoc, VpnClientDoc> {
@plugins.smartdata.unI()
@plugins.smartdata.svDb()
public clientId!: string;
@plugins.smartdata.svDb()
public enabled!: boolean;
@plugins.smartdata.svDb()
public serverDefinedClientTags?: string[];
@plugins.smartdata.svDb()
public description?: string;
@plugins.smartdata.svDb()
public assignedIp?: string;
@plugins.smartdata.svDb()
public noisePublicKey!: string;
@plugins.smartdata.svDb()
public wgPublicKey!: string;
@plugins.smartdata.svDb()
public wgPrivateKey?: string;
@plugins.smartdata.svDb()
public createdAt!: number;
@plugins.smartdata.svDb()
public updatedAt!: number;
@plugins.smartdata.svDb()
public expiresAt?: string;
constructor() {
super();
}
public static async findByClientId(clientId: string): Promise<VpnClientDoc | null> {
return await VpnClientDoc.getInstance({ clientId });
}
public static async findAll(): Promise<VpnClientDoc[]> {
return await VpnClientDoc.getInstances({});
}
public static async findEnabled(): Promise<VpnClientDoc[]> {
return await VpnClientDoc.getInstances({ enabled: true });
}
}

View File

@@ -0,0 +1,31 @@
import * as plugins from '../../plugins.js';
import { DcRouterDb } from '../classes.dcrouter-db.js';
const getDb = () => DcRouterDb.getInstance().getDb();
@plugins.smartdata.Collection(() => getDb())
export class VpnServerKeysDoc extends plugins.smartdata.SmartDataDbDoc<VpnServerKeysDoc, VpnServerKeysDoc> {
@plugins.smartdata.unI()
@plugins.smartdata.svDb()
public configId: string = 'vpn-server-keys';
@plugins.smartdata.svDb()
public noisePrivateKey!: string;
@plugins.smartdata.svDb()
public noisePublicKey!: string;
@plugins.smartdata.svDb()
public wgPrivateKey!: string;
@plugins.smartdata.svDb()
public wgPublicKey!: string;
constructor() {
super();
}
public static async load(): Promise<VpnServerKeysDoc | null> {
return await VpnServerKeysDoc.getInstance({ configId: 'vpn-server-keys' });
}
}

24
ts/db/documents/index.ts Normal file
View File

@@ -0,0 +1,24 @@
// Cached/TTL document classes
export * from './classes.cached.email.js';
export * from './classes.cached.ip.reputation.js';
// Config document classes
export * from './classes.stored-route.doc.js';
export * from './classes.route-override.doc.js';
export * from './classes.api-token.doc.js';
// VPN document classes
export * from './classes.vpn-server-keys.doc.js';
export * from './classes.vpn-client.doc.js';
// Certificate document classes
export * from './classes.acme-cert.doc.js';
export * from './classes.proxy-cert.doc.js';
export * from './classes.cert-backoff.doc.js';
// Remote ingress document classes
export * from './classes.remote-ingress-edge.doc.js';
// RADIUS document classes
export * from './classes.vlan-mappings.doc.js';
export * from './classes.accounting-session.doc.js';

11
ts/db/index.ts Normal file
View File

@@ -0,0 +1,11 @@
// Unified database manager
export * from './classes.dcrouter-db.js';
// TTL base class and constants
export * from './classes.cached.document.js';
// Cache cleaner
export * from './classes.cache.cleaner.js';
// Document classes
export * from './documents/index.js';