mirror of
https://git.meshkee.com/Meshkee/backend.git
synced 2026-08-12 06:40:58 +04:30
Add website API docs, SSL api-hosts, and git-only deploy workflow.
Serve public storefront docs at /docs/website, expose api.{domain} hosts for API SSL sync, and require push-then-pull deploys instead of rsync.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
bb59d5e9ba
commit
016cc15bf0
@@ -1,4 +1,15 @@
|
||||
import { Body, Controller, Delete, Get, Param, Patch, Query, UseGuards } from '@nestjs/common';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpCode,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { AuthUser } from '../auth/auth.types';
|
||||
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
@@ -18,6 +29,13 @@ export class DomainAdminController {
|
||||
return this.service.list(query, user);
|
||||
}
|
||||
|
||||
@Post(':domainId/deploy')
|
||||
@HttpCode(202)
|
||||
@UseGuards(JwtAuthGuard)
|
||||
deploy(@Param('domainId') domainId: string, @CurrentUser() user: AuthUser) {
|
||||
return this.service.deploy(domainId, user);
|
||||
}
|
||||
|
||||
@Patch(':domainId')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
update(
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { DomainAdminController } from './domain-admin.controller';
|
||||
import { DomainAdminService } from './domain-admin.service';
|
||||
|
||||
@Module({
|
||||
imports: [AuthModule],
|
||||
imports: [AuthModule, ConfigModule],
|
||||
controllers: [DomainAdminController],
|
||||
providers: [DomainAdminService],
|
||||
})
|
||||
|
||||
@@ -4,7 +4,9 @@ import {
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
ServiceUnavailableException,
|
||||
} from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { AuthUser } from '../auth/auth.types';
|
||||
import { PermissionsService } from '../auth/permissions.service';
|
||||
@@ -14,6 +16,11 @@ import { ListDomainsDto } from './dto/list-domains.dto';
|
||||
import { ToggleSslDto } from './dto/toggle-ssl.dto';
|
||||
import { UpdateDomainAdminDto } from './dto/update-domain-admin.dto';
|
||||
|
||||
/** Apex hosts that have a storefront deploy on the websites VM. */
|
||||
const WEBSITE_DEPLOY_SLUGS: Record<string, string> = {
|
||||
'ali-mohammadi.ir': 'ali-mohammadi',
|
||||
};
|
||||
|
||||
type DomainRow = {
|
||||
id: bigint;
|
||||
host: string;
|
||||
@@ -23,6 +30,8 @@ type DomainRow = {
|
||||
isActive: boolean;
|
||||
expiresAt: Date | null;
|
||||
createdAt: Date;
|
||||
lastDeployedAt: Date | null;
|
||||
lastDeployStatus: string | null;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -30,6 +39,7 @@ export class DomainAdminService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly permissions: PermissionsService,
|
||||
private readonly config: ConfigService,
|
||||
) {}
|
||||
|
||||
private async assertSuperAdmin(actor: AuthUser) {
|
||||
@@ -38,6 +48,10 @@ export class DomainAdminService {
|
||||
}
|
||||
}
|
||||
|
||||
private deploySlugForHost(host: string): string | null {
|
||||
return WEBSITE_DEPLOY_SLUGS[host.trim().toLowerCase()] ?? null;
|
||||
}
|
||||
|
||||
async list(query: ListDomainsDto, actor: AuthUser) {
|
||||
await this.assertSuperAdmin(actor);
|
||||
|
||||
@@ -51,7 +65,7 @@ export class DomainAdminService {
|
||||
${nameLike ? Prisma.sql`AND d.host ILIKE ${nameLike}` : Prisma.empty}
|
||||
`;
|
||||
|
||||
const [items, totalRow] = await Promise.all([
|
||||
const [rows, totalRow] = await Promise.all([
|
||||
this.prisma.$queryRaw<DomainRow[]>(Prisma.sql`
|
||||
SELECT
|
||||
d.id AS "id",
|
||||
@@ -61,7 +75,9 @@ export class DomainAdminService {
|
||||
d.ssl_enabled AS "sslEnabled",
|
||||
d.is_active AS "isActive",
|
||||
d.expires_at AS "expiresAt",
|
||||
d.created_at AS "createdAt"
|
||||
d.created_at AS "createdAt",
|
||||
d.last_deployed_at AS "lastDeployedAt",
|
||||
d.last_deploy_status AS "lastDeployStatus"
|
||||
FROM domains d
|
||||
JOIN businesses b ON b.id = d.business_id
|
||||
${where}
|
||||
@@ -75,9 +91,80 @@ export class DomainAdminService {
|
||||
`),
|
||||
]);
|
||||
|
||||
const items = rows.map((row) => ({
|
||||
...row,
|
||||
deploySlug: this.deploySlugForHost(row.host),
|
||||
}));
|
||||
|
||||
return { items, total: totalRow[0]?.total ?? 0, page, pageSize };
|
||||
}
|
||||
|
||||
async deploy(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 = this.deploySlugForHost(domain.host);
|
||||
if (!slug) {
|
||||
throw new BadRequestException('This domain has no storefront deploy configured');
|
||||
}
|
||||
|
||||
const agentUrl = this.config.get<string>('WEBSITE_DEPLOY_AGENT_URL')?.trim();
|
||||
const token = this.config.get<string>('WEBSITE_DEPLOY_TOKEN')?.trim();
|
||||
if (!agentUrl || !token) {
|
||||
throw new ServiceUnavailableException('Website deploy agent is not configured');
|
||||
}
|
||||
|
||||
const markDeploy = async (status: 'started' | 'failed') => {
|
||||
const updated = await this.prisma.domain.update({
|
||||
where: { id: domainId },
|
||||
data: {
|
||||
lastDeployedAt: new Date(),
|
||||
lastDeployStatus: status,
|
||||
},
|
||||
});
|
||||
return updated;
|
||||
};
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(agentUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Deploy-Token': token,
|
||||
},
|
||||
body: JSON.stringify({ slug }),
|
||||
});
|
||||
} catch {
|
||||
await markDeploy('failed');
|
||||
throw new ServiceUnavailableException('Could not reach website deploy agent');
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
await markDeploy('failed');
|
||||
const text = await response.text().catch(() => '');
|
||||
throw new ServiceUnavailableException(
|
||||
`Deploy agent rejected request (${response.status})${text ? `: ${text}` : ''}`,
|
||||
);
|
||||
}
|
||||
|
||||
const updated = await markDeploy('started');
|
||||
|
||||
return {
|
||||
status: 'accepted' as const,
|
||||
slug,
|
||||
host: domain.host,
|
||||
message: 'Deploy started on websites server',
|
||||
lastDeployedAt: updated.lastDeployedAt?.toISOString() ?? null,
|
||||
lastDeployStatus: updated.lastDeployStatus,
|
||||
};
|
||||
}
|
||||
|
||||
async update(domainIdRaw: string, dto: UpdateDomainAdminDto, actor: AuthUser) {
|
||||
await this.assertSuperAdmin(actor);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user