Files
IdP/apps/sso-core/src/domain/oauth-ssl-sans.service.ts
2026-06-29 23:35:12 +03:00

100 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { mkdir, writeFile } from 'node:fs/promises';
import { dirname } from 'node:path';
import { PrismaService } from '../infra/prisma.service';
import {
collectOAuthSslHostnames,
parseHostnameFromPublicUrl,
resolveRegistrableDomain
} from '../infra/oauth-ssl-sans.util';
const OAUTH_SSL_SANS_SETTING_KEY = 'OAUTH_SSL_SANS';
@Injectable()
export class OAuthSslSansService implements OnModuleInit {
private readonly logger = new Logger(OAuthSslSansService.name);
constructor(
private readonly prisma: PrismaService,
private readonly config: ConfigService
) {}
async onModuleInit() {
try {
await this.syncFromDatabase();
} catch (error) {
this.logger.warn(
`Не удалось синхронизировать SAN из OAuth redirect URI: ${error instanceof Error ? error.message : 'ошибка'}`
);
}
}
async syncFromDatabase(): Promise<string[]> {
const clients = await this.prisma.oAuthClient.findMany({
where: { isActive: true },
select: { redirectUris: true }
});
const redirectUris = clients.flatMap((client) => client.redirectUris);
const hostnames = collectOAuthSslHostnames(redirectUris, this.buildExcludedHostnames());
await this.persistHostnames(hostnames);
return hostnames;
}
private buildExcludedHostnames(): Set<string> {
const excluded = new Set<string>();
for (const envKey of ['DOMAIN_API', 'DOMAIN_FRONTEND', 'DOMAIN_DOCS', 'DOMAIN_WS', 'DOMAIN_MINIO', 'DOMAIN_MINIO_CONSOLE']) {
const value = this.config.get<string>(envKey)?.trim().toLowerCase();
if (value) excluded.add(value);
}
for (const url of [
this.config.get<string>('PUBLIC_API_URL'),
this.config.get<string>('PUBLIC_FRONTEND_URL'),
this.config.get<string>('PUBLIC_DOCS_URL'),
this.config.get<string>('PUBLIC_WS_URL')
]) {
const host = parseHostnameFromPublicUrl(url);
if (host) excluded.add(host);
}
const idpHosts = [...excluded];
for (const host of idpHosts) {
excluded.add(resolveRegistrableDomain(host));
const parts = host.split('.');
if (parts.length >= 3) {
excluded.add(parts.slice(-2).join('.'));
}
}
return excluded;
}
private async persistHostnames(hostnames: string[]) {
const serialized = hostnames.join(',');
await this.prisma.systemSetting.upsert({
where: { key: OAUTH_SSL_SANS_SETTING_KEY },
create: {
key: OAUTH_SSL_SANS_SETTING_KEY,
value: serialized,
description: 'DNS-имена из redirect_uri OAuth-приложений для SAN сертификата (авто)',
isSecret: false
},
update: {
value: serialized,
description: 'DNS-имена из redirect_uri OAuth-приложений для SAN сертификата (авто)'
}
});
const targetFile =
this.config.get<string>('OAUTH_SSL_SANS_FILE')?.trim() ||
'/data/nginx-certs/oauth-redirect-sans.txt';
const body = `${hostnames.join('\n')}\n`;
await mkdir(dirname(targetFile), { recursive: true });
await writeFile(targetFile, body, 'utf8');
this.logger.log(`OAuth SSL SAN обновлён (${hostnames.length}): ${hostnames.join(', ') || '—'}`);
}
}