Brand OTP SMS with business name and support local SMS proxy.

Append tenant Farsi name to verification SMS, allow optional domain on send-otp, and add SMS_PROXY_* for local delivery via production. Also include websites SSL sync agent/API wiring.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-08 23:29:56 +03:30
co-authored by Cursor
parent 77f3cb2a67
commit 8f05ee2b58
14 changed files with 385 additions and 14 deletions
@@ -36,6 +36,14 @@ export class DomainAdminController {
return this.service.syncSsl(user);
}
/** Issue Let's Encrypt for storefront domains missing SSL (websites VM). */
@Post('website-ssl')
@HttpCode(200)
@UseGuards(JwtAuthGuard)
issueWebsiteSsl(@CurrentUser() user: AuthUser) {
return this.service.issueWebsiteSsl(user);
}
@Post(':domainId/deploy')
@HttpCode(202)
@UseGuards(JwtAuthGuard)
@@ -43,6 +51,13 @@ export class DomainAdminController {
return this.service.deploy(domainId, user);
}
@Post(':domainId/issue-ssl')
@HttpCode(200)
@UseGuards(JwtAuthGuard)
issueSsl(@Param('domainId') domainId: string, @CurrentUser() user: AuthUser) {
return this.service.issueSsl(domainId, user);
}
@Patch(':domainId')
@UseGuards(JwtAuthGuard)
update(
+113 -1
View File
@@ -10,6 +10,7 @@ import { ConfigService } from '@nestjs/config';
import { Prisma } from '@prisma/client';
import { AuthUser } from '../auth/auth.types';
import { PermissionsService } from '../auth/permissions.service';
import { probeTlsHost } from '../common/tls-probe';
import { PrismaService } from '../prisma/prisma.service';
import { WebsiteDeployAgentService } from '../website-deploy/website-deploy-agent.service';
import { DisableDomainDto } from './dto/disable-domain.dto';
@@ -125,7 +126,118 @@ export class DomainAdminService {
return {
status: 'accepted' as const,
message: 'SSL sync started on dashboards server',
message: 'Dashboard SSL sync started (manage / business.* / customer.*)',
};
}
/**
* Issue Let's Encrypt certs on the websites VM for storefront domains
* that have deploy_slug and currently report ssl_enabled=false.
*/
async issueWebsiteSsl(actor: AuthUser) {
await this.assertSuperAdmin(actor);
const targets = await this.prisma.domain.findMany({
where: {
isActive: true,
sslEnabled: false,
deploySlug: { not: null },
},
select: { id: true, host: true, deploySlug: true },
orderBy: { host: 'asc' },
});
if (targets.length === 0) {
return {
status: 'ok' as const,
message: 'No storefront domains need SSL',
issued: [] as string[],
failed: [] as Array<{ host: string; error: string }>,
};
}
const issued: string[] = [];
const failed: Array<{ host: string; error: string }> = [];
for (const domain of targets) {
try {
await this.websiteDeployAgent.issueSsl({
host: domain.host,
slug: domain.deploySlug,
});
const ok = await probeTlsHost(domain.host);
await this.prisma.domain.update({
where: { id: domain.id },
data: { sslEnabled: ok },
});
if (ok) {
issued.push(domain.host);
} else {
failed.push({
host: domain.host,
error: 'Certbot finished but TLS probe still failed',
});
}
} catch (err) {
failed.push({
host: domain.host,
error: err instanceof Error ? err.message : 'SSL issue failed',
});
}
}
const message =
failed.length === 0
? `Issued SSL for ${issued.length} website(s)`
: `Issued ${issued.length}, failed ${failed.length} website SSL`;
return { status: 'ok' as const, message, issued, failed };
}
async issueSsl(domainIdRaw: string, actor: AuthUser) {
await this.assertSuperAdmin(actor);
const domainId = BigInt(domainIdRaw);
const domain = await this.prisma.domain.findUnique({ where: { id: domainId } });
if (!domain) {
throw new NotFoundException('Domain not found');
}
const slug = domain.deploySlug?.trim() || null;
if (!slug) {
throw new BadRequestException(
'This domain has no storefront deploy configured — use Sync dashboard SSL for business/customer hosts',
);
}
try {
await this.websiteDeployAgent.issueSsl({ host: domain.host, slug });
} catch (err) {
if (err instanceof ServiceUnavailableException) {
throw err;
}
throw new ServiceUnavailableException(
err instanceof Error ? err.message : 'SSL issue failed',
);
}
const ok = await probeTlsHost(domain.host);
const updated = await this.prisma.domain.update({
where: { id: domainId },
data: { sslEnabled: ok },
});
if (!ok) {
throw new ServiceUnavailableException(
`Certbot ran for ${domain.host} but HTTPS probe failed — check DNS for apex and www`,
);
}
return {
status: 'issued' as const,
host: domain.host,
sslEnabled: updated.sslEnabled,
message: `SSL issued for ${domain.host}`,
};
}