diff --git a/src/domain-admin/domain-admin.service.ts b/src/domain-admin/domain-admin.service.ts index cdea774..02ca9f7 100644 --- a/src/domain-admin/domain-admin.service.ts +++ b/src/domain-admin/domain-admin.service.ts @@ -19,6 +19,11 @@ import { } from '../common/tls-probe'; import { PrismaService } from '../prisma/prisma.service'; import { WebsiteDeployAgentService } from '../website-deploy/website-deploy-agent.service'; +import { + deploySlugFromHost, + isValidGitRepoUrl, + normalizeGitRepoUrlForClone, +} from '../website-deploy/website-deploy.util'; import { DisableDomainDto } from './dto/disable-domain.dto'; import { ListDomainsDto } from './dto/list-domains.dto'; import { ToggleSslDto } from './dto/toggle-ssl.dto'; @@ -37,6 +42,7 @@ type DomainRow = { lastDeployedAt: Date | null; lastDeployStatus: string | null; deploySlug: string | null; + gitRepoUrl: string | null; }; @Injectable() @@ -82,7 +88,8 @@ export class DomainAdminService { d.created_at AS "createdAt", d.last_deployed_at AS "lastDeployedAt", d.last_deploy_status AS "lastDeployStatus", - d.deploy_slug AS "deploySlug" + d.deploy_slug AS "deploySlug", + d.git_repo_url AS "gitRepoUrl" FROM domains d JOIN businesses b ON b.id = d.business_id ${where} @@ -552,27 +559,90 @@ export class DomainAdminService { throw new NotFoundException('Domain not found'); } + const host = (dto.host?.trim() || domain.host).toLowerCase(); + const gitRepoUrl = dto.gitRepoUrl?.trim() || null; + if (dto.host) { - const host = dto.host.trim(); const existing = await this.prisma.domain.findUnique({ where: { host } }); if (existing && existing.id !== domainId) { throw new ConflictException('Domain host is already taken'); } } + if (gitRepoUrl && !isValidGitRepoUrl(gitRepoUrl)) { + throw new BadRequestException( + 'gitRepoUrl must be a git URL (e.g. https://git.meshkee.com/Meshkee-Websites/oaktasty.git)', + ); + } + + let deploySlug = domain.deploySlug; + let provisionError: string | null = null; + let nextGitRepoUrl = domain.gitRepoUrl; + const hostChanged = domain.host !== host; + const effectiveGitRepoUrl = gitRepoUrl || domain.gitRepoUrl?.trim() || null; + + const alreadyWired = + !!effectiveGitRepoUrl && + !!domain.deploySlug?.trim() && + (!gitRepoUrl || domain.gitRepoUrl?.trim() === gitRepoUrl) && + !hostChanged; + + if (effectiveGitRepoUrl && !alreadyWired) { + 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: normalizeGitRepoUrlForClone(effectiveGitRepoUrl), + }); + deploySlug = slug; + nextGitRepoUrl = effectiveGitRepoUrl; + } 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'; + } + } + } else if (alreadyWired) { + deploySlug = domain.deploySlug; + nextGitRepoUrl = effectiveGitRepoUrl; + } + const updated = await this.prisma.domain.update({ where: { id: domainId }, data: { - host: dto.host?.trim(), + host, expiresAt: dto.expiresAt !== undefined ? new Date(dto.expiresAt) : undefined, + ...(gitRepoUrl && !provisionError + ? { deploySlug, gitRepoUrl: nextGitRepoUrl } + : {}), }, }); - const host = updated.host; + // Same Arvan helper as business-admin domain add/edit (`ensureTenantRecords`). const dnsError = dto.updateDns ? await this.applyArvanDns(host) : null; return { ...updated, + provisionError, dnsError, }; } diff --git a/src/domain-admin/dto/update-domain-admin.dto.ts b/src/domain-admin/dto/update-domain-admin.dto.ts index a3dc6cc..90299a0 100644 --- a/src/domain-admin/dto/update-domain-admin.dto.ts +++ b/src/domain-admin/dto/update-domain-admin.dto.ts @@ -1,4 +1,5 @@ -import { IsBoolean, IsDateString, IsOptional, IsString, MinLength } from 'class-validator'; +import { IsBoolean, IsDateString, IsOptional, IsString, Matches, MinLength } from 'class-validator'; +import { GIT_REPO_URL_RE } from '../../website-deploy/website-deploy.util'; export class UpdateDomainAdminDto { @IsOptional() @@ -14,4 +15,13 @@ export class UpdateDomainAdminDto { @IsOptional() @IsBoolean() updateDns?: boolean; + + /** HTTPS or SSH git URL — when set, provisions storefront deploy on the websites VM. */ + @IsOptional() + @IsString() + @Matches(GIT_REPO_URL_RE, { + message: + 'gitRepoUrl must be a git URL (e.g. https://git.meshkee.com/Meshkee-Websites/oaktasty.git)', + }) + gitRepoUrl?: string; }