Fix SSL list badges, labeled AI prompt URLs, and product-category migrate.

Storefront SSL now follows apex expiry instead of requiring www, prompt links include domain and prompt name, and extra color variations merge instead of aborting migrate.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-30 14:47:10 +03:30
co-authored by Cursor
parent e37cb4a20d
commit a5f2f8d63b
6 changed files with 208 additions and 87 deletions
+18
View File
@@ -99,6 +99,24 @@ export class AiPromptsController {
return this.service.removeDomainPrompt(domainId, promptId, user); return this.service.removeDomainPrompt(domainId, promptId, user);
} }
@Get('public/ai-prompts/:host/:name/:prompt/:publicId')
@Header('Content-Type', 'text/plain; charset=utf-8')
getPublicFull(@Param('publicId') publicId: string) {
return this.service.getPublicText(publicId);
}
@Get('public/ai-prompts/:host/:name/:publicId')
@Header('Content-Type', 'text/plain; charset=utf-8')
getPublicLabeled(@Param('publicId') publicId: string) {
return this.service.getPublicText(publicId);
}
@Get('public/ai-prompts/:host/:publicId')
@Header('Content-Type', 'text/plain; charset=utf-8')
getPublicHost(@Param('publicId') publicId: string) {
return this.service.getPublicText(publicId);
}
@Get('public/ai-prompts/:publicId') @Get('public/ai-prompts/:publicId')
@Header('Content-Type', 'text/plain; charset=utf-8') @Header('Content-Type', 'text/plain; charset=utf-8')
getPublic(@Param('publicId') publicId: string) { getPublic(@Param('publicId') publicId: string) {
+78 -9
View File
@@ -40,11 +40,43 @@ export class AiPromptsService {
throw new BadRequestException('Unable to allocate a public prompt id'); throw new BadRequestException('Unable to allocate a public prompt id');
} }
private publicUrl(publicId: string): string { private slugSegment(value: string, fallback = 'site'): string {
const slug = value
.trim()
.toLowerCase()
.replace(/['"]/g, '')
.replace(/[^a-z0-9\u0600-\u06ff]+/gi, '-')
.replace(/-+/g, '-')
.replace(/^-+|-+$/g, '')
.slice(0, 80);
return slug || fallback;
}
private publicUrl(
publicId: string,
labels?: {
host?: string | null;
name?: string | null;
promptName?: string | null;
},
): string {
const base = const base =
this.config.get<string>('API_PUBLIC_BASE_URL')?.replace(/\/$/, '') || this.config.get<string>('API_PUBLIC_BASE_URL')?.replace(/\/$/, '') ||
'https://api.meshkee.com'; 'https://api.meshkee.com';
return `${base}/api/v1/public/ai-prompts/${publicId}`; const host = labels?.host?.trim().toLowerCase();
const nameSlug = labels?.name?.trim()
? this.slugSegment(labels.name)
: '';
const promptSlug = labels?.promptName?.trim()
? this.slugSegment(labels.promptName)
: '';
const parts = [
host ? encodeURIComponent(host) : '',
nameSlug ? encodeURIComponent(nameSlug) : '',
promptSlug ? encodeURIComponent(promptSlug) : '',
publicId,
].filter(Boolean);
return `${base}/api/v1/public/ai-prompts/${parts.join('/')}`;
} }
private serializeTemplate(row: { private serializeTemplate(row: {
@@ -63,7 +95,8 @@ export class AiPromptsService {
}; };
} }
private serializeDomainPrompt(row: { private serializeDomainPrompt(
row: {
id: bigint; id: bigint;
domainId: bigint; domainId: bigint;
sourceTemplateId: bigint | null; sourceTemplateId: bigint | null;
@@ -73,7 +106,12 @@ export class AiPromptsService {
sortOrder: number; sortOrder: number;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
}) { },
labels?: {
host?: string | null;
name?: string | null;
},
) {
return { return {
id: row.id.toString(), id: row.id.toString(),
domainId: row.domainId.toString(), domainId: row.domainId.toString(),
@@ -81,13 +119,29 @@ export class AiPromptsService {
name: row.name, name: row.name,
body: row.body, body: row.body,
publicId: row.publicId, publicId: row.publicId,
publicUrl: this.publicUrl(row.publicId), publicUrl: this.publicUrl(row.publicId, {
host: labels?.host,
name: labels?.name,
promptName: row.name,
}),
sortOrder: row.sortOrder, sortOrder: row.sortOrder,
createdAt: row.createdAt.toISOString(), createdAt: row.createdAt.toISOString(),
updatedAt: row.updatedAt.toISOString(), updatedAt: row.updatedAt.toISOString(),
}; };
} }
private async domainLabels(domainId: bigint) {
const domain = await this.prisma.domain.findUnique({
where: { id: domainId },
select: {
host: true,
business: { select: { name: true } },
},
});
if (!domain) return undefined;
return { host: domain.host, name: domain.business.name };
}
async list(actor: AuthUser) { async list(actor: AuthUser) {
await this.assertSuperAdmin(actor); await this.assertSuperAdmin(actor);
const items = await this.prisma.aiPromptTemplate.findMany({ const items = await this.prisma.aiPromptTemplate.findMany({
@@ -163,7 +217,12 @@ export class AiPromptsService {
domainId: domain.id.toString(), domainId: domain.id.toString(),
host: domain.host, host: domain.host,
businessName: domain.business.name, businessName: domain.business.name,
items: items.map((row) => this.serializeDomainPrompt(row)), items: items.map((row) =>
this.serializeDomainPrompt(row, {
host: domain.host,
name: domain.business.name,
}),
),
}; };
} }
@@ -176,7 +235,11 @@ export class AiPromptsService {
const domainId = BigInt(domainIdRaw); const domainId = BigInt(domainIdRaw);
const domain = await this.prisma.domain.findUnique({ const domain = await this.prisma.domain.findUnique({
where: { id: domainId }, where: { id: domainId },
select: { id: true }, select: {
id: true,
host: true,
business: { select: { name: true } },
},
}); });
if (!domain) throw new NotFoundException('Website not found'); if (!domain) throw new NotFoundException('Website not found');
@@ -205,7 +268,10 @@ export class AiPromptsService {
sortOrder: (maxSort._max.sortOrder ?? -1) + 1, sortOrder: (maxSort._max.sortOrder ?? -1) + 1,
}, },
}); });
return this.serializeDomainPrompt(created); return this.serializeDomainPrompt(created, {
host: domain.host,
name: domain.business.name,
});
} }
async updateDomainPrompt( async updateDomainPrompt(
@@ -230,7 +296,10 @@ export class AiPromptsService {
...(dto.body !== undefined ? { body: dto.body.trim() } : {}), ...(dto.body !== undefined ? { body: dto.body.trim() } : {}),
}, },
}); });
return this.serializeDomainPrompt(updated); return this.serializeDomainPrompt(
updated,
await this.domainLabels(domainId),
);
} }
async removeDomainPrompt( async removeDomainPrompt(
+39 -8
View File
@@ -2869,13 +2869,20 @@ export class LegacyMigrateService {
} }
// Load already-migrated variations to stay idempotent. // Load already-migrated variations to stay idempotent.
// Meshkee allows at most one color and one size variation per category.
const existingVariations = await this.prisma.categoryVariation.findMany({ const existingVariations = await this.prisma.categoryVariation.findMany({
where: { businessId }, where: { businessId },
select: { id: true, categoryId: true, name: true }, select: { id: true, categoryId: true, name: true, variationType: true },
}); });
const existingVarSet = new Set( const existingVarSet = new Set(
existingVariations.map((v) => `${v.categoryId}:${v.name}`), existingVariations.map((v) => `${v.categoryId}:${v.name}`),
); );
const variationIdByType = new Map<string, bigint>();
for (const v of existingVariations) {
if (v.variationType === 'color' || v.variationType === 'size') {
variationIdByType.set(`${v.categoryId}:${v.variationType}`, v.id);
}
}
for (const attr of attributes) { for (const attr of attributes) {
const newCategoryId = oldToNew.get(attr.attributable_id); const newCategoryId = oldToNew.get(attr.attributable_id);
@@ -2883,12 +2890,20 @@ export class LegacyMigrateService {
const variationName = (attr.name ?? '').trim() || 'Variation'; const variationName = (attr.name ?? '').trim() || 'Variation';
const varKey = `${newCategoryId}:${variationName}`; const varKey = `${newCategoryId}:${variationName}`;
if (existingVarSet.has(varKey)) continue;
const variationType = this.legacyAttributeTypeToVariationType( const variationType = this.legacyAttributeTypeToVariationType(
attr.attribute_type, attr.attribute_type,
); );
const typeKey =
variationType === 'color' || variationType === 'size'
? `${newCategoryId}:${variationType}`
: null;
let variationId = typeKey ? variationIdByType.get(typeKey) : undefined;
if (!variationId && existingVarSet.has(varKey)) continue;
if (!variationId) {
try {
const variation = await this.prisma.categoryVariation.create({ const variation = await this.prisma.categoryVariation.create({
data: { data: {
businessId, businessId,
@@ -2899,9 +2914,27 @@ export class LegacyMigrateService {
}, },
select: { id: true }, select: { id: true },
}); });
variationId = variation.id;
existingVarSet.add(varKey); existingVarSet.add(varKey);
if (typeKey) variationIdByType.set(typeKey, variationId);
} catch {
// Unique: one color/size per category, or custom name already exists.
if (typeKey) {
const existing = await this.prisma.categoryVariation.findFirst({
where: { categoryId: newCategoryId, variationType },
select: { id: true },
});
variationId = existing?.id;
if (variationId) variationIdByType.set(typeKey, variationId);
}
if (!variationId) continue;
}
}
const options = valuesByAttr.get(attr.id) ?? []; const options = valuesByAttr.get(attr.id) ?? [];
const existingOptionCount = await this.prisma.categoryVariationOption.count(
{ where: { variationId } },
);
for (const [idx, opt] of options.entries()) { for (const [idx, opt] of options.entries()) {
const label = (opt.name ?? '').trim() || 'Option'; const label = (opt.name ?? '').trim() || 'Option';
const value = const value =
@@ -2912,14 +2945,12 @@ export class LegacyMigrateService {
try { try {
await this.prisma.categoryVariationOption.create({ await this.prisma.categoryVariationOption.create({
data: { data: {
variationId: variation.id, variationId,
label, label,
value, value,
colorHex: colorHex:
variationType === 'color' variationType === 'color' ? this.guessColorHex(label) : null,
? this.guessColorHex(label) sortOrder: existingOptionCount + idx,
: null,
sortOrder: idx,
}, },
}); });
} catch { } catch {
+23 -24
View File
@@ -98,12 +98,14 @@ export class DomainAdminService {
const now = Date.now(); const now = Date.now();
const items = rows.map((row) => { const items = rows.map((row) => {
const certStillValid = const expiryMs = row.sslExpiresAt?.getTime() ?? null;
!row.sslExpiresAt || row.sslExpiresAt.getTime() > now; const certStillValid = expiryMs == null || expiryMs > now;
// Prefer stored notAfter when present (daily probe is only a writer).
const sslEnabled =
expiryMs != null ? expiryMs > now : row.sslEnabled && certStillValid;
return { return {
...row, ...row,
// List badge: issued/probed OK and cert not past notAfter. sslEnabled,
sslEnabled: row.sslEnabled && certStillValid,
sslExpiresAt: row.sslExpiresAt, sslExpiresAt: row.sslExpiresAt,
}; };
}); });
@@ -208,8 +210,9 @@ export class DomainAdminService {
? domain.host ? domain.host
: `www.${domain.host}`; : `www.${domain.host}`;
const wwwProbe = await probeTlsHostDetailed(wwwHost); const wwwProbe = await probeTlsHostDetailed(wwwHost);
const ok = apexProbe.ok && wwwProbe.ok; const apexOk = apexProbe.ok;
if (ok) { const bothOk = apexOk && wwwProbe.ok;
if (apexOk) {
const sslExpiresAt = earliestTlsExpiry(apexProbe, wwwProbe); const sslExpiresAt = earliestTlsExpiry(apexProbe, wwwProbe);
await this.prisma.domain.update({ await this.prisma.domain.update({
where: { id: domain.id }, where: { id: domain.id },
@@ -218,10 +221,8 @@ export class DomainAdminService {
...(sslExpiresAt ? { sslExpiresAt } : {}), ...(sslExpiresAt ? { sslExpiresAt } : {}),
}, },
}); });
continue; if (bothOk) continue;
} } else if (domain.sslEnabled) {
if (domain.sslEnabled) {
await this.prisma.domain.update({ await this.prisma.domain.update({
where: { id: domain.id }, where: { id: domain.id },
data: { sslEnabled: false, sslExpiresAt: null }, data: { sslEnabled: false, sslExpiresAt: null },
@@ -253,23 +254,23 @@ export class DomainAdminService {
? domain.host ? domain.host
: `www.${domain.host}`; : `www.${domain.host}`;
const wwwProbe = await probeTlsHostDetailed(wwwHost); const wwwProbe = await probeTlsHostDetailed(wwwHost);
const wwwOk = apexProbe.ok && wwwProbe.ok; const apexOk = apexProbe.ok;
const sslExpiresAt = wwwOk const sslExpiresAt = apexOk
? earliestTlsExpiry(apexProbe, wwwProbe) ? earliestTlsExpiry(apexProbe, wwwProbe)
: null; : null;
await this.prisma.domain.update({ await this.prisma.domain.update({
where: { id: domain.id }, where: { id: domain.id },
data: { data: {
sslEnabled: wwwOk, sslEnabled: apexOk,
sslExpiresAt, sslExpiresAt,
}, },
}); });
if (wwwOk) { if (apexOk) {
issued.push(domain.host); issued.push(domain.host);
} else { } else {
failed.push({ failed.push({
host: domain.host, host: domain.host,
error: 'Certbot finished but TLS probe still failed (apex or www)', error: 'Certbot finished but TLS probe still failed on apex',
}); });
} }
} catch (err) { } catch (err) {
@@ -446,22 +447,20 @@ export class DomainAdminService {
} }
} }
const storefrontOk = apexOk && wwwOk; // List badge follows apex. www can still fail (no DNS) without marking Invalid.
// Re-probe apex/www once more when already-ok so we persist current cert expiry. if (apexOk && !apexProbe.expiresAt) {
if (storefrontOk && (!apexProbe.expiresAt || !wwwProbe.expiresAt)) {
apexProbe = await probeTlsHostDetailed(apex); apexProbe = await probeTlsHostDetailed(apex);
wwwProbe = await probeTlsHostDetailed(wwwHost);
apexOk = apexProbe.ok; apexOk = apexProbe.ok;
wwwOk = wwwProbe.ok;
} }
const sslExpiresAt = const sslExpiresAt = apexOk
apexOk && wwwOk ? earliestTlsExpiry(apexProbe, wwwProbe) : null; ? earliestTlsExpiry(apexProbe, wwwOk ? wwwProbe : apexProbe)
: null;
const updated = await this.prisma.domain.update({ const updated = await this.prisma.domain.update({
where: { id: domainId }, where: { id: domainId },
data: { data: {
sslEnabled: storefrontOk && apexOk && wwwOk, sslEnabled: apexOk,
sslExpiresAt: storefrontOk && apexOk && wwwOk ? sslExpiresAt : null, sslExpiresAt: apexOk ? sslExpiresAt : null,
}, },
}); });
+1 -1
View File
@@ -19,7 +19,7 @@ export class InternalSslController {
return this.service.listApiHosts(); return this.service.listApiHosts();
} }
/** Manually probe apex hosts and sync domains.ssl_enabled. */ /** Manually probe apex hosts and sync domains.ssl_enabled / ssl_expires_at. */
@Post('refresh-status') @Post('refresh-status')
refreshStatus() { refreshStatus() {
return this.service.refreshSslStatuses('manual'); return this.service.refreshSslStatuses('manual');
+29 -25
View File
@@ -85,8 +85,9 @@ export class InternalSslService {
} }
/** /**
* Occasional safety probe (once a day). Issue SSL writes ssl_expires_at and * Occasional safety probe (once a day). Apex TLS is the list source of
* is the main source of truth for the list badge between probes. * truth; a failed probe does not wipe ssl_expires_at while that date is
* still in the future. Issue SSL also writes ssl_expires_at.
*/ */
@Cron(CronExpression.EVERY_DAY_AT_4AM) @Cron(CronExpression.EVERY_DAY_AT_4AM)
async refreshSslStatusesCron() { async refreshSslStatusesCron() {
@@ -125,42 +126,45 @@ export class InternalSslService {
} }
} }
const hostsToProbe = domains.flatMap((d) => { // Apex is the list badge. www is optional — missing www DNS used to
const apex = d.host.trim().toLowerCase(); // mark every row Invalid and wipe ssl_expires_at.
if (!apex) return []; const hostsToProbe = domains
const www = apex.startsWith('www.') ? apex : `www.${apex}`; .map((d) => d.host.trim().toLowerCase())
return [apex, www]; .filter(Boolean);
});
const sslByHost = await probeTlsHostsDetailed(hostsToProbe); const sslByHost = await probeTlsHostsDetailed(hostsToProbe);
for (const domain of domains) { for (const domain of domains) {
const hostKey = domain.host.trim().toLowerCase(); const hostKey = domain.host.trim().toLowerCase();
const wwwKey = hostKey.startsWith('www.') ? hostKey : `www.${hostKey}`;
const apexResult = sslByHost.get(hostKey) ?? { const apexResult = sslByHost.get(hostKey) ?? {
ok: false, ok: false,
expiresAt: null, expiresAt: null,
}; };
const wwwResult = sslByHost.get(wwwKey) ?? { const storedExpiry = domain.sslExpiresAt;
ok: false, const storedStillValid =
expiresAt: null, !!storedExpiry && storedExpiry.getTime() > now.getTime();
};
const liveOk = apexResult.ok && wwwResult.ok;
const sslExpiresAt = liveOk
? earliestTlsExpiry(apexResult, wwwResult)
: null;
const nextEnabled = liveOk; let nextEnabled: boolean;
const nextExpires = let nextExpires: Date | null;
liveOk && sslExpiresAt
? sslExpiresAt if (apexResult.ok) {
: liveOk nextEnabled = true;
? domain.sslExpiresAt nextExpires = earliestTlsExpiry(apexResult) ?? storedExpiry;
: null; } else if (storedStillValid) {
// Failed probe must not clear a cert that is still in date.
nextEnabled = true;
nextExpires = storedExpiry;
this.logger.warn(
`SSL status ${domain.host}: live probe failed; keeping stored expiry ${storedExpiry.toISOString()}`,
);
} else {
nextEnabled = false;
nextExpires = null;
}
const enabledChanged = nextEnabled !== domain.sslEnabled; const enabledChanged = nextEnabled !== domain.sslEnabled;
const expiryChanged = const expiryChanged =
(nextExpires?.getTime() ?? null) !== (nextExpires?.getTime() ?? null) !==
(domain.sslExpiresAt?.getTime() ?? null); (storedExpiry?.getTime() ?? null);
if (!enabledChanged && !expiryChanged) continue; if (!enabledChanged && !expiryChanged) continue;