diff --git a/src/business-admin/business-admin.service.ts b/src/business-admin/business-admin.service.ts index fc594c2..032ebca 100644 --- a/src/business-admin/business-admin.service.ts +++ b/src/business-admin/business-admin.service.ts @@ -55,6 +55,8 @@ type BusinessRow = { domainId: bigint | null; domain: string | null; sslEnabled: boolean | null; + gitRepoUrl: string | null; + deploySlug: string | null; ownerUserId: bigint | null; ownerName: string | null; ownerNameEn: string | null; @@ -134,6 +136,8 @@ export class BusinessAdminService { dom.id AS "domainId", dom.host AS "domain", dom.ssl_enabled AS "sslEnabled", + dom.git_repo_url AS "gitRepoUrl", + dom.deploy_slug AS "deploySlug", own."ownerUserId" AS "ownerUserId", own."ownerName" AS "ownerName", own."ownerNameEn" AS "ownerNameEn", @@ -144,7 +148,7 @@ export class BusinessAdminService { b.settings->'modules'->'charts' AS "homeCharts" FROM businesses b LEFT JOIN LATERAL ( - SELECT d.id, d.host, d.ssl_enabled + SELECT d.id, d.host, d.ssl_enabled, d.git_repo_url, d.deploy_slug FROM domains d WHERE d.business_id = b.id ORDER BY d.is_primary DESC, d.created_at DESC @@ -548,12 +552,19 @@ export class BusinessAdminService { const businessId = BigInt(businessIdRaw); const domainId = BigInt(domainIdRaw); - const host = dto.host.trim(); + const host = dto.host.trim().toLowerCase(); + const gitRepoUrl = dto.gitRepoUrl?.trim() || null; if (!host) { throw new BadRequestException('host is required'); } + if (gitRepoUrl && !isValidGitRepoUrl(gitRepoUrl)) { + throw new BadRequestException( + 'gitRepoUrl must be an SSH git URL (e.g. git@git.meshkee.com:Meshkee-Websites/oaktasty.git)', + ); + } + const domain = await this.prisma.domain.findFirst({ where: { id: domainId, businessId }, }); @@ -567,10 +578,55 @@ export class BusinessAdminService { throw new ConflictException('Domain host is already taken'); } - return this.prisma.domain.update({ + let deploySlug = domain.deploySlug; + let provisionError: string | null = null; + let nextGitRepoUrl = domain.gitRepoUrl; + + if (gitRepoUrl) { + const slug = domain.deploySlug?.trim() || deploySlugFromHost(host); + if (!slug) { + throw new BadRequestException('Could not derive deploy slug from host'); + } + + const slugTaken = await this.prisma.domain.findFirst({ + where: { deploySlug: slug, NOT: { id: domainId } }, + select: { id: true }, + }); + if (slugTaken) { + throw new ConflictException(`Deploy slug "${slug}" is already in use`); + } + + try { + await this.websiteDeployAgent.provision({ + slug, + host, + gitRepoUrl, + }); + deploySlug = slug; + nextGitRepoUrl = gitRepoUrl; + } catch (err) { + if (err && typeof err === 'object' && 'message' in err && typeof err.message === 'string') { + provisionError = err.message; + } else { + provisionError = 'Storefront provision failed on websites server'; + } + } + } + + const updated = await this.prisma.domain.update({ where: { id: domainId }, - data: { host }, + data: { + host, + ...(gitRepoUrl && !provisionError + ? { deploySlug, gitRepoUrl: nextGitRepoUrl } + : {}), + }, }); + + return { + ...updated, + provisionError, + }; } async disable(businessIdRaw: string, dto: DisableBusinessDto, actor: AuthUser) { diff --git a/src/business-admin/dto/update-domain.dto.ts b/src/business-admin/dto/update-domain.dto.ts index 71515bd..e515b92 100644 --- a/src/business-admin/dto/update-domain.dto.ts +++ b/src/business-admin/dto/update-domain.dto.ts @@ -1,7 +1,16 @@ -import { IsString, MinLength } from 'class-validator'; +import { IsOptional, IsString, Matches, MinLength } from 'class-validator'; export class UpdateDomainDto { @IsString() @MinLength(1) host!: string; + + /** SSH git URL — when set, provisions storefront deploy on the websites VM. */ + @IsOptional() + @IsString() + @Matches(/^(?:git@[\w.-]+:[\w./-]+\.git|ssh:\/\/git@[\w.-]+(?::\d+)?\/[\w./-]+\.git)$/i, { + message: + 'gitRepoUrl must be an SSH git URL (e.g. git@git.meshkee.com:Meshkee-Websites/oaktasty.git)', + }) + gitRepoUrl?: string; }