Add partner SMS gateway via Gama SendQuick and document it.

Expose POST /public/sms/send with API key + domain allowlist for external backends like Balout, wire Meshkee OTP/message sends to Gama, and publish Partner SMS docs on /docs/website.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-04 11:09:21 +03:30
co-authored by Cursor
parent 267a218c26
commit f4295e780c
22 changed files with 803 additions and 16 deletions
+48
View File
@@ -0,0 +1,48 @@
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Request } from 'express';
import {
normalizePartnerDomain,
parseSmsPartners,
timingSafeEqualString,
} from './sms-partners.util';
export const SMS_PARTNER_DOMAIN_KEY = 'smsPartnerDomain';
@Injectable()
export class SmsPartnerGuard implements CanActivate {
constructor(private readonly config: ConfigService) {}
canActivate(context: ExecutionContext): boolean {
const partners = parseSmsPartners(this.config.get<string>('SMS_PARTNERS'));
if (partners.size === 0) {
throw new UnauthorizedException('SMS partner gateway is not configured');
}
const req = context.switchToHttp().getRequest<Request>();
const providedKey = String(req.headers['x-api-key'] ?? '').trim();
if (!providedKey) {
throw new UnauthorizedException('Missing X-Api-Key');
}
const body = (req.body ?? {}) as { domain?: unknown };
const domain = normalizePartnerDomain(String(body.domain ?? ''));
if (!domain) {
throw new UnauthorizedException('Missing or invalid domain');
}
const expectedKey = partners.get(domain);
if (!expectedKey || !timingSafeEqualString(providedKey, expectedKey)) {
throw new UnauthorizedException('Invalid API key or domain');
}
(req as Request & { [SMS_PARTNER_DOMAIN_KEY]?: string })[SMS_PARTNER_DOMAIN_KEY] =
domain;
return true;
}
}