import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; import { publicEncrypt, constants as cryptoConstants } from 'crypto'; import axios from 'axios'; import { TelephonyConfigService } from '../config/telephony-config.service'; // Ozonetel admin API auth — login with RSA-encrypted credentials, cache JWT. // Used by supervisor barge endpoints to call dashboardApi. // // Auth flow (from CA-Admin source code): // 1. GET /api/auth/public-key → { publicKey, keyId } // 2. RSA-encrypt username + password with publicKey // 3. POST /auth/login → JWT token // 4. All admin API calls use: Authorization: Bearer , userId, userName, isSuperAdmin @Injectable() export class OzonetelAdminAuthService implements OnModuleInit { private readonly logger = new Logger(OzonetelAdminAuthService.name); private cachedToken: string | null = null; private cachedUserId: string | null = null; private cachedUserName: string | null = null; private tokenExpiresAt = 0; constructor(private readonly telephony: TelephonyConfigService) {} async onModuleInit() { const config = this.telephony.getConfig(); if (config.ozonetel.adminUsername && config.ozonetel.adminPassword) { this.logger.log('Ozonetel admin credentials configured — will authenticate on first use'); } else { this.logger.warn('Ozonetel admin credentials not configured — supervisor barge will be unavailable'); } } private get apiBase(): string { return 'https://api.cloudagent.ozonetel.com'; } async getAuthHeaders(): Promise> { const token = await this.getToken(); return { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, 'userId': this.cachedUserId ?? '', 'userName': this.cachedUserName ?? '', 'isSuperAdmin': 'true', 'dAccessType': 'false', }; } async getToken(): Promise { if (this.cachedToken && Date.now() < this.tokenExpiresAt) { return this.cachedToken; } return this.login(); } private rsaEncrypt(publicKeyRaw: string, plaintext: string): string { // Ozonetel returns raw base64 without PEM headers — wrap it const pem = publicKeyRaw.includes('-----BEGIN') ? publicKeyRaw : `-----BEGIN PUBLIC KEY-----\n${publicKeyRaw}\n-----END PUBLIC KEY-----`; const buffer = Buffer.from(plaintext, 'utf8'); const encrypted = publicEncrypt( { key: pem, padding: cryptoConstants.RSA_PKCS1_PADDING }, buffer, ); return encrypted.toString('base64'); } private async login(): Promise { const config = this.telephony.getConfig(); const { adminUsername, adminPassword } = config.ozonetel; if (!adminUsername || !adminPassword) { throw new Error('Ozonetel admin credentials not configured'); } // Step 1: Get RSA public key this.logger.log('Fetching Ozonetel public key...'); const preLoginRes = await axios.get(`${this.apiBase}/api/auth/public-key`); const { publicKey, keyId } = preLoginRes.data; if (!publicKey || !keyId) { throw new Error('Failed to get Ozonetel public key'); } // Step 2: RSA-encrypt credentials using Node crypto const encryptedUsername = this.rsaEncrypt(publicKey, adminUsername); const encryptedPassword = this.rsaEncrypt(publicKey, adminPassword); // Step 3: Login this.logger.log('Logging into Ozonetel admin portal...'); const loginRes = await axios.post(`${this.apiBase}/auth/login`, { username: encryptedUsername, password: encryptedPassword, keyId, ltype: 'PORTAL', }, { headers: { 'Content-Type': 'application/json' }, }); const data = loginRes.data; if (!data.token) { throw new Error(`Ozonetel admin login failed: ${JSON.stringify(data)}`); } this.cachedToken = data.token; this.cachedUserId = data.userId?.toString() ?? data.UserId?.toString() ?? ''; this.cachedUserName = data.name ?? adminUsername; // Decode token expiry — fallback to 6 hours try { const payload = JSON.parse(Buffer.from(data.token.split('.')[1], 'base64').toString()); this.tokenExpiresAt = (payload.exp ?? 0) * 1000 - 60_000; // refresh 1 min early } catch { this.tokenExpiresAt = Date.now() + 6 * 60 * 60 * 1000; } this.logger.log(`Ozonetel admin login successful (userId=${this.cachedUserId}, expires in ${Math.round((this.tokenExpiresAt - Date.now()) / 60000)}min)`); return this.cachedToken!; } isConfigured(): boolean { const config = this.telephony.getConfig(); return !!(config.ozonetel.adminUsername && config.ozonetel.adminPassword); } }