mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-05-18 20:08:19 +00:00
feat: website widget + omnichannel lead webhooks
Widget (embeddable): - Preact + Vite library mode → 35KB IIFE bundle served from sidecar - Shadow DOM for CSS isolation, themed from sidecar theme API - AI chatbot (streaming), appointment booking (4-step wizard), lead capture form - FontAwesome Pro duotone SVGs bundled as inline strings - HMAC-signed site keys (Redis storage, origin validation) - Captcha guard (Cloudflare Turnstile ready) Sidecar endpoints: - GET/PUT/DELETE /api/widget/keys/* — site key management - GET /api/widget/init — theme + config (key-gated) - GET /api/widget/doctors, /slots — doctor list + availability - POST /api/widget/book — appointment booking (captcha-gated) - POST /api/widget/lead — lead capture (captcha-gated) Omnichannel webhooks: - POST /api/webhook/facebook — Meta Lead Ads (verification + lead ingestion) - POST /api/webhook/google — Google Ads lead form extension - POST /api/webhook/whatsapp — Ozonetel WhatsApp callback (receiver ready) - POST /api/webhook/sms — Ozonetel SMS callback (receiver ready) Infrastructure: - SessionService.setCachePersistent() for non-expiring Redis keys - Static file serving from /public (widget.js) - WidgetModule registered in AppModule Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
74
src/widget/widget.controller.ts
Normal file
74
src/widget/widget.controller.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { Controller, Get, Post, Delete, Body, Query, Param, UseGuards, Logger, HttpException } from '@nestjs/common';
|
||||
import { WidgetService } from './widget.service';
|
||||
import { WidgetKeysService } from './widget-keys.service';
|
||||
import { WidgetKeyGuard } from './widget-key.guard';
|
||||
import { CaptchaGuard } from './captcha.guard';
|
||||
import type { WidgetBookRequest, WidgetLeadRequest } from './widget.types';
|
||||
|
||||
@Controller('api/widget')
|
||||
export class WidgetController {
|
||||
private readonly logger = new Logger(WidgetController.name);
|
||||
|
||||
constructor(
|
||||
private readonly widget: WidgetService,
|
||||
private readonly keys: WidgetKeysService,
|
||||
) {}
|
||||
|
||||
@Get('init')
|
||||
@UseGuards(WidgetKeyGuard)
|
||||
init() {
|
||||
return this.widget.getInitData();
|
||||
}
|
||||
|
||||
@Get('doctors')
|
||||
@UseGuards(WidgetKeyGuard)
|
||||
async doctors() {
|
||||
return this.widget.getDoctors();
|
||||
}
|
||||
|
||||
@Get('slots')
|
||||
@UseGuards(WidgetKeyGuard)
|
||||
async slots(@Query('doctorId') doctorId: string, @Query('date') date: string) {
|
||||
if (!doctorId || !date) throw new HttpException('doctorId and date required', 400);
|
||||
return this.widget.getSlots(doctorId, date);
|
||||
}
|
||||
|
||||
@Post('book')
|
||||
@UseGuards(WidgetKeyGuard, CaptchaGuard)
|
||||
async book(@Body() body: WidgetBookRequest) {
|
||||
if (!body.patientName || !body.patientPhone || !body.doctorId || !body.scheduledAt) {
|
||||
throw new HttpException('patientName, patientPhone, doctorId, and scheduledAt required', 400);
|
||||
}
|
||||
return this.widget.bookAppointment(body);
|
||||
}
|
||||
|
||||
@Post('lead')
|
||||
@UseGuards(WidgetKeyGuard, CaptchaGuard)
|
||||
async lead(@Body() body: WidgetLeadRequest) {
|
||||
if (!body.name || !body.phone) {
|
||||
throw new HttpException('name and phone required', 400);
|
||||
}
|
||||
return this.widget.createLead(body);
|
||||
}
|
||||
|
||||
// Key management (admin endpoints)
|
||||
@Post('keys/generate')
|
||||
async generateKey(@Body() body: { hospitalName: string; allowedOrigins: string[] }) {
|
||||
if (!body.hospitalName) throw new HttpException('hospitalName required', 400);
|
||||
const { key, siteKey } = this.keys.generateKey(body.hospitalName, body.allowedOrigins ?? []);
|
||||
await this.keys.saveKey(siteKey);
|
||||
return { key, siteKey };
|
||||
}
|
||||
|
||||
@Get('keys')
|
||||
async listKeys() {
|
||||
return this.keys.listKeys();
|
||||
}
|
||||
|
||||
@Delete('keys/:siteId')
|
||||
async revokeKey(@Param('siteId') siteId: string) {
|
||||
const revoked = await this.keys.revokeKey(siteId);
|
||||
if (!revoked) throw new HttpException('Key not found', 404);
|
||||
return { status: 'revoked' };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user