mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-04-12 02:18:18 +00:00
Merge branch 'dev-main' into dev-kartik
This commit is contained in:
@@ -1,7 +1,18 @@
|
||||
import { Controller, Post, Body, Logger, HttpException } from '@nestjs/common';
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Body,
|
||||
Headers,
|
||||
Req,
|
||||
Logger,
|
||||
HttpException,
|
||||
} from '@nestjs/common';
|
||||
import type { Request } from 'express';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import axios from 'axios';
|
||||
import { OzonetelAgentService } from '../ozonetel/ozonetel-agent.service';
|
||||
import { SessionService } from './session.service';
|
||||
import { AgentConfigService } from './agent-config.service';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
@@ -13,6 +24,8 @@ export class AuthController {
|
||||
constructor(
|
||||
private config: ConfigService,
|
||||
private ozonetelAgent: OzonetelAgentService,
|
||||
private sessionService: SessionService,
|
||||
private agentConfigService: AgentConfigService,
|
||||
) {
|
||||
this.graphqlUrl = config.get<string>('platform.graphqlUrl')!;
|
||||
this.workspaceSubdomain =
|
||||
@@ -22,7 +35,10 @@ export class AuthController {
|
||||
}
|
||||
|
||||
@Post('login')
|
||||
async login(@Body() body: { email: string; password: string }) {
|
||||
async login(
|
||||
@Body() body: { email: string; password: string },
|
||||
@Req() req: Request,
|
||||
) {
|
||||
this.logger.log(`Login attempt for ${body.email}`);
|
||||
|
||||
try {
|
||||
@@ -128,18 +144,63 @@ export class AuthController {
|
||||
`User ${body.email} logged in with role: ${appRole} (platform roles: ${roleLabels.join(', ')})`,
|
||||
);
|
||||
|
||||
// Auto-login Ozonetel agent for CC agents (fire and forget)
|
||||
// Multi-agent: resolve agent config + session lock for CC agents
|
||||
let agentConfigResponse: any = undefined;
|
||||
|
||||
if (appRole === 'cc-agent') {
|
||||
const ozAgentId = process.env.OZONETEL_AGENT_ID ?? 'agent3';
|
||||
const memberId = workspaceMember?.id;
|
||||
if (!memberId)
|
||||
throw new HttpException('Workspace member not found', 400);
|
||||
|
||||
const agentConfig =
|
||||
await this.agentConfigService.getByMemberId(memberId);
|
||||
if (!agentConfig) {
|
||||
throw new HttpException(
|
||||
'Agent account not configured. Contact administrator.',
|
||||
403,
|
||||
);
|
||||
}
|
||||
|
||||
// Check for duplicate login — strict: one device only
|
||||
const clientIp =
|
||||
(req.headers['x-forwarded-for'] as string)?.split(',')[0]?.trim() ??
|
||||
req.ip ??
|
||||
'unknown';
|
||||
const existingSession = await this.sessionService.getSession(
|
||||
agentConfig.ozonetelAgentId,
|
||||
);
|
||||
if (existingSession) {
|
||||
this.logger.warn(
|
||||
`Duplicate login blocked for ${body.email} — session held by IP ${existingSession.ip} since ${existingSession.lockedAt}`,
|
||||
);
|
||||
throw new HttpException(
|
||||
`You are already logged in from another device (${existingSession.ip}). Please log out there first.`,
|
||||
409,
|
||||
);
|
||||
}
|
||||
|
||||
// Lock session in Redis with IP
|
||||
await this.sessionService.lockSession(
|
||||
agentConfig.ozonetelAgentId,
|
||||
memberId,
|
||||
clientIp,
|
||||
);
|
||||
|
||||
// Force-refresh Ozonetel API token on login
|
||||
this.ozonetelAgent.refreshToken().catch((err) => {
|
||||
this.logger.warn(
|
||||
`Ozonetel token refresh on login failed: ${err.message}`,
|
||||
);
|
||||
});
|
||||
|
||||
// Login to Ozonetel with agent-specific credentials
|
||||
const ozAgentPassword =
|
||||
process.env.OZONETEL_AGENT_PASSWORD ?? 'Test123$';
|
||||
const ozSipId = process.env.OZONETEL_SIP_ID ?? '521814';
|
||||
|
||||
this.ozonetelAgent
|
||||
.loginAgent({
|
||||
agentId: ozAgentId,
|
||||
agentId: agentConfig.ozonetelAgentId,
|
||||
password: ozAgentPassword,
|
||||
phoneNumber: ozSipId,
|
||||
phoneNumber: agentConfig.sipExtension,
|
||||
mode: 'blended',
|
||||
})
|
||||
.catch((err) => {
|
||||
@@ -147,6 +208,19 @@ export class AuthController {
|
||||
`Ozonetel agent login failed (non-blocking): ${err.message}`,
|
||||
);
|
||||
});
|
||||
|
||||
agentConfigResponse = {
|
||||
ozonetelAgentId: agentConfig.ozonetelAgentId,
|
||||
sipExtension: agentConfig.sipExtension,
|
||||
sipPassword: agentConfig.sipPassword,
|
||||
sipUri: agentConfig.sipUri,
|
||||
sipWsServer: agentConfig.sipWsServer,
|
||||
campaignName: agentConfig.campaignName,
|
||||
};
|
||||
|
||||
this.logger.log(
|
||||
`CC agent ${body.email} → Ozonetel ${agentConfig.ozonetelAgentId} / SIP ${agentConfig.sipExtension}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -161,6 +235,7 @@ export class AuthController {
|
||||
role: appRole,
|
||||
platformRoles: roleLabels,
|
||||
},
|
||||
...(agentConfigResponse ? { agentConfig: agentConfigResponse } : {}),
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException) throw error;
|
||||
@@ -214,4 +289,76 @@ export class AuthController {
|
||||
throw new HttpException('Token refresh failed', 401);
|
||||
}
|
||||
}
|
||||
|
||||
@Post('logout')
|
||||
async logout(@Headers('authorization') auth: string) {
|
||||
if (!auth) return { status: 'ok' };
|
||||
|
||||
try {
|
||||
const profileRes = await axios.post(
|
||||
this.graphqlUrl,
|
||||
{
|
||||
query: '{ currentUser { workspaceMember { id } } }',
|
||||
},
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json', Authorization: auth },
|
||||
},
|
||||
);
|
||||
|
||||
const memberId = profileRes.data?.data?.currentUser?.workspaceMember?.id;
|
||||
if (!memberId) return { status: 'ok' };
|
||||
|
||||
const agentConfig = this.agentConfigService.getFromCache(memberId);
|
||||
if (agentConfig) {
|
||||
await this.sessionService.unlockSession(agentConfig.ozonetelAgentId);
|
||||
this.logger.log(`Session unlocked for ${agentConfig.ozonetelAgentId}`);
|
||||
|
||||
this.ozonetelAgent
|
||||
.logoutAgent({
|
||||
agentId: agentConfig.ozonetelAgentId,
|
||||
password: process.env.OZONETEL_AGENT_PASSWORD ?? 'Test123$',
|
||||
})
|
||||
.catch((err) =>
|
||||
this.logger.warn(`Ozonetel logout failed: ${err.message}`),
|
||||
);
|
||||
|
||||
this.agentConfigService.clearCache(memberId);
|
||||
}
|
||||
|
||||
return { status: 'ok' };
|
||||
} catch (err) {
|
||||
this.logger.warn(`Logout cleanup failed: ${err}`);
|
||||
return { status: 'ok' };
|
||||
}
|
||||
}
|
||||
|
||||
@Post('heartbeat')
|
||||
async heartbeat(@Headers('authorization') auth: string) {
|
||||
if (!auth) return { status: 'ok' };
|
||||
|
||||
try {
|
||||
const profileRes = await axios.post(
|
||||
this.graphqlUrl,
|
||||
{
|
||||
query: '{ currentUser { workspaceMember { id } } }',
|
||||
},
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json', Authorization: auth },
|
||||
},
|
||||
);
|
||||
|
||||
const memberId = profileRes.data?.data?.currentUser?.workspaceMember?.id;
|
||||
const agentConfig = memberId
|
||||
? this.agentConfigService.getFromCache(memberId)
|
||||
: null;
|
||||
|
||||
if (agentConfig) {
|
||||
await this.sessionService.refreshSession(agentConfig.ozonetelAgentId);
|
||||
}
|
||||
|
||||
return { status: 'ok' };
|
||||
} catch {
|
||||
return { status: 'ok' };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user