feat: Implement repositories for authentication, certificates, metrics, and platform services

- Added AuthRepository for user and settings management with CRUD operations.
- Introduced CertificateRepository to handle domains, certificates, and requirements.
- Created MetricsRepository for managing metrics and logs.
- Developed PlatformRepository for platform services and resources management.
- Established RegistryRepository for registry and token operations.
- Implemented ServiceRepository for CRUD operations on services.
- Defined types and interfaces in types.ts for database interactions.
This commit is contained in:
2025-11-25 23:27:27 +00:00
parent 9d58971983
commit ad89f2cc1f
18 changed files with 2249 additions and 1966 deletions

View File

@@ -0,0 +1,45 @@
# Onebox Project Hints
## SSL Certificate Storage (November 2025)
SSL certificates are now stored directly in the SQLite database as PEM content instead of file paths:
- `ISslCertificate` and `ICertificate` interfaces use `certPem`, `keyPem`, `fullchainPem` properties
- Database migration 8 converted the `certificates` table schema
- No filesystem storage for certificates - everything in DB
- `reverseproxy.ts` reads certificate PEM content from database
- `certmanager.ts` stores SmartACME certificates directly to database
## Architecture Notes
### Database Layer (November 2025 Refactoring)
The database layer has been refactored into a repository pattern:
**Directory Structure:**
```
ts/database/
├── index.ts # Main OneboxDatabase class (composes repositories, handles migrations)
├── types.ts # Shared types (TBindValue, TQueryFunction)
├── base.repository.ts # Base repository class
└── repositories/
├── index.ts # Repository exports
├── service.repository.ts # Services CRUD
├── registry.repository.ts # Registries + Registry Tokens
├── certificate.repository.ts # Domains, Certificates, Cert Requirements, SSL Certificates (legacy)
├── auth.repository.ts # Users, Settings
├── metrics.repository.ts # Metrics, Logs
└── platform.repository.ts # Platform Services, Platform Resources
```
**Import paths:**
- Main: `import { OneboxDatabase } from './database/index.ts'`
- Legacy (deprecated): `import { OneboxDatabase } from './classes/database.ts'` (re-exports from new location)
**API Compatibility:**
- The `OneboxDatabase` class maintains the same public API
- All methods delegate to the appropriate repository
- No breaking changes for existing code
## Current Migration Version: 8
Migration 8 converted certificate storage from file paths to PEM content.

View File

@@ -152,9 +152,9 @@ export class CertRequirementManager {
domainId: domain.id!, domainId: domain.id!,
certDomain: domain.domain, certDomain: domain.domain,
isWildcard: useWildcard, isWildcard: useWildcard,
certPath: certData.certPath, certPem: certData.certPem,
keyPath: certData.keyPath, keyPem: certData.keyPem,
fullChainPath: certData.fullChainPath, fullchainPem: certData.fullchainPem,
expiryDate: certData.expiryDate, expiryDate: certData.expiryDate,
issuer: certData.issuer, issuer: certData.issuer,
isValid: true, isValid: true,
@@ -239,9 +239,9 @@ export class CertRequirementManager {
// Update certificate in database // Update certificate in database
this.database.updateCertificate(cert.id!, { this.database.updateCertificate(cert.id!, {
certPath: certData.certPath, certPem: certData.certPem,
keyPath: certData.keyPath, keyPem: certData.keyPem,
fullChainPath: certData.fullChainPath, fullchainPem: certData.fullchainPem,
expiryDate: certData.expiryDate, expiryDate: certData.expiryDate,
issuer: certData.issuer, issuer: certData.issuer,
}); });
@@ -260,7 +260,7 @@ export class CertRequirementManager {
/** /**
* Clean up old invalid certificates (90+ days old) * Clean up old invalid certificates (90+ days old)
*/ */
async cleanupOldCertificates(): Promise<void> { cleanupOldCertificates(): void {
try { try {
const allCertificates = this.database.getAllCertificates(); const allCertificates = this.database.getAllCertificates();
const now = Date.now(); const now = Date.now();
@@ -272,18 +272,7 @@ export class CertRequirementManager {
// Check if certificate has been invalid for 90+ days // Check if certificate has been invalid for 90+ days
const timeSinceExpiry = now - cert.expiryDate; const timeSinceExpiry = now - cert.expiryDate;
if (timeSinceExpiry >= this.CLEANUP_DELAY_MS) { if (timeSinceExpiry >= this.CLEANUP_DELAY_MS) {
// Delete certificate files // Delete from database (PEM content is stored in DB, no files to delete)
try {
await Deno.remove(cert.certPath);
await Deno.remove(cert.keyPath);
await Deno.remove(cert.fullChainPath);
} catch (error) {
logger.debug(
`Failed to delete certificate files for ${cert.certDomain}: ${getErrorMessage(error)}`
);
}
// Delete from database
this.database.deleteCertificate(cert.id!); this.database.deleteCertificate(cert.id!);
deletedCount++; deletedCount++;
logger.info( logger.info(

View File

@@ -1,8 +1,8 @@
/** /**
* SQLite-based Certificate Manager for SmartACME * SQLite-based Certificate Manager for SmartACME
* *
* Implements ICertManager interface to store SSL certificates in SQLite database * Implements ICertManager interface to store SSL certificates directly in SQLite database.
* and write PEM files to filesystem for use by the reverse proxy. * Certificate PEM content is stored in the database, not on the filesystem.
*/ */
import * as plugins from '../plugins.ts'; import * as plugins from '../plugins.ts';
@@ -12,25 +12,16 @@ import { OneboxDatabase } from './database.ts';
export class SqliteCertManager implements plugins.smartacme.ICertManager { export class SqliteCertManager implements plugins.smartacme.ICertManager {
private database: OneboxDatabase; private database: OneboxDatabase;
private certBasePath: string;
constructor(database: OneboxDatabase, certBasePath = './.nogit/ssl/live') { constructor(database: OneboxDatabase) {
this.database = database; this.database = database;
this.certBasePath = certBasePath;
} }
/** /**
* Initialize the certificate manager * Initialize the certificate manager
*/ */
async init(): Promise<void> { async init(): Promise<void> {
try { logger.info('Certificate manager initialized (database storage)');
// Ensure certificate directory exists
await Deno.mkdir(this.certBasePath, { recursive: true });
logger.info(`Certificate manager initialized (path: ${this.certBasePath})`);
} catch (error) {
logger.error(`Failed to initialize certificate manager: ${getErrorMessage(error)}`);
throw error;
}
} }
/** /**
@@ -40,7 +31,7 @@ export class SqliteCertManager implements plugins.smartacme.ICertManager {
try { try {
const dbCert = this.database.getSSLCertificate(domainName); const dbCert = this.database.getSSLCertificate(domainName);
if (!dbCert) { if (!dbCert || !dbCert.keyPem || !dbCert.fullchainPem) {
return null; return null;
} }
@@ -49,8 +40,8 @@ export class SqliteCertManager implements plugins.smartacme.ICertManager {
id: dbCert.id?.toString() || domainName, id: dbCert.id?.toString() || domainName,
domainName: dbCert.domain, domainName: dbCert.domain,
created: dbCert.createdAt, created: dbCert.createdAt,
privateKey: await this.readPemFile(dbCert.keyPath), privateKey: dbCert.keyPem,
publicKey: await this.readPemFile(dbCert.fullChainPath), // Full chain as public key publicKey: dbCert.fullchainPem, // Full chain as public key
csr: '', // CSR not stored separately csr: '', // CSR not stored separately
validUntil: dbCert.expiryDate, validUntil: dbCert.expiryDate,
}); });
@@ -68,42 +59,28 @@ export class SqliteCertManager implements plugins.smartacme.ICertManager {
async storeCertificate(cert: plugins.smartacme.Cert): Promise<void> { async storeCertificate(cert: plugins.smartacme.Cert): Promise<void> {
try { try {
const domain = cert.domainName; const domain = cert.domainName;
const domainPath = `${this.certBasePath}/${domain}`;
// Create domain-specific directory
await Deno.mkdir(domainPath, { recursive: true });
// Write PEM files
const keyPath = `${domainPath}/privkey.pem`;
const certPath = `${domainPath}/cert.pem`;
const fullChainPath = `${domainPath}/fullchain.pem`;
await Deno.writeTextFile(keyPath, cert.privateKey);
await Deno.writeTextFile(fullChainPath, cert.publicKey);
// Extract certificate from full chain (first certificate in the chain) // Extract certificate from full chain (first certificate in the chain)
const certOnly = this.extractCertFromChain(cert.publicKey); const certPem = this.extractCertFromChain(cert.publicKey);
await Deno.writeTextFile(certPath, certOnly);
// Store/update in database // Store/update in database with PEM content
const existing = this.database.getSSLCertificate(domain); const existing = this.database.getSSLCertificate(domain);
if (existing) { if (existing) {
this.database.updateSSLCertificate(domain, { this.database.updateSSLCertificate(domain, {
certPath, certPem: certPem,
keyPath, keyPem: cert.privateKey,
fullChainPath, fullchainPem: cert.publicKey,
expiryDate: cert.validUntil, expiryDate: cert.validUntil,
updatedAt: Date.now(),
}); });
} else { } else {
await this.database.createSSLCertificate({ await this.database.createSSLCertificate({
domain, domain,
certPath, certPem: certPem,
keyPath, keyPem: cert.privateKey,
fullChainPath, fullchainPem: cert.publicKey,
expiryDate: cert.validUntil, expiryDate: cert.validUntil,
issuer: 'Let\'s Encrypt', issuer: "Let's Encrypt",
createdAt: cert.created, createdAt: cert.created,
updatedAt: Date.now(), updatedAt: Date.now(),
}); });
@@ -124,17 +101,8 @@ export class SqliteCertManager implements plugins.smartacme.ICertManager {
const dbCert = this.database.getSSLCertificate(domainName); const dbCert = this.database.getSSLCertificate(domainName);
if (dbCert) { if (dbCert) {
// Delete PEM files
const domainPath = `${this.certBasePath}/${domainName}`;
try {
await Deno.remove(domainPath, { recursive: true });
} catch (error) {
logger.warn(`Failed to delete PEM files for ${domainName}: ${getErrorMessage(error)}`);
}
// Delete from database // Delete from database
this.database.deleteSSLCertificate(domainName); this.database.deleteSSLCertificate(domainName);
logger.info(`Certificate deleted for ${domainName}`); logger.info(`Certificate deleted for ${domainName}`);
} }
} catch (error) { } catch (error) {
@@ -169,17 +137,6 @@ export class SqliteCertManager implements plugins.smartacme.ICertManager {
} }
} }
/**
* Read PEM file from filesystem
*/
private async readPemFile(path: string): Promise<string> {
try {
return await Deno.readTextFile(path);
} catch (error) {
throw new Error(`Failed to read PEM file ${path}: ${getErrorMessage(error)}`);
}
}
/** /**
* Extract the first certificate from a PEM chain * Extract the first certificate from a PEM chain
*/ */

File diff suppressed because it is too large Load Diff

View File

@@ -18,8 +18,8 @@ interface IProxyRoute {
interface ITlsConfig { interface ITlsConfig {
domain: string; domain: string;
certPath: string; certPem: string; // Certificate PEM content
keyPath: string; keyPem: string; // Private key PEM content
} }
export class OneboxReverseProxy { export class OneboxReverseProxy {
@@ -112,8 +112,8 @@ export class OneboxReverseProxy {
{ {
port: this.httpsPort, port: this.httpsPort,
hostname: '0.0.0.0', hostname: '0.0.0.0',
cert: await Deno.readTextFile(defaultConfig.certPath), cert: defaultConfig.certPem,
key: await Deno.readTextFile(defaultConfig.keyPath), key: defaultConfig.keyPem,
onListen: ({ hostname, port }) => { onListen: ({ hostname, port }) => {
logger.success(`HTTPS reverse proxy listening on https://${hostname}:${port}`); logger.success(`HTTPS reverse proxy listening on https://${hostname}:${port}`);
}, },
@@ -402,18 +402,18 @@ export class OneboxReverseProxy {
} }
/** /**
* Add TLS certificate for a domain * Add TLS certificate for a domain (using PEM content)
*/ */
async addCertificate(domain: string, certPath: string, keyPath: string): Promise<void> { addCertificate(domain: string, certPem: string, keyPem: string): void {
try { if (!certPem || !keyPem) {
// Verify certificate files exist logger.warn(`Cannot add certificate for ${domain}: missing PEM content`);
await Deno.stat(certPath); return;
await Deno.stat(keyPath); }
this.tlsConfigs.set(domain, { this.tlsConfigs.set(domain, {
domain, domain,
certPath, certPem,
keyPath, keyPem,
}); });
logger.success(`Added TLS certificate for ${domain}`); logger.success(`Added TLS certificate for ${domain}`);
@@ -423,10 +423,6 @@ export class OneboxReverseProxy {
if (this.httpsServer) { if (this.httpsServer) {
logger.warn('HTTPS server restart required for new certificate to take effect'); logger.warn('HTTPS server restart required for new certificate to take effect');
} }
} catch (error) {
logger.error(`Failed to add certificate for ${domain}: ${getErrorMessage(error)}`);
throw error;
}
} }
/** /**
@@ -441,23 +437,22 @@ export class OneboxReverseProxy {
} }
/** /**
* Reload TLS certificates from SSL manager * Reload TLS certificates from database
*/ */
async reloadCertificates(): Promise<void> { async reloadCertificates(): Promise<void> {
try { try {
logger.info('Reloading TLS certificates...'); logger.info('Reloading TLS certificates from database...');
this.tlsConfigs.clear(); this.tlsConfigs.clear();
const certificates = this.database.getAllSSLCertificates(); const certificates = this.database.getAllSSLCertificates();
for (const cert of certificates) { for (const cert of certificates) {
if (cert.domain && cert.certPath && cert.keyPath) { // Use fullchainPem for the cert (includes intermediates) and keyPem for the key
try { if (cert.domain && cert.fullchainPem && cert.keyPem) {
await this.addCertificate(cert.domain, cert.fullChainPath, cert.keyPath); this.addCertificate(cert.domain, cert.fullchainPem, cert.keyPem);
} catch (error) { } else {
logger.warn(`Failed to load certificate for ${cert.domain}: ${getErrorMessage(error)}`); logger.warn(`Skipping certificate for ${cert.domain}: missing PEM content`);
}
} }
} }

View File

@@ -92,15 +92,15 @@ export class OneboxSslManager {
/** /**
* Acquire certificate and return certificate data (for CertRequirementManager) * Acquire certificate and return certificate data (for CertRequirementManager)
* Returns certificate paths and expiry information * Returns certificate PEM content and expiry information
*/ */
async acquireCertificate( async acquireCertificate(
domain: string, domain: string,
includeWildcard = false includeWildcard = false
): Promise<{ ): Promise<{
certPath: string; certPem: string;
keyPath: string; keyPem: string;
fullChainPath: string; fullchainPem: string;
expiryDate: number; expiryDate: number;
issuer: string; issuer: string;
}> { }> {
@@ -122,8 +122,8 @@ export class OneboxSslManager {
// Reload certificates in reverse proxy // Reload certificates in reverse proxy
await this.oneboxRef.reverseProxy.reloadCertificates(); await this.oneboxRef.reverseProxy.reloadCertificates();
// The certManager stores the cert to disk and database during getCertificateForDomain // The certManager stores the cert to database during getCertificateForDomain
// Look up the paths from the database // Look up the PEM content from the database
const dbCert = this.database.getSSLCertificate(domain); const dbCert = this.database.getSSLCertificate(domain);
if (!dbCert) { if (!dbCert) {
throw new Error(`Certificate stored but not found in database for ${domain}`); throw new Error(`Certificate stored but not found in database for ${domain}`);
@@ -131,11 +131,11 @@ export class OneboxSslManager {
// Return certificate data from database // Return certificate data from database
return { return {
certPath: dbCert.certPath, certPem: dbCert.certPem,
keyPath: dbCert.keyPath, keyPem: dbCert.keyPem,
fullChainPath: dbCert.fullChainPath, fullchainPem: dbCert.fullchainPem,
expiryDate: cert.validUntil, expiryDate: cert.validUntil,
issuer: dbCert.issuer || 'Let\'s Encrypt', issuer: dbCert.issuer || "Let's Encrypt",
}; };
} catch (error) { } catch (error) {
logger.error(`Failed to acquire certificate for ${domain}: ${getErrorMessage(error)}`); logger.error(`Failed to acquire certificate for ${domain}: ${getErrorMessage(error)}`);

View File

@@ -0,0 +1,13 @@
/**
* Base repository class with common query methods
*/
import type { TBindValue, TQueryFunction } from './types.ts';
export abstract class BaseRepository {
protected query: TQueryFunction;
constructor(queryFn: TQueryFunction) {
this.query = queryFn;
}
}

1081
ts/database/index.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,83 @@
/**
* Auth Repository
* Handles CRUD operations for users and settings tables
*/
import { BaseRepository } from '../base.repository.ts';
import type { IUser } from '../../types.ts';
export class AuthRepository extends BaseRepository {
// ============ Users ============
async createUser(user: Omit<IUser, 'id'>): Promise<IUser> {
const now = Date.now();
this.query(
'INSERT INTO users (username, password_hash, role, created_at, updated_at) VALUES (?, ?, ?, ?, ?)',
[user.username, user.passwordHash, user.role, now, now]
);
return this.getUserByUsername(user.username)!;
}
getUserByUsername(username: string): IUser | null {
const rows = this.query('SELECT * FROM users WHERE username = ?', [username]);
return rows.length > 0 ? this.rowToUser(rows[0]) : null;
}
getAllUsers(): IUser[] {
const rows = this.query('SELECT * FROM users ORDER BY created_at DESC');
return rows.map((row) => this.rowToUser(row));
}
updateUserPassword(username: string, passwordHash: string): void {
this.query('UPDATE users SET password_hash = ?, updated_at = ? WHERE username = ?', [
passwordHash,
Date.now(),
username,
]);
}
deleteUser(username: string): void {
this.query('DELETE FROM users WHERE username = ?', [username]);
}
private rowToUser(row: any): IUser {
return {
id: Number(row.id || row[0]),
username: String(row.username || row[1]),
passwordHash: String(row.password_hash || row[2]),
role: String(row.role || row[3]) as IUser['role'],
createdAt: Number(row.created_at || row[4]),
updatedAt: Number(row.updated_at || row[5]),
};
}
// ============ Settings ============
getSetting(key: string): string | null {
const rows = this.query('SELECT value FROM settings WHERE key = ?', [key]);
if (rows.length === 0) return null;
const value = (rows[0] as any).value || rows[0][0];
return value ? String(value) : null;
}
setSetting(key: string, value: string): void {
const now = Date.now();
this.query(
'INSERT OR REPLACE INTO settings (key, value, updated_at) VALUES (?, ?, ?)',
[key, value, now]
);
}
getAllSettings(): Record<string, string> {
const rows = this.query('SELECT key, value FROM settings');
const settings: Record<string, string> = {};
for (const row of rows) {
const key = (row as any).key || row[0];
const value = (row as any).value || row[1];
settings[String(key)] = String(value);
}
return settings;
}
}

View File

@@ -0,0 +1,381 @@
/**
* Certificate Repository
* Handles CRUD operations for domains, certificates, cert_requirements, and legacy ssl_certificates
*/
import { BaseRepository } from '../base.repository.ts';
import type { TBindValue } from '../types.ts';
import type { IDomain, ICertificate, ICertRequirement, ISslCertificate } from '../../types.ts';
export class CertificateRepository extends BaseRepository {
// ============ Domains ============
createDomain(domain: Omit<IDomain, 'id'>): IDomain {
this.query(
`INSERT INTO domains (domain, dns_provider, cloudflare_zone_id, is_obsolete, default_wildcard, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[
domain.domain,
domain.dnsProvider,
domain.cloudflareZoneId,
domain.isObsolete ? 1 : 0,
domain.defaultWildcard ? 1 : 0,
domain.createdAt,
domain.updatedAt,
]
);
return this.getDomainByName(domain.domain)!;
}
getDomainByName(domain: string): IDomain | null {
const rows = this.query('SELECT * FROM domains WHERE domain = ?', [domain]);
return rows.length > 0 ? this.rowToDomain(rows[0]) : null;
}
getDomainById(id: number): IDomain | null {
const rows = this.query('SELECT * FROM domains WHERE id = ?', [id]);
return rows.length > 0 ? this.rowToDomain(rows[0]) : null;
}
getAllDomains(): IDomain[] {
const rows = this.query('SELECT * FROM domains ORDER BY domain ASC');
return rows.map((row) => this.rowToDomain(row));
}
getDomainsByProvider(provider: 'cloudflare' | 'manual'): IDomain[] {
const rows = this.query('SELECT * FROM domains WHERE dns_provider = ? ORDER BY domain ASC', [provider]);
return rows.map((row) => this.rowToDomain(row));
}
updateDomain(id: number, updates: Partial<IDomain>): void {
const fields: string[] = [];
const values: TBindValue[] = [];
if (updates.domain !== undefined) {
fields.push('domain = ?');
values.push(updates.domain);
}
if (updates.dnsProvider !== undefined) {
fields.push('dns_provider = ?');
values.push(updates.dnsProvider);
}
if (updates.cloudflareZoneId !== undefined) {
fields.push('cloudflare_zone_id = ?');
values.push(updates.cloudflareZoneId);
}
if (updates.isObsolete !== undefined) {
fields.push('is_obsolete = ?');
values.push(updates.isObsolete ? 1 : 0);
}
if (updates.defaultWildcard !== undefined) {
fields.push('default_wildcard = ?');
values.push(updates.defaultWildcard ? 1 : 0);
}
fields.push('updated_at = ?');
values.push(Date.now());
values.push(id);
this.query(`UPDATE domains SET ${fields.join(', ')} WHERE id = ?`, values);
}
deleteDomain(id: number): void {
this.query('DELETE FROM domains WHERE id = ?', [id]);
}
private rowToDomain(row: any): IDomain {
return {
id: Number(row.id || row[0]),
domain: String(row.domain || row[1]),
dnsProvider: (row.dns_provider || row[2]) as IDomain['dnsProvider'],
cloudflareZoneId: row.cloudflare_zone_id || row[3] || undefined,
isObsolete: Boolean(row.is_obsolete || row[4]),
defaultWildcard: Boolean(row.default_wildcard || row[5]),
createdAt: Number(row.created_at || row[6]),
updatedAt: Number(row.updated_at || row[7]),
};
}
// ============ Certificates ============
createCertificate(cert: Omit<ICertificate, 'id'>): ICertificate {
this.query(
`INSERT INTO certificates (domain_id, cert_domain, is_wildcard, cert_pem, key_pem, fullchain_pem, expiry_date, issuer, is_valid, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
cert.domainId,
cert.certDomain,
cert.isWildcard ? 1 : 0,
cert.certPem,
cert.keyPem,
cert.fullchainPem,
cert.expiryDate,
cert.issuer,
cert.isValid ? 1 : 0,
cert.createdAt,
cert.updatedAt,
]
);
const rows = this.query('SELECT * FROM certificates WHERE id = last_insert_rowid()');
return this.rowToCertificate(rows[0]);
}
getCertificateById(id: number): ICertificate | null {
const rows = this.query('SELECT * FROM certificates WHERE id = ?', [id]);
return rows.length > 0 ? this.rowToCertificate(rows[0]) : null;
}
getCertificatesByDomain(domainId: number): ICertificate[] {
const rows = this.query('SELECT * FROM certificates WHERE domain_id = ? ORDER BY expiry_date DESC', [domainId]);
return rows.map((row) => this.rowToCertificate(row));
}
getAllCertificates(): ICertificate[] {
const rows = this.query('SELECT * FROM certificates ORDER BY expiry_date DESC');
return rows.map((row) => this.rowToCertificate(row));
}
updateCertificate(id: number, updates: Partial<ICertificate>): void {
const fields: string[] = [];
const values: TBindValue[] = [];
if (updates.certDomain !== undefined) {
fields.push('cert_domain = ?');
values.push(updates.certDomain);
}
if (updates.isWildcard !== undefined) {
fields.push('is_wildcard = ?');
values.push(updates.isWildcard ? 1 : 0);
}
if (updates.certPem !== undefined) {
fields.push('cert_pem = ?');
values.push(updates.certPem);
}
if (updates.keyPem !== undefined) {
fields.push('key_pem = ?');
values.push(updates.keyPem);
}
if (updates.fullchainPem !== undefined) {
fields.push('fullchain_pem = ?');
values.push(updates.fullchainPem);
}
if (updates.expiryDate !== undefined) {
fields.push('expiry_date = ?');
values.push(updates.expiryDate);
}
if (updates.issuer !== undefined) {
fields.push('issuer = ?');
values.push(updates.issuer);
}
if (updates.isValid !== undefined) {
fields.push('is_valid = ?');
values.push(updates.isValid ? 1 : 0);
}
fields.push('updated_at = ?');
values.push(Date.now());
values.push(id);
this.query(`UPDATE certificates SET ${fields.join(', ')} WHERE id = ?`, values);
}
deleteCertificate(id: number): void {
this.query('DELETE FROM certificates WHERE id = ?', [id]);
}
private rowToCertificate(row: any): ICertificate {
return {
id: Number(row.id || row[0]),
domainId: Number(row.domain_id || row[1]),
certDomain: String(row.cert_domain || row[2]),
isWildcard: Boolean(row.is_wildcard || row[3]),
certPem: String(row.cert_pem || row[4] || ''),
keyPem: String(row.key_pem || row[5] || ''),
fullchainPem: String(row.fullchain_pem || row[6] || ''),
expiryDate: Number(row.expiry_date || row[7]),
issuer: String(row.issuer || row[8]),
isValid: Boolean(row.is_valid || row[9]),
createdAt: Number(row.created_at || row[10]),
updatedAt: Number(row.updated_at || row[11]),
};
}
// ============ Certificate Requirements ============
createCertRequirement(req: Omit<ICertRequirement, 'id'>): ICertRequirement {
this.query(
`INSERT INTO cert_requirements (service_id, domain_id, subdomain, certificate_id, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[
req.serviceId,
req.domainId,
req.subdomain,
req.certificateId,
req.status,
req.createdAt,
req.updatedAt,
]
);
const rows = this.query('SELECT * FROM cert_requirements WHERE id = last_insert_rowid()');
return this.rowToCertRequirement(rows[0]);
}
getCertRequirementById(id: number): ICertRequirement | null {
const rows = this.query('SELECT * FROM cert_requirements WHERE id = ?', [id]);
return rows.length > 0 ? this.rowToCertRequirement(rows[0]) : null;
}
getCertRequirementsByService(serviceId: number): ICertRequirement[] {
const rows = this.query('SELECT * FROM cert_requirements WHERE service_id = ?', [serviceId]);
return rows.map((row) => this.rowToCertRequirement(row));
}
getCertRequirementsByDomain(domainId: number): ICertRequirement[] {
const rows = this.query('SELECT * FROM cert_requirements WHERE domain_id = ?', [domainId]);
return rows.map((row) => this.rowToCertRequirement(row));
}
getAllCertRequirements(): ICertRequirement[] {
const rows = this.query('SELECT * FROM cert_requirements ORDER BY created_at DESC');
return rows.map((row) => this.rowToCertRequirement(row));
}
updateCertRequirement(id: number, updates: Partial<ICertRequirement>): void {
const fields: string[] = [];
const values: TBindValue[] = [];
if (updates.subdomain !== undefined) {
fields.push('subdomain = ?');
values.push(updates.subdomain);
}
if (updates.certificateId !== undefined) {
fields.push('certificate_id = ?');
values.push(updates.certificateId);
}
if (updates.status !== undefined) {
fields.push('status = ?');
values.push(updates.status);
}
fields.push('updated_at = ?');
values.push(Date.now());
values.push(id);
this.query(`UPDATE cert_requirements SET ${fields.join(', ')} WHERE id = ?`, values);
}
deleteCertRequirement(id: number): void {
this.query('DELETE FROM cert_requirements WHERE id = ?', [id]);
}
private rowToCertRequirement(row: any): ICertRequirement {
return {
id: Number(row.id || row[0]),
serviceId: Number(row.service_id || row[1]),
domainId: Number(row.domain_id || row[2]),
subdomain: String(row.subdomain || row[3]),
certificateId: row.certificate_id || row[4] || undefined,
status: String(row.status || row[5]) as ICertRequirement['status'],
createdAt: Number(row.created_at || row[6]),
updatedAt: Number(row.updated_at || row[7]),
};
}
// ============ SSL Certificates (Legacy API) ============
async createSSLCertificate(cert: Omit<ISslCertificate, 'id'>): Promise<ISslCertificate> {
// First, ensure domain exists in domains table
let domainRecord = this.getDomainByName(cert.domain);
if (!domainRecord) {
const now = Date.now();
domainRecord = this.createDomain({
domain: cert.domain,
dnsProvider: null,
isObsolete: false,
defaultWildcard: true,
createdAt: now,
updatedAt: now,
});
}
const now = Date.now();
this.query(
`INSERT INTO certificates (domain_id, cert_domain, is_wildcard, cert_pem, key_pem, fullchain_pem, expiry_date, issuer, is_valid, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
domainRecord.id,
cert.domain,
0,
cert.certPem,
cert.keyPem,
cert.fullchainPem,
cert.expiryDate,
cert.issuer,
1,
now,
now,
]
);
return this.getSSLCertificate(cert.domain)!;
}
getSSLCertificate(domain: string): ISslCertificate | null {
const rows = this.query('SELECT * FROM certificates WHERE cert_domain = ?', [domain]);
return rows.length > 0 ? this.rowToSSLCert(rows[0]) : null;
}
getAllSSLCertificates(): ISslCertificate[] {
const rows = this.query('SELECT * FROM certificates ORDER BY expiry_date ASC');
return rows.map((row) => this.rowToSSLCert(row));
}
updateSSLCertificate(domain: string, updates: Partial<ISslCertificate>): void {
const fields: string[] = [];
const values: TBindValue[] = [];
if (updates.certPem) {
fields.push('cert_pem = ?');
values.push(updates.certPem);
}
if (updates.keyPem) {
fields.push('key_pem = ?');
values.push(updates.keyPem);
}
if (updates.fullchainPem) {
fields.push('fullchain_pem = ?');
values.push(updates.fullchainPem);
}
if (updates.expiryDate) {
fields.push('expiry_date = ?');
values.push(updates.expiryDate);
}
fields.push('updated_at = ?');
values.push(Date.now());
values.push(domain);
this.query(`UPDATE certificates SET ${fields.join(', ')} WHERE cert_domain = ?`, values);
}
deleteSSLCertificate(domain: string): void {
this.query('DELETE FROM certificates WHERE cert_domain = ?', [domain]);
}
private rowToSSLCert(row: any): ISslCertificate {
return {
id: Number(row.id || row[0]),
domain: String(row.cert_domain || row[2] || ''),
certPem: String(row.cert_pem || row[4] || ''),
keyPem: String(row.key_pem || row[5] || ''),
fullchainPem: String(row.fullchain_pem || row[6] || ''),
expiryDate: Number(row.expiry_date || row[7]),
issuer: String(row.issuer || row[8]),
createdAt: Number(row.created_at || row[10]),
updatedAt: Number(row.updated_at || row[11]),
};
}
}

View File

@@ -0,0 +1,10 @@
/**
* Repository exports
*/
export { ServiceRepository } from './service.repository.ts';
export { RegistryRepository } from './registry.repository.ts';
export { CertificateRepository } from './certificate.repository.ts';
export { AuthRepository } from './auth.repository.ts';
export { MetricsRepository } from './metrics.repository.ts';
export { PlatformRepository } from './platform.repository.ts';

View File

@@ -0,0 +1,76 @@
/**
* Metrics Repository
* Handles CRUD operations for metrics and logs tables
*/
import { BaseRepository } from '../base.repository.ts';
import type { IMetric, ILogEntry } from '../../types.ts';
export class MetricsRepository extends BaseRepository {
// ============ Metrics ============
addMetric(metric: Omit<IMetric, 'id'>): void {
this.query(
`INSERT INTO metrics (service_id, timestamp, cpu_percent, memory_used, memory_limit, network_rx_bytes, network_tx_bytes)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
[
metric.serviceId,
metric.timestamp,
metric.cpuPercent,
metric.memoryUsed,
metric.memoryLimit,
metric.networkRxBytes,
metric.networkTxBytes,
]
);
}
getMetrics(serviceId: number, limit = 100): IMetric[] {
const rows = this.query(
'SELECT * FROM metrics WHERE service_id = ? ORDER BY timestamp DESC LIMIT ?',
[serviceId, limit]
);
return rows.map((row) => this.rowToMetric(row));
}
private rowToMetric(row: any): IMetric {
return {
id: Number(row.id || row[0]),
serviceId: Number(row.service_id || row[1]),
timestamp: Number(row.timestamp || row[2]),
cpuPercent: Number(row.cpu_percent || row[3]),
memoryUsed: Number(row.memory_used || row[4]),
memoryLimit: Number(row.memory_limit || row[5]),
networkRxBytes: Number(row.network_rx_bytes || row[6]),
networkTxBytes: Number(row.network_tx_bytes || row[7]),
};
}
// ============ Logs ============
addLog(log: Omit<ILogEntry, 'id'>): void {
this.query(
'INSERT INTO logs (service_id, timestamp, message, level, source) VALUES (?, ?, ?, ?, ?)',
[log.serviceId, log.timestamp, log.message, log.level, log.source]
);
}
getLogs(serviceId: number, limit = 1000): ILogEntry[] {
const rows = this.query(
'SELECT * FROM logs WHERE service_id = ? ORDER BY timestamp DESC LIMIT ?',
[serviceId, limit]
);
return rows.map((row) => this.rowToLog(row));
}
private rowToLog(row: any): ILogEntry {
return {
id: Number(row.id || row[0]),
serviceId: Number(row.service_id || row[1]),
timestamp: Number(row.timestamp || row[2]),
message: String(row.message || row[3]),
level: String(row.level || row[4]) as ILogEntry['level'],
source: String(row.source || row[5]) as ILogEntry['source'],
};
}
}

View File

@@ -0,0 +1,171 @@
/**
* Platform Repository
* Handles CRUD operations for platform_services and platform_resources tables
*/
import { BaseRepository } from '../base.repository.ts';
import type { TBindValue } from '../types.ts';
import type { IPlatformService, IPlatformResource, TPlatformServiceType } from '../../types.ts';
import { logger } from '../../logging.ts';
import { getErrorMessage } from '../../utils/error.ts';
export class PlatformRepository extends BaseRepository {
// ============ Platform Services ============
createPlatformService(service: Omit<IPlatformService, 'id'>): IPlatformService {
const now = Date.now();
this.query(
`INSERT INTO platform_services (name, type, status, container_id, config, admin_credentials_encrypted, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[
service.name,
service.type,
service.status,
service.containerId || null,
JSON.stringify(service.config),
service.adminCredentialsEncrypted || null,
now,
now,
]
);
return this.getPlatformServiceByName(service.name)!;
}
getPlatformServiceByName(name: string): IPlatformService | null {
const rows = this.query('SELECT * FROM platform_services WHERE name = ?', [name]);
return rows.length > 0 ? this.rowToPlatformService(rows[0]) : null;
}
getPlatformServiceById(id: number): IPlatformService | null {
const rows = this.query('SELECT * FROM platform_services WHERE id = ?', [id]);
return rows.length > 0 ? this.rowToPlatformService(rows[0]) : null;
}
getPlatformServiceByType(type: TPlatformServiceType): IPlatformService | null {
const rows = this.query('SELECT * FROM platform_services WHERE type = ?', [type]);
return rows.length > 0 ? this.rowToPlatformService(rows[0]) : null;
}
getAllPlatformServices(): IPlatformService[] {
const rows = this.query('SELECT * FROM platform_services ORDER BY created_at DESC');
return rows.map((row) => this.rowToPlatformService(row));
}
updatePlatformService(id: number, updates: Partial<IPlatformService>): void {
const fields: string[] = [];
const values: TBindValue[] = [];
if (updates.status !== undefined) {
fields.push('status = ?');
values.push(updates.status);
}
if (updates.containerId !== undefined) {
fields.push('container_id = ?');
values.push(updates.containerId);
}
if (updates.config !== undefined) {
fields.push('config = ?');
values.push(JSON.stringify(updates.config));
}
if (updates.adminCredentialsEncrypted !== undefined) {
fields.push('admin_credentials_encrypted = ?');
values.push(updates.adminCredentialsEncrypted);
}
fields.push('updated_at = ?');
values.push(Date.now());
values.push(id);
this.query(`UPDATE platform_services SET ${fields.join(', ')} WHERE id = ?`, values);
}
deletePlatformService(id: number): void {
this.query('DELETE FROM platform_services WHERE id = ?', [id]);
}
private rowToPlatformService(row: any): IPlatformService {
let config = { image: '', port: 0 };
const configRaw = row.config;
if (configRaw) {
try {
config = JSON.parse(String(configRaw));
} catch (e) {
logger.warn(`Failed to parse platform service config: ${getErrorMessage(e)}`);
}
}
return {
id: Number(row.id),
name: String(row.name),
type: String(row.type) as TPlatformServiceType,
status: String(row.status) as IPlatformService['status'],
containerId: row.container_id ? String(row.container_id) : undefined,
config,
adminCredentialsEncrypted: row.admin_credentials_encrypted ? String(row.admin_credentials_encrypted) : undefined,
createdAt: Number(row.created_at),
updatedAt: Number(row.updated_at),
};
}
// ============ Platform Resources ============
createPlatformResource(resource: Omit<IPlatformResource, 'id'>): IPlatformResource {
const now = Date.now();
this.query(
`INSERT INTO platform_resources (platform_service_id, service_id, resource_type, resource_name, credentials_encrypted, created_at)
VALUES (?, ?, ?, ?, ?, ?)`,
[
resource.platformServiceId,
resource.serviceId,
resource.resourceType,
resource.resourceName,
resource.credentialsEncrypted,
now,
]
);
const rows = this.query('SELECT * FROM platform_resources WHERE id = last_insert_rowid()');
return this.rowToPlatformResource(rows[0]);
}
getPlatformResourceById(id: number): IPlatformResource | null {
const rows = this.query('SELECT * FROM platform_resources WHERE id = ?', [id]);
return rows.length > 0 ? this.rowToPlatformResource(rows[0]) : null;
}
getPlatformResourcesByService(serviceId: number): IPlatformResource[] {
const rows = this.query('SELECT * FROM platform_resources WHERE service_id = ?', [serviceId]);
return rows.map((row) => this.rowToPlatformResource(row));
}
getPlatformResourcesByPlatformService(platformServiceId: number): IPlatformResource[] {
const rows = this.query('SELECT * FROM platform_resources WHERE platform_service_id = ?', [platformServiceId]);
return rows.map((row) => this.rowToPlatformResource(row));
}
getAllPlatformResources(): IPlatformResource[] {
const rows = this.query('SELECT * FROM platform_resources ORDER BY created_at DESC');
return rows.map((row) => this.rowToPlatformResource(row));
}
deletePlatformResource(id: number): void {
this.query('DELETE FROM platform_resources WHERE id = ?', [id]);
}
deletePlatformResourcesByService(serviceId: number): void {
this.query('DELETE FROM platform_resources WHERE service_id = ?', [serviceId]);
}
private rowToPlatformResource(row: any): IPlatformResource {
return {
id: Number(row.id),
platformServiceId: Number(row.platform_service_id),
serviceId: Number(row.service_id),
resourceType: String(row.resource_type) as IPlatformResource['resourceType'],
resourceName: String(row.resource_name),
credentialsEncrypted: String(row.credentials_encrypted),
createdAt: Number(row.created_at),
};
}
}

View File

@@ -0,0 +1,123 @@
/**
* Registry Repository
* Handles CRUD operations for registries and registry_tokens tables
*/
import { BaseRepository } from '../base.repository.ts';
import type { IRegistry, IRegistryToken } from '../../types.ts';
export class RegistryRepository extends BaseRepository {
// ============ Registries ============
async createRegistry(registry: Omit<IRegistry, 'id'>): Promise<IRegistry> {
const now = Date.now();
this.query(
'INSERT INTO registries (url, username, password_encrypted, created_at) VALUES (?, ?, ?, ?)',
[registry.url, registry.username, registry.passwordEncrypted, now]
);
return this.getRegistryByURL(registry.url)!;
}
getRegistryByURL(url: string): IRegistry | null {
const rows = this.query('SELECT * FROM registries WHERE url = ?', [url]);
return rows.length > 0 ? this.rowToRegistry(rows[0]) : null;
}
getAllRegistries(): IRegistry[] {
const rows = this.query('SELECT * FROM registries ORDER BY created_at DESC');
return rows.map((row) => this.rowToRegistry(row));
}
deleteRegistry(url: string): void {
this.query('DELETE FROM registries WHERE url = ?', [url]);
}
private rowToRegistry(row: any): IRegistry {
return {
id: Number(row.id || row[0]),
url: String(row.url || row[1]),
username: String(row.username || row[2]),
passwordEncrypted: String(row.password_encrypted || row[3]),
createdAt: Number(row.created_at || row[4]),
};
}
// ============ Registry Tokens ============
createToken(token: Omit<IRegistryToken, 'id'>): IRegistryToken {
const scopeJson = Array.isArray(token.scope) ? JSON.stringify(token.scope) : token.scope;
this.query(
`INSERT INTO registry_tokens (name, token_hash, token_type, scope, expires_at, created_at, last_used_at, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[
token.name,
token.tokenHash,
token.type,
scopeJson,
token.expiresAt,
token.createdAt,
token.lastUsedAt,
token.createdBy,
]
);
const rows = this.query('SELECT * FROM registry_tokens WHERE id = last_insert_rowid()');
return this.rowToToken(rows[0]);
}
getTokenById(id: number): IRegistryToken | null {
const rows = this.query('SELECT * FROM registry_tokens WHERE id = ?', [id]);
return rows.length > 0 ? this.rowToToken(rows[0]) : null;
}
getTokenByHash(tokenHash: string): IRegistryToken | null {
const rows = this.query('SELECT * FROM registry_tokens WHERE token_hash = ?', [tokenHash]);
return rows.length > 0 ? this.rowToToken(rows[0]) : null;
}
getAllTokens(): IRegistryToken[] {
const rows = this.query('SELECT * FROM registry_tokens ORDER BY created_at DESC');
return rows.map((row) => this.rowToToken(row));
}
getTokensByType(type: 'global' | 'ci'): IRegistryToken[] {
const rows = this.query('SELECT * FROM registry_tokens WHERE token_type = ? ORDER BY created_at DESC', [type]);
return rows.map((row) => this.rowToToken(row));
}
updateTokenLastUsed(id: number): void {
this.query('UPDATE registry_tokens SET last_used_at = ? WHERE id = ?', [Date.now(), id]);
}
deleteToken(id: number): void {
this.query('DELETE FROM registry_tokens WHERE id = ?', [id]);
}
private rowToToken(row: any): IRegistryToken {
let scope: 'all' | string[];
const scopeRaw = row.scope || row[4];
if (scopeRaw === 'all') {
scope = 'all';
} else {
try {
scope = JSON.parse(String(scopeRaw));
} catch {
scope = 'all';
}
}
return {
id: Number(row.id || row[0]),
name: String(row.name || row[1]),
tokenHash: String(row.token_hash || row[2]),
type: String(row.token_type || row[3]) as IRegistryToken['type'],
scope,
expiresAt: row.expires_at || row[5] ? Number(row.expires_at || row[5]) : null,
createdAt: Number(row.created_at || row[6]),
lastUsedAt: row.last_used_at || row[7] ? Number(row.last_used_at || row[7]) : null,
createdBy: String(row.created_by || row[8]),
};
}
}

View File

@@ -0,0 +1,177 @@
/**
* Service Repository
* Handles CRUD operations for services table
*/
import { BaseRepository } from '../base.repository.ts';
import type { TBindValue } from '../types.ts';
import type { IService, IPlatformRequirements } from '../../types.ts';
import { logger } from '../../logging.ts';
import { getErrorMessage } from '../../utils/error.ts';
export class ServiceRepository extends BaseRepository {
async create(service: Omit<IService, 'id'>): Promise<IService> {
const now = Date.now();
this.query(
`INSERT INTO services (
name, image, registry, env_vars, port, domain, container_id, status,
created_at, updated_at,
use_onebox_registry, registry_repository, registry_image_tag,
auto_update_on_push, image_digest, platform_requirements
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
service.name,
service.image,
service.registry || null,
JSON.stringify(service.envVars),
service.port,
service.domain || null,
service.containerID || null,
service.status,
now,
now,
service.useOneboxRegistry ? 1 : 0,
service.registryRepository || null,
service.registryImageTag || 'latest',
service.autoUpdateOnPush ? 1 : 0,
service.imageDigest || null,
JSON.stringify(service.platformRequirements || {}),
]
);
return this.getByName(service.name)!;
}
getByName(name: string): IService | null {
const rows = this.query('SELECT * FROM services WHERE name = ?', [name]);
if (rows.length > 0) {
logger.info(`getServiceByName: raw row data: ${JSON.stringify(rows[0])}`);
const service = this.rowToService(rows[0]);
logger.info(`getServiceByName: service object containerID: ${service.containerID}`);
return service;
}
return null;
}
getById(id: number): IService | null {
const rows = this.query('SELECT * FROM services WHERE id = ?', [id]);
return rows.length > 0 ? this.rowToService(rows[0]) : null;
}
getAll(): IService[] {
const rows = this.query('SELECT * FROM services ORDER BY created_at DESC');
return rows.map((row) => this.rowToService(row));
}
update(id: number, updates: Partial<IService>): void {
const fields: string[] = [];
const values: TBindValue[] = [];
if (updates.image !== undefined) {
fields.push('image = ?');
values.push(updates.image);
}
if (updates.registry !== undefined) {
fields.push('registry = ?');
values.push(updates.registry);
}
if (updates.envVars !== undefined) {
fields.push('env_vars = ?');
values.push(JSON.stringify(updates.envVars));
}
if (updates.port !== undefined) {
fields.push('port = ?');
values.push(updates.port);
}
if (updates.domain !== undefined) {
fields.push('domain = ?');
values.push(updates.domain);
}
if (updates.containerID !== undefined) {
fields.push('container_id = ?');
values.push(updates.containerID);
}
if (updates.status !== undefined) {
fields.push('status = ?');
values.push(updates.status);
}
if (updates.useOneboxRegistry !== undefined) {
fields.push('use_onebox_registry = ?');
values.push(updates.useOneboxRegistry ? 1 : 0);
}
if (updates.registryRepository !== undefined) {
fields.push('registry_repository = ?');
values.push(updates.registryRepository);
}
if (updates.registryImageTag !== undefined) {
fields.push('registry_image_tag = ?');
values.push(updates.registryImageTag);
}
if (updates.autoUpdateOnPush !== undefined) {
fields.push('auto_update_on_push = ?');
values.push(updates.autoUpdateOnPush ? 1 : 0);
}
if (updates.imageDigest !== undefined) {
fields.push('image_digest = ?');
values.push(updates.imageDigest);
}
if (updates.platformRequirements !== undefined) {
fields.push('platform_requirements = ?');
values.push(JSON.stringify(updates.platformRequirements));
}
fields.push('updated_at = ?');
values.push(Date.now());
values.push(id);
this.query(`UPDATE services SET ${fields.join(', ')} WHERE id = ?`, values);
}
delete(id: number): void {
this.query('DELETE FROM services WHERE id = ?', [id]);
}
private rowToService(row: any): IService {
let envVars = {};
const envVarsRaw = row.env_vars || row[4];
if (envVarsRaw && envVarsRaw !== 'undefined' && envVarsRaw !== 'null') {
try {
envVars = JSON.parse(String(envVarsRaw));
} catch (e) {
logger.warn(`Failed to parse env_vars for service: ${getErrorMessage(e)}`);
envVars = {};
}
}
let platformRequirements: IPlatformRequirements | undefined;
const platformReqRaw = row.platform_requirements;
if (platformReqRaw && platformReqRaw !== 'undefined' && platformReqRaw !== 'null' && platformReqRaw !== '{}') {
try {
platformRequirements = JSON.parse(String(platformReqRaw));
} catch (e) {
logger.warn(`Failed to parse platform_requirements for service: ${getErrorMessage(e)}`);
platformRequirements = undefined;
}
}
return {
id: Number(row.id || row[0]),
name: String(row.name || row[1]),
image: String(row.image || row[2]),
registry: (row.registry || row[3]) ? String(row.registry || row[3]) : undefined,
envVars,
port: Number(row.port || row[5]),
domain: (row.domain || row[6]) ? String(row.domain || row[6]) : undefined,
containerID: (row.container_id || row[7]) ? String(row.container_id || row[7]) : undefined,
status: String(row.status || row[8]) as IService['status'],
createdAt: Number(row.created_at || row[9]),
updatedAt: Number(row.updated_at || row[10]),
useOneboxRegistry: row.use_onebox_registry ? Boolean(row.use_onebox_registry) : undefined,
registryRepository: row.registry_repository ? String(row.registry_repository) : undefined,
registryImageTag: row.registry_image_tag ? String(row.registry_image_tag) : undefined,
autoUpdateOnPush: row.auto_update_on_push ? Boolean(row.auto_update_on_push) : undefined,
imageDigest: row.image_digest ? String(row.image_digest) : undefined,
platformRequirements,
};
}
}

17
ts/database/types.ts Normal file
View File

@@ -0,0 +1,17 @@
/**
* Database types and interfaces
*/
import * as plugins from '../plugins.ts';
// Type alias for sqlite bind parameters
export type TBindValue = string | number | bigint | boolean | null | undefined | Uint8Array;
// Database connection type
export type TDatabaseConnection = InstanceType<typeof plugins.sqlite.DB>;
// Query function type
export type TQueryFunction = <T = Record<string, unknown>>(
sql: string,
params?: TBindValue[]
) => T[];

View File

@@ -4,7 +4,7 @@
export { Onebox } from './classes/onebox.ts'; export { Onebox } from './classes/onebox.ts';
export { runCli } from './cli.ts'; export { runCli } from './cli.ts';
export { OneboxDatabase } from './classes/database.ts'; export { OneboxDatabase } from './database/index.ts';
export { OneboxDockerManager } from './classes/docker.ts'; export { OneboxDockerManager } from './classes/docker.ts';
export { OneboxServicesManager } from './classes/services.ts'; export { OneboxServicesManager } from './classes/services.ts';
export { OneboxRegistriesManager } from './classes/registries.ts'; export { OneboxRegistriesManager } from './classes/registries.ts';

View File

@@ -153,9 +153,9 @@ export interface ICertificate {
domainId: number; domainId: number;
certDomain: string; certDomain: string;
isWildcard: boolean; isWildcard: boolean;
certPath: string; certPem: string; // Certificate PEM content
keyPath: string; keyPem: string; // Private key PEM content
fullChainPath: string; fullchainPem: string; // Full chain PEM content (cert + intermediates)
expiryDate: number; expiryDate: number;
issuer: string; issuer: string;
isValid: boolean; isValid: boolean;
@@ -183,13 +183,13 @@ export interface IDomainView {
daysRemaining: number | null; daysRemaining: number | null;
} }
// Legacy SSL certificate type (for backward compatibility) // SSL certificate type - stores certificate content directly in database
export interface ISslCertificate { export interface ISslCertificate {
id?: number; id?: number;
domain: string; domain: string;
certPath: string; certPem: string; // Certificate PEM content
keyPath: string; keyPem: string; // Private key PEM content
fullChainPath: string; fullchainPem: string; // Full chain PEM content (cert + intermediates)
expiryDate: number; expiryDate: number;
issuer: string; issuer: string;
createdAt: number; createdAt: number;