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:
45
src/widget/captcha.guard.ts
Normal file
45
src/widget/captcha.guard.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { CanActivate, ExecutionContext, Injectable, HttpException, Logger } from '@nestjs/common';
|
||||
|
||||
const RECAPTCHA_VERIFY_URL = 'https://www.google.com/recaptcha/api/siteverify';
|
||||
|
||||
@Injectable()
|
||||
export class CaptchaGuard implements CanActivate {
|
||||
private readonly logger = new Logger(CaptchaGuard.name);
|
||||
private readonly secretKey: string;
|
||||
|
||||
constructor() {
|
||||
this.secretKey = process.env.RECAPTCHA_SECRET_KEY ?? '';
|
||||
}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
if (!this.secretKey) {
|
||||
this.logger.warn('RECAPTCHA_SECRET_KEY not set — captcha disabled');
|
||||
return true;
|
||||
}
|
||||
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const token = request.body?.captchaToken;
|
||||
|
||||
if (!token) throw new HttpException('Captcha token required', 400);
|
||||
|
||||
try {
|
||||
const res = await fetch(RECAPTCHA_VERIFY_URL, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: `secret=${this.secretKey}&response=${token}`,
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.success || (data.score != null && data.score < 0.3)) {
|
||||
this.logger.warn(`Captcha failed: score=${data.score} success=${data.success}`);
|
||||
throw new HttpException('Captcha verification failed', 403);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
if (err instanceof HttpException) throw err;
|
||||
this.logger.error(`Captcha verification error: ${err.message}`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user