mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-05-18 20:08:19 +00:00
feat(sidecar): Ozonetel admin auth service — RSA login, JWT cache
- 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) <noreply@anthropic.com>
This commit is contained in:
123
src/ozonetel/ozonetel-admin-auth.service.ts
Normal file
123
src/ozonetel/ozonetel-admin-auth.service.ts
Normal file
@@ -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 <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(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<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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user