mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-04-11 18:08:16 +00:00
feat: wire sidecar to platform — auth proxy with workspace subdomain, GraphQL proxy, health check
This commit is contained in:
@@ -6,6 +6,9 @@ import { AuthModule } from './auth/auth.module';
|
|||||||
import { PlatformModule } from './platform/platform.module';
|
import { PlatformModule } from './platform/platform.module';
|
||||||
import { ExotelModule } from './exotel/exotel.module';
|
import { ExotelModule } from './exotel/exotel.module';
|
||||||
import { CallEventsModule } from './call-events/call-events.module';
|
import { CallEventsModule } from './call-events/call-events.module';
|
||||||
|
import { OzonetelAgentModule } from './ozonetel/ozonetel-agent.module';
|
||||||
|
import { GraphqlProxyModule } from './graphql-proxy/graphql-proxy.module';
|
||||||
|
import { HealthModule } from './health/health.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -18,6 +21,9 @@ import { CallEventsModule } from './call-events/call-events.module';
|
|||||||
PlatformModule,
|
PlatformModule,
|
||||||
ExotelModule,
|
ExotelModule,
|
||||||
CallEventsModule,
|
CallEventsModule,
|
||||||
|
OzonetelAgentModule,
|
||||||
|
GraphqlProxyModule,
|
||||||
|
HealthModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@@ -6,9 +6,13 @@ import axios from 'axios';
|
|||||||
export class AuthController {
|
export class AuthController {
|
||||||
private readonly logger = new Logger(AuthController.name);
|
private readonly logger = new Logger(AuthController.name);
|
||||||
private readonly graphqlUrl: string;
|
private readonly graphqlUrl: string;
|
||||||
|
private readonly workspaceSubdomain: string;
|
||||||
|
private readonly origin: string;
|
||||||
|
|
||||||
constructor(private config: ConfigService) {
|
constructor(private config: ConfigService) {
|
||||||
this.graphqlUrl = config.get<string>('platform.graphqlUrl')!;
|
this.graphqlUrl = config.get<string>('platform.graphqlUrl')!;
|
||||||
|
this.workspaceSubdomain = process.env.PLATFORM_WORKSPACE_SUBDOMAIN ?? 'fortytwo-dev';
|
||||||
|
this.origin = process.env.PLATFORM_ORIGIN ?? 'http://fortytwo-dev.localhost:4010';
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('login')
|
@Post('login')
|
||||||
@@ -16,46 +20,40 @@ export class AuthController {
|
|||||||
this.logger.log(`Login attempt for ${body.email}`);
|
this.logger.log(`Login attempt for ${body.email}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(this.graphqlUrl, {
|
// Step 1: Get login token
|
||||||
|
const loginRes = await axios.post(this.graphqlUrl, {
|
||||||
query: `mutation GetLoginToken($email: String!, $password: String!) {
|
query: `mutation GetLoginToken($email: String!, $password: String!) {
|
||||||
getLoginTokenFromCredentials(
|
getLoginTokenFromCredentials(
|
||||||
email: $email
|
email: $email
|
||||||
password: $password
|
password: $password
|
||||||
origin: "http://localhost:5173"
|
origin: "${this.origin}"
|
||||||
) {
|
) {
|
||||||
loginToken { token }
|
loginToken { token }
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
variables: { email: body.email, password: body.password },
|
variables: { email: body.email, password: body.password },
|
||||||
}, {
|
}, {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Workspace-Subdomain': this.workspaceSubdomain,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.data.errors) {
|
if (loginRes.data.errors) {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
response.data.errors[0]?.message ?? 'Login failed',
|
loginRes.data.errors[0]?.message ?? 'Login failed',
|
||||||
401,
|
401,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.data.data.getLoginTokenFromCredentials;
|
const loginToken = loginRes.data.data.getLoginTokenFromCredentials.loginToken.token;
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof HttpException) throw error;
|
|
||||||
this.logger.error(`Login proxy failed: ${error}`);
|
|
||||||
throw new HttpException('Authentication service unavailable', 503);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('tokens')
|
// Step 2: Exchange for access + refresh tokens
|
||||||
async getTokens(@Body() body: { loginToken: string }) {
|
const tokenRes = await axios.post(this.graphqlUrl, {
|
||||||
this.logger.log('Exchanging login token for access token');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await axios.post(this.graphqlUrl, {
|
|
||||||
query: `mutation GetAuthTokens($loginToken: String!) {
|
query: `mutation GetAuthTokens($loginToken: String!) {
|
||||||
getAuthTokensFromLoginToken(
|
getAuthTokensFromLoginToken(
|
||||||
loginToken: $loginToken
|
loginToken: $loginToken
|
||||||
origin: "http://localhost:5173"
|
origin: "${this.origin}"
|
||||||
) {
|
) {
|
||||||
tokens {
|
tokens {
|
||||||
accessOrWorkspaceAgnosticToken { token }
|
accessOrWorkspaceAgnosticToken { token }
|
||||||
@@ -63,22 +61,30 @@ export class AuthController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
variables: { loginToken: body.loginToken },
|
variables: { loginToken },
|
||||||
}, {
|
}, {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Workspace-Subdomain': this.workspaceSubdomain,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.data.errors) {
|
if (tokenRes.data.errors) {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
response.data.errors[0]?.message ?? 'Token exchange failed',
|
tokenRes.data.errors[0]?.message ?? 'Token exchange failed',
|
||||||
401,
|
401,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.data.data.getAuthTokensFromLoginToken;
|
const tokens = tokenRes.data.data.getAuthTokensFromLoginToken.tokens;
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken: tokens.accessOrWorkspaceAgnosticToken.token,
|
||||||
|
refreshToken: tokens.refreshToken.token,
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof HttpException) throw error;
|
if (error instanceof HttpException) throw error;
|
||||||
this.logger.error(`Token exchange proxy failed: ${error}`);
|
this.logger.error(`Login proxy failed: ${error}`);
|
||||||
throw new HttpException('Authentication service unavailable', 503);
|
throw new HttpException('Authentication service unavailable', 503);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
45
src/graphql-proxy/graphql-proxy.controller.ts
Normal file
45
src/graphql-proxy/graphql-proxy.controller.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { Controller, Post, Req, Res, Logger, HttpException } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import type { Request, Response } from 'express';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
@Controller('graphql')
|
||||||
|
export class GraphqlProxyController {
|
||||||
|
private readonly logger = new Logger(GraphqlProxyController.name);
|
||||||
|
private readonly graphqlUrl: string;
|
||||||
|
|
||||||
|
constructor(private config: ConfigService) {
|
||||||
|
this.graphqlUrl = config.get<string>('platform.graphqlUrl')!;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
async proxy(@Req() req: Request, @Res() res: Response) {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
|
||||||
|
if (!authHeader) {
|
||||||
|
throw new HttpException('Authorization header required', 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
this.graphqlUrl,
|
||||||
|
req.body,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': authHeader,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
res.status(response.status).json(response.data);
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.response) {
|
||||||
|
res.status(error.response.status).json(error.response.data);
|
||||||
|
} else {
|
||||||
|
this.logger.error(`GraphQL proxy error: ${error.message}`);
|
||||||
|
throw new HttpException('Platform unreachable', 503);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/graphql-proxy/graphql-proxy.module.ts
Normal file
7
src/graphql-proxy/graphql-proxy.module.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { GraphqlProxyController } from './graphql-proxy.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [GraphqlProxyController],
|
||||||
|
})
|
||||||
|
export class GraphqlProxyModule {}
|
||||||
38
src/health/health.controller.ts
Normal file
38
src/health/health.controller.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Controller, Get, Logger } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
@Controller('api/health')
|
||||||
|
export class HealthController {
|
||||||
|
private readonly logger = new Logger(HealthController.name);
|
||||||
|
private readonly graphqlUrl: string;
|
||||||
|
|
||||||
|
constructor(private config: ConfigService) {
|
||||||
|
this.graphqlUrl = config.get<string>('platform.graphqlUrl')!;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
async check() {
|
||||||
|
let platformReachable = false;
|
||||||
|
let platformLatency = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const start = Date.now();
|
||||||
|
await axios.post(this.graphqlUrl, { query: '{ __typename }' }, {
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
timeout: 5000,
|
||||||
|
});
|
||||||
|
platformLatency = Date.now() - start;
|
||||||
|
platformReachable = true;
|
||||||
|
} catch {
|
||||||
|
platformReachable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: platformReachable ? 'ok' : 'degraded',
|
||||||
|
platform: { reachable: platformReachable, latencyMs: platformLatency },
|
||||||
|
ozonetel: { configured: !!process.env.EXOTEL_API_KEY },
|
||||||
|
ai: { configured: !!process.env.ANTHROPIC_API_KEY },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/health/health.module.ts
Normal file
7
src/health/health.module.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { HealthController } from './health.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [HealthController],
|
||||||
|
})
|
||||||
|
export class HealthModule {}
|
||||||
43
src/ozonetel/ozonetel-agent.controller.ts
Normal file
43
src/ozonetel/ozonetel-agent.controller.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { Controller, Post, Body, Logger, HttpException } from '@nestjs/common';
|
||||||
|
import { OzonetelAgentService } from './ozonetel-agent.service';
|
||||||
|
|
||||||
|
@Controller('api/ozonetel')
|
||||||
|
export class OzonetelAgentController {
|
||||||
|
private readonly logger = new Logger(OzonetelAgentController.name);
|
||||||
|
|
||||||
|
constructor(private readonly ozonetelAgent: OzonetelAgentService) {}
|
||||||
|
|
||||||
|
@Post('agent-login')
|
||||||
|
async agentLogin(
|
||||||
|
@Body() body: { agentId: string; password: string; phoneNumber: string; mode?: string },
|
||||||
|
) {
|
||||||
|
this.logger.log(`Agent login request for ${body.agentId}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.ozonetelAgent.loginAgent(body);
|
||||||
|
return result;
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new HttpException(
|
||||||
|
error.response?.data?.message ?? 'Agent login failed',
|
||||||
|
error.response?.status ?? 500,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('agent-logout')
|
||||||
|
async agentLogout(
|
||||||
|
@Body() body: { agentId: string; password: string },
|
||||||
|
) {
|
||||||
|
this.logger.log(`Agent logout request for ${body.agentId}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.ozonetelAgent.logoutAgent(body);
|
||||||
|
return result;
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new HttpException(
|
||||||
|
error.response?.data?.message ?? 'Agent logout failed',
|
||||||
|
error.response?.status ?? 500,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/ozonetel/ozonetel-agent.module.ts
Normal file
10
src/ozonetel/ozonetel-agent.module.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { OzonetelAgentController } from './ozonetel-agent.controller';
|
||||||
|
import { OzonetelAgentService } from './ozonetel-agent.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [OzonetelAgentController],
|
||||||
|
providers: [OzonetelAgentService],
|
||||||
|
exports: [OzonetelAgentService],
|
||||||
|
})
|
||||||
|
export class OzonetelAgentModule {}
|
||||||
94
src/ozonetel/ozonetel-agent.service.ts
Normal file
94
src/ozonetel/ozonetel-agent.service.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class OzonetelAgentService {
|
||||||
|
private readonly logger = new Logger(OzonetelAgentService.name);
|
||||||
|
private readonly apiDomain: string;
|
||||||
|
private readonly apiKey: string;
|
||||||
|
private readonly accountId: string;
|
||||||
|
|
||||||
|
constructor(private config: ConfigService) {
|
||||||
|
this.apiDomain = config.get<string>('exotel.subdomain') ?? 'in1-ccaas-api.ozonetel.com';
|
||||||
|
this.apiKey = config.get<string>('exotel.apiKey') ?? '';
|
||||||
|
this.accountId = config.get<string>('exotel.accountSid') ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async loginAgent(params: {
|
||||||
|
agentId: string;
|
||||||
|
password: string;
|
||||||
|
phoneNumber: string;
|
||||||
|
mode?: string;
|
||||||
|
}): Promise<{ status: string; message: string }> {
|
||||||
|
const url = `https://${this.apiDomain}/CAServices/AgentAuthenticationV2/index.php`;
|
||||||
|
|
||||||
|
this.logger.log(`Logging in agent ${params.agentId} with phone ${params.phoneNumber}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
url,
|
||||||
|
new URLSearchParams({
|
||||||
|
userName: this.accountId,
|
||||||
|
apiKey: this.apiKey,
|
||||||
|
phoneNumber: params.phoneNumber,
|
||||||
|
action: 'login',
|
||||||
|
mode: params.mode ?? 'blended',
|
||||||
|
state: 'Ready',
|
||||||
|
}).toString(),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
username: params.agentId,
|
||||||
|
password: params.password,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
this.logger.log(`Agent login response: ${JSON.stringify(response.data)}`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Agent login failed: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async logoutAgent(params: {
|
||||||
|
agentId: string;
|
||||||
|
password: string;
|
||||||
|
}): Promise<{ status: string; message: string }> {
|
||||||
|
const url = `https://${this.apiDomain}/CAServices/AgentAuthenticationV2/index.php`;
|
||||||
|
|
||||||
|
this.logger.log(`Logging out agent ${params.agentId}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
url,
|
||||||
|
new URLSearchParams({
|
||||||
|
userName: this.accountId,
|
||||||
|
apiKey: this.apiKey,
|
||||||
|
action: 'logout',
|
||||||
|
mode: 'blended',
|
||||||
|
state: 'Ready',
|
||||||
|
}).toString(),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
username: params.agentId,
|
||||||
|
password: params.password,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
this.logger.log(`Agent logout response: ${JSON.stringify(response.data)}`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.error(`Agent logout failed: ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user