From c23792496b681cc88e2ce7bbd9b09027f381f264 Mon Sep 17 00:00:00 2001 From: saridsa2 Date: Sun, 12 Apr 2026 16:05:24 +0530 Subject: [PATCH] =?UTF-8?q?feat(sidecar):=20Ozonetel=20admin=20auth=20serv?= =?UTF-8?q?ice=20=E2=80=94=20RSA=20login,=20JWT=20cache?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Node crypto RSA encryption (not jsencrypt — server-side) - Pre-login public key fetch, encrypted login, JWT caching - Auto-refresh before token expiry (decoded from JWT payload) - Auth headers: Bearer token + userId + userName + isSuperAdmin - Registered in SupervisorModule with ConfigThemeModule import Co-Authored-By: Claude Opus 4.6 (1M context) --- src/ozonetel/ozonetel-admin-auth.service.ts | 123 ++++++++++++++++++++ src/supervisor/supervisor.module.ts | 8 +- 2 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 src/ozonetel/ozonetel-admin-auth.service.ts diff --git a/src/ozonetel/ozonetel-admin-auth.service.ts b/src/ozonetel/ozonetel-admin-auth.service.ts new file mode 100644 index 0000000..46f0b0c --- /dev/null +++ b/src/ozonetel/ozonetel-admin-auth.service.ts @@ -0,0 +1,123 @@ +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(publicKeyPem: string, plaintext: string): string { + const buffer = Buffer.from(plaintext, 'utf8'); + const encrypted = publicEncrypt( + { key: publicKeyPem, 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); + } +} diff --git a/src/supervisor/supervisor.module.ts b/src/supervisor/supervisor.module.ts index e5c1cbc..3e37234 100644 --- a/src/supervisor/supervisor.module.ts +++ b/src/supervisor/supervisor.module.ts @@ -1,13 +1,15 @@ import { Module, forwardRef } from '@nestjs/common'; import { PlatformModule } from '../platform/platform.module'; import { OzonetelAgentModule } from '../ozonetel/ozonetel-agent.module'; +import { ConfigThemeModule } from '../config/config-theme.module'; import { SupervisorController } from './supervisor.controller'; import { SupervisorService } from './supervisor.service'; +import { OzonetelAdminAuthService } from '../ozonetel/ozonetel-admin-auth.service'; @Module({ - imports: [PlatformModule, forwardRef(() => OzonetelAgentModule)], + imports: [PlatformModule, forwardRef(() => OzonetelAgentModule), ConfigThemeModule], controllers: [SupervisorController], - providers: [SupervisorService], - exports: [SupervisorService], + providers: [SupervisorService, OzonetelAdminAuthService], + exports: [SupervisorService, OzonetelAdminAuthService], }) export class SupervisorModule {}