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

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

View File

@@ -1,8 +1,8 @@
/**
* SQLite-based Certificate Manager for SmartACME
*
* Implements ICertManager interface to store SSL certificates in SQLite database
* and write PEM files to filesystem for use by the reverse proxy.
* Implements ICertManager interface to store SSL certificates directly in SQLite database.
* Certificate PEM content is stored in the database, not on the filesystem.
*/
import * as plugins from '../plugins.ts';
@@ -12,25 +12,16 @@ import { OneboxDatabase } from './database.ts';
export class SqliteCertManager implements plugins.smartacme.ICertManager {
private database: OneboxDatabase;
private certBasePath: string;
constructor(database: OneboxDatabase, certBasePath = './.nogit/ssl/live') {
constructor(database: OneboxDatabase) {
this.database = database;
this.certBasePath = certBasePath;
}
/**
* Initialize the certificate manager
*/
async init(): Promise<void> {
try {
// 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;
}
logger.info('Certificate manager initialized (database storage)');
}
/**
@@ -40,7 +31,7 @@ export class SqliteCertManager implements plugins.smartacme.ICertManager {
try {
const dbCert = this.database.getSSLCertificate(domainName);
if (!dbCert) {
if (!dbCert || !dbCert.keyPem || !dbCert.fullchainPem) {
return null;
}
@@ -49,8 +40,8 @@ export class SqliteCertManager implements plugins.smartacme.ICertManager {
id: dbCert.id?.toString() || domainName,
domainName: dbCert.domain,
created: dbCert.createdAt,
privateKey: await this.readPemFile(dbCert.keyPath),
publicKey: await this.readPemFile(dbCert.fullChainPath), // Full chain as public key
privateKey: dbCert.keyPem,
publicKey: dbCert.fullchainPem, // Full chain as public key
csr: '', // CSR not stored separately
validUntil: dbCert.expiryDate,
});
@@ -68,42 +59,28 @@ export class SqliteCertManager implements plugins.smartacme.ICertManager {
async storeCertificate(cert: plugins.smartacme.Cert): Promise<void> {
try {
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)
const certOnly = this.extractCertFromChain(cert.publicKey);
await Deno.writeTextFile(certPath, certOnly);
const certPem = this.extractCertFromChain(cert.publicKey);
// Store/update in database
// Store/update in database with PEM content
const existing = this.database.getSSLCertificate(domain);
if (existing) {
this.database.updateSSLCertificate(domain, {
certPath,
keyPath,
fullChainPath,
certPem: certPem,
keyPem: cert.privateKey,
fullchainPem: cert.publicKey,
expiryDate: cert.validUntil,
updatedAt: Date.now(),
});
} else {
await this.database.createSSLCertificate({
domain,
certPath,
keyPath,
fullChainPath,
certPem: certPem,
keyPem: cert.privateKey,
fullchainPem: cert.publicKey,
expiryDate: cert.validUntil,
issuer: 'Let\'s Encrypt',
issuer: "Let's Encrypt",
createdAt: cert.created,
updatedAt: Date.now(),
});
@@ -124,17 +101,8 @@ export class SqliteCertManager implements plugins.smartacme.ICertManager {
const dbCert = this.database.getSSLCertificate(domainName);
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
this.database.deleteSSLCertificate(domainName);
logger.info(`Certificate deleted for ${domainName}`);
}
} 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
*/

File diff suppressed because it is too large Load Diff

View File

@@ -18,8 +18,8 @@ interface IProxyRoute {
interface ITlsConfig {
domain: string;
certPath: string;
keyPath: string;
certPem: string; // Certificate PEM content
keyPem: string; // Private key PEM content
}
export class OneboxReverseProxy {
@@ -112,8 +112,8 @@ export class OneboxReverseProxy {
{
port: this.httpsPort,
hostname: '0.0.0.0',
cert: await Deno.readTextFile(defaultConfig.certPath),
key: await Deno.readTextFile(defaultConfig.keyPath),
cert: defaultConfig.certPem,
key: defaultConfig.keyPem,
onListen: ({ hostname, port }) => {
logger.success(`HTTPS reverse proxy listening on https://${hostname}:${port}`);
},
@@ -402,30 +402,26 @@ 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> {
try {
// Verify certificate files exist
await Deno.stat(certPath);
await Deno.stat(keyPath);
addCertificate(domain: string, certPem: string, keyPem: string): void {
if (!certPem || !keyPem) {
logger.warn(`Cannot add certificate for ${domain}: missing PEM content`);
return;
}
this.tlsConfigs.set(domain, {
domain,
certPath,
keyPath,
});
this.tlsConfigs.set(domain, {
domain,
certPem,
keyPem,
});
logger.success(`Added TLS certificate for ${domain}`);
logger.success(`Added TLS certificate for ${domain}`);
// If HTTPS server is already running, we need to restart it
// TODO: Implement hot reload for certificates
if (this.httpsServer) {
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;
// If HTTPS server is already running, we need to restart it
// TODO: Implement hot reload for certificates
if (this.httpsServer) {
logger.warn('HTTPS server restart required for new certificate to take effect');
}
}
@@ -441,23 +437,22 @@ export class OneboxReverseProxy {
}
/**
* Reload TLS certificates from SSL manager
* Reload TLS certificates from database
*/
async reloadCertificates(): Promise<void> {
try {
logger.info('Reloading TLS certificates...');
logger.info('Reloading TLS certificates from database...');
this.tlsConfigs.clear();
const certificates = this.database.getAllSSLCertificates();
for (const cert of certificates) {
if (cert.domain && cert.certPath && cert.keyPath) {
try {
await this.addCertificate(cert.domain, cert.fullChainPath, cert.keyPath);
} catch (error) {
logger.warn(`Failed to load certificate for ${cert.domain}: ${getErrorMessage(error)}`);
}
// Use fullchainPem for the cert (includes intermediates) and keyPem for the key
if (cert.domain && cert.fullchainPem && cert.keyPem) {
this.addCertificate(cert.domain, cert.fullchainPem, cert.keyPem);
} else {
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)
* Returns certificate paths and expiry information
* Returns certificate PEM content and expiry information
*/
async acquireCertificate(
domain: string,
includeWildcard = false
): Promise<{
certPath: string;
keyPath: string;
fullChainPath: string;
certPem: string;
keyPem: string;
fullchainPem: string;
expiryDate: number;
issuer: string;
}> {
@@ -122,8 +122,8 @@ export class OneboxSslManager {
// Reload certificates in reverse proxy
await this.oneboxRef.reverseProxy.reloadCertificates();
// The certManager stores the cert to disk and database during getCertificateForDomain
// Look up the paths from the database
// The certManager stores the cert to database during getCertificateForDomain
// Look up the PEM content from the database
const dbCert = this.database.getSSLCertificate(domain);
if (!dbCert) {
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 {
certPath: dbCert.certPath,
keyPath: dbCert.keyPath,
fullChainPath: dbCert.fullChainPath,
certPem: dbCert.certPem,
keyPem: dbCert.keyPem,
fullchainPem: dbCert.fullchainPem,
expiryDate: cert.validUntil,
issuer: dbCert.issuer || 'Let\'s Encrypt',
issuer: dbCert.issuer || "Let's Encrypt",
};
} catch (error) {
logger.error(`Failed to acquire certificate for ${domain}: ${getErrorMessage(error)}`);