import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Cron, CronExpression } from '@nestjs/schedule'; import { PrismaService } from '../prisma/prisma.service'; import { probeTlsHosts } from '../common/tls-probe'; @Injectable() export class InternalSslService implements OnModuleInit { private readonly logger = new Logger(InternalSslService.name); private refreshRunning = false; constructor( private readonly prisma: PrismaService, private readonly config: ConfigService, ) {} onModuleInit() { // Warm ssl_enabled shortly after boot so lists are accurate without waiting for cron. setTimeout(() => { void this.refreshSslStatuses('startup'); }, 5_000); } private async listActiveDomains(): Promise> { return this.prisma.domain.findMany({ where: { isActive: true }, select: { id: true, host: true, sslEnabled: true }, orderBy: { host: 'asc' }, }); } private async listActiveApexHosts(): Promise { const domains = await this.listActiveDomains(); const hosts: string[] = []; for (const { host } of domains) { const apex = host.trim().toLowerCase(); if (apex) hosts.push(apex); } return hosts; } /** Dashboards VPS: manage + business./customer. per active apex. */ async listDashboardHosts(): Promise<{ hosts: string[] }> { const adminHost = this.config.get('DASHBOARD_ADMIN_HOST')?.trim() || 'manage.meshkee.com'; const hosts = new Set([adminHost]); for (const apex of await this.listActiveApexHosts()) { hosts.add(`business.${apex}`); hosts.add(`customer.${apex}`); } return { hosts: [...hosts].sort() }; } /** * API VPS: central api host + api.{apex} aliases for each active domain. * Same Nest process; nginx terminates TLS for every name on this list. */ async listApiHosts(): Promise<{ hosts: string[] }> { const centralHost = this.config.get('CENTRAL_API_HOST')?.trim() || 'api.meshkee.com'; const hosts = new Set([centralHost]); for (const apex of await this.listActiveApexHosts()) { hosts.add(`api.${apex}`); } return { hosts: [...hosts].sort() }; } /** * Probe each active apex host and sync domains.ssl_enabled to the live result. * Runs every 2 hours (and once shortly after API boot). */ @Cron(CronExpression.EVERY_2_HOURS) async refreshSslStatusesCron() { await this.refreshSslStatuses('cron'); } async refreshSslStatuses(reason: 'cron' | 'startup' | 'manual' = 'manual') { if (this.refreshRunning) { this.logger.warn(`SSL status refresh skipped (${reason}): already running`); return { updated: 0, checked: 0, skipped: true as const }; } this.refreshRunning = true; const started = Date.now(); try { const domains = await this.listActiveDomains(); const hostsToProbe = domains.flatMap((d) => { const apex = d.host.trim().toLowerCase(); if (!apex) return []; const www = apex.startsWith('www.') ? apex : `www.${apex}`; return [apex, www]; }); const sslByHost = await probeTlsHosts(hostsToProbe); let updated = 0; for (const domain of domains) { const hostKey = domain.host.trim().toLowerCase(); const wwwKey = hostKey.startsWith('www.') ? hostKey : `www.${hostKey}`; const sslEnabled = (sslByHost.get(hostKey) ?? false) && (sslByHost.get(wwwKey) ?? false); if (sslEnabled === domain.sslEnabled) continue; await this.prisma.domain.update({ where: { id: domain.id }, data: { sslEnabled }, }); updated += 1; this.logger.log( `SSL status ${domain.host}: ${domain.sslEnabled} → ${sslEnabled}`, ); } this.logger.log( `SSL status refresh (${reason}): checked=${domains.length} updated=${updated} in ${Date.now() - started}ms`, ); return { updated, checked: domains.length, skipped: false as const }; } finally { this.refreshRunning = false; } } }