mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-05-18 20:08:19 +00:00
Ozonetel returns raw base64 public key without PEM headers. Node's crypto.publicEncrypt requires PEM format. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
128 lines
4.9 KiB
TypeScript
128 lines
4.9 KiB
TypeScript
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 <jwt>, 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<Record<string, string>> {
|
|
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<string> {
|
|
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<string> {
|
|
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);
|
|
}
|
|
}
|