Allow adding or editing a storefront git repo from the Websites domain editor.
Provision uses the same websites-VM agent as Businesses; DNS still goes through Arvan ensureTenantRecords. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
cda901d049
commit
5813e9fc53
@@ -19,6 +19,11 @@ import {
|
|||||||
} from '../common/tls-probe';
|
} from '../common/tls-probe';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { WebsiteDeployAgentService } from '../website-deploy/website-deploy-agent.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 { DisableDomainDto } from './dto/disable-domain.dto';
|
||||||
import { ListDomainsDto } from './dto/list-domains.dto';
|
import { ListDomainsDto } from './dto/list-domains.dto';
|
||||||
import { ToggleSslDto } from './dto/toggle-ssl.dto';
|
import { ToggleSslDto } from './dto/toggle-ssl.dto';
|
||||||
@@ -37,6 +42,7 @@ type DomainRow = {
|
|||||||
lastDeployedAt: Date | null;
|
lastDeployedAt: Date | null;
|
||||||
lastDeployStatus: string | null;
|
lastDeployStatus: string | null;
|
||||||
deploySlug: string | null;
|
deploySlug: string | null;
|
||||||
|
gitRepoUrl: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -82,7 +88,8 @@ export class DomainAdminService {
|
|||||||
d.created_at AS "createdAt",
|
d.created_at AS "createdAt",
|
||||||
d.last_deployed_at AS "lastDeployedAt",
|
d.last_deployed_at AS "lastDeployedAt",
|
||||||
d.last_deploy_status AS "lastDeployStatus",
|
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
|
FROM domains d
|
||||||
JOIN businesses b ON b.id = d.business_id
|
JOIN businesses b ON b.id = d.business_id
|
||||||
${where}
|
${where}
|
||||||
@@ -552,27 +559,90 @@ export class DomainAdminService {
|
|||||||
throw new NotFoundException('Domain not found');
|
throw new NotFoundException('Domain not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const host = (dto.host?.trim() || domain.host).toLowerCase();
|
||||||
|
const gitRepoUrl = dto.gitRepoUrl?.trim() || null;
|
||||||
|
|
||||||
if (dto.host) {
|
if (dto.host) {
|
||||||
const host = dto.host.trim();
|
|
||||||
const existing = await this.prisma.domain.findUnique({ where: { host } });
|
const existing = await this.prisma.domain.findUnique({ where: { host } });
|
||||||
if (existing && existing.id !== domainId) {
|
if (existing && existing.id !== domainId) {
|
||||||
throw new ConflictException('Domain host is already taken');
|
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({
|
const updated = await this.prisma.domain.update({
|
||||||
where: { id: domainId },
|
where: { id: domainId },
|
||||||
data: {
|
data: {
|
||||||
host: dto.host?.trim(),
|
host,
|
||||||
expiresAt: dto.expiresAt !== undefined ? new Date(dto.expiresAt) : undefined,
|
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;
|
const dnsError = dto.updateDns ? await this.applyArvanDns(host) : null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...updated,
|
...updated,
|
||||||
|
provisionError,
|
||||||
dnsError,
|
dnsError,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
export class UpdateDomainAdminDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@@ -14,4 +15,13 @@ export class UpdateDomainAdminDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
updateDns?: boolean;
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user