Ensure per-row SSL for apex, business.*, and customer.* hosts.

Lock icon probes live TLS and issues only missing certs; toast z-index sits above modals; ssl-sync agent supports blocking wait.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-09 16:32:05 +03:30
co-authored by Cursor
parent aeaad0f645
commit 0212148a73
8 changed files with 209 additions and 116 deletions
@@ -2,7 +2,8 @@
position: fixed;
bottom: 24px;
right: 24px;
z-index: 300;
/* Above Modal overlay (z-index 300) so success/error feedback is visible while dialogs are open */
z-index: 400;
display: flex;
flex-direction: column;
align-items: flex-end;
+54 -16
View File
@@ -692,23 +692,39 @@ export function BusinessesPage() {
}
}
const domainSubmitGenRef = useRef(0)
function closeDomainModal() {
domainSubmitGenRef.current += 1
setDomainSubmitting(false)
setDomainOpen(false)
setDomainBusiness(null)
setDomainId(null)
setDomainHost('')
setDomainGitRepoUrl('')
setDomainError('')
}
async function submitDomain() {
if (!domainBusiness) return
const host = domainHost.trim()
if (!host) return
const gitRepoUrl = domainGitRepoUrl.trim()
const businessId = domainBusiness.id
const submitId = ++domainSubmitGenRef.current
setDomainSubmitting(true)
setDomainError('')
try {
if (domainId) {
const updated = (await updateBusinessDomain(domainBusiness.id, domainId, {
const updated = (await updateBusinessDomain(businessId, domainId, {
host,
...(gitRepoUrl ? { gitRepoUrl } : {}),
})) as {
id: number
host: string
deploySlug?: string | null
gitRepoUrl?: string | null
provisionError?: string | null
}
setData((prev) => {
@@ -716,7 +732,18 @@ export function BusinessesPage() {
return {
...prev,
items: prev.items.map((item) =>
item.id === domainBusiness.id ? { ...item, domain: host } : item,
item.id === businessId
? {
...item,
domain: host,
...(gitRepoUrl && !updated.provisionError
? {
gitRepoUrl: updated.gitRepoUrl ?? gitRepoUrl,
deploySlug: updated.deploySlug ?? item.deploySlug,
}
: {}),
}
: item,
),
}
})
@@ -732,7 +759,7 @@ export function BusinessesPage() {
showToast(`Domain updated to "${host}".`, 'success')
}
} else {
const created = (await addBusinessDomain(domainBusiness.id, {
const created = (await addBusinessDomain(businessId, {
host,
isPrimary: true,
...(gitRepoUrl ? { gitRepoUrl } : {}),
@@ -741,6 +768,7 @@ export function BusinessesPage() {
host: string
sslEnabled: boolean
deploySlug?: string | null
gitRepoUrl?: string | null
provisionError?: string | null
}
setData((prev) => {
@@ -748,12 +776,18 @@ export function BusinessesPage() {
return {
...prev,
items: prev.items.map((item) =>
item.id === domainBusiness.id
item.id === businessId
? {
...item,
domainId: created.id,
domain: created.host,
sslEnabled: created.sslEnabled ?? false,
...(gitRepoUrl && !created.provisionError
? {
gitRepoUrl: created.gitRepoUrl ?? gitRepoUrl,
deploySlug: created.deploySlug ?? null,
}
: {}),
}
: item,
),
@@ -771,14 +805,20 @@ export function BusinessesPage() {
showToast(`Domain "${host}" added.`, 'success')
}
}
if (submitId !== domainSubmitGenRef.current) return
setDomainOpen(false)
setDomainBusiness(null)
setDomainId(null)
setDomainHost('')
setDomainGitRepoUrl('')
setDomainError('')
} catch (err) {
if (submitId !== domainSubmitGenRef.current) return
setDomainError(err instanceof ApiError ? err.message : 'Unable to save domain.')
} finally {
setDomainSubmitting(false)
if (submitId === domainSubmitGenRef.current) {
setDomainSubmitting(false)
}
}
}
@@ -1270,19 +1310,18 @@ export function BusinessesPage() {
<Modal
open={domainOpen}
title={domainId ? 'Edit domain' : 'Add domain'}
onClose={() => {
setDomainOpen(false)
setDomainBusiness(null)
setDomainId(null)
setDomainGitRepoUrl('')
setDomainError('')
}}
onClose={closeDomainModal}
>
{domainError ? (
<p className={styles.alertError} role="alert">
{domainError}
</p>
) : null}
{domainSubmitting && domainGitRepoUrl.trim() ? (
<p className={styles.helperText} role="status">
Saving domain and setting up storefront on the websites server
</p>
) : null}
<div className={styles.field}>
<label htmlFor="domain-host">Domain</label>
<input
@@ -1300,7 +1339,7 @@ export function BusinessesPage() {
id="domain-git-repo"
value={domainGitRepoUrl}
onChange={(e) => setDomainGitRepoUrl(e.target.value)}
placeholder="https://git.meshkee.com/Meshkee-Websites/oaktasty.git"
placeholder="https://git.meshkee.com/Meshkee-Websites/oaktasty.git"
disabled={domainSubmitting}
autoComplete="off"
spellCheck={false}
@@ -1315,8 +1354,7 @@ export function BusinessesPage() {
<button
type="button"
className={`${styles.btn} ${styles.btnGhost}`}
onClick={() => setDomainOpen(false)}
disabled={domainSubmitting}
onClick={closeDomainModal}
>
Cancel
</button>
@@ -1326,7 +1364,7 @@ export function BusinessesPage() {
onClick={() => void submitDomain()}
disabled={domainSubmitting || !domainHost.trim()}
>
Save
{domainSubmitting ? 'Saving…' : 'Save'}
</button>
</div>
</Modal>
+42 -92
View File
@@ -10,7 +10,6 @@ import {
RotateCcw,
Search,
ShieldPlus,
Unlock,
} from 'lucide-react'
import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal'
import { DomainLinksOverflowMenu } from '../components/DomainLinksOverflowMenu'
@@ -26,7 +25,6 @@ import {
listDomains,
removeDomain,
setDomainActive,
setDomainSsl,
syncSsl,
updateDomain,
} from '../services/domainService'
@@ -105,7 +103,6 @@ export function WebsitesPage() {
const [removeTarget, setRemoveTarget] = useState<DomainListItem | null>(null)
const [togglingActiveId, setTogglingActiveId] = useState<number | null>(null)
const [togglingSslId, setTogglingSslId] = useState<number | null>(null)
const [deployingId, setDeployingId] = useState<DomainListItem['id'] | null>(null)
const [syncingSsl, setSyncingSsl] = useState(false)
const [issuingWebsiteSsl, setIssuingWebsiteSsl] = useState(false)
@@ -236,38 +233,6 @@ export function WebsitesPage() {
}
}
async function handleToggleSsl(domain: DomainListItem, sslEnabled: boolean) {
setTogglingSslId(domain.id)
setError('')
setData((prev) => {
if (!prev) return prev
return {
...prev,
items: prev.items.map((item) => (item.id === domain.id ? { ...item, sslEnabled } : item)),
}
})
try {
await setDomainSsl(domain.id, sslEnabled)
showToast(
`SSL ${sslEnabled ? 'enabled' : 'disabled'} for "${domain.host}".`,
'success',
)
} catch (err) {
setData((prev) => {
if (!prev) return prev
return {
...prev,
items: prev.items.map((item) =>
item.id === domain.id ? { ...item, sslEnabled: !sslEnabled } : item,
),
}
})
setError(err instanceof ApiError ? err.message : 'Unable to update SSL status.')
} finally {
setTogglingSslId(null)
}
}
async function handleSyncSsl() {
if (syncingSsl || issuingWebsiteSsl) return
@@ -309,18 +274,9 @@ export function WebsitesPage() {
} else {
showToast(result.message || 'Website SSL updated.', 'success')
}
if (result.issued.length > 0) {
setData((prev) => {
if (!prev) return prev
const issued = new Set(result.issued)
return {
...prev,
items: prev.items.map((item) =>
issued.has(item.host) ? { ...item, sslEnabled: true } : item,
),
}
})
}
// Re-load so ssl_enabled matches live probes (including corrected false flags).
const refreshed = await listDomains({ page, pageSize: PAGE_SIZE, ...appliedFilters })
setData(refreshed)
} catch (err) {
const message = err instanceof ApiError ? err.message : 'Unable to issue website SSL.'
setError(message)
@@ -331,13 +287,16 @@ export function WebsitesPage() {
}
async function handleIssueDomainSsl(domain: DomainListItem) {
if (!domain.deploySlug || issuingSslId != null) return
if (issuingSslId != null) return
flushSync(() => {
setIssuingSslId(domain.id)
})
setError('')
showToast(`Issuing SSL for "${domain.host}"…`, 'info')
showToast(
`Checking SSL for ${domain.host}, business.${domain.host}, customer.${domain.host}`,
'info',
)
try {
const result = await issueDomainSsl(domain.id)
@@ -350,9 +309,15 @@ export function WebsitesPage() {
),
}
})
showToast(result.message || `SSL issued for "${domain.host}".`, 'success')
if (result.failed.length > 0) {
const detail = result.failed.map((f) => `${f.host}: ${f.error}`).join(' · ')
setError(detail)
showToast(result.message, result.issued.length > 0 ? 'info' : 'error')
} else {
showToast(result.message || `SSL ok for "${domain.host}".`, 'success')
}
} catch (err) {
const message = err instanceof ApiError ? err.message : 'Unable to issue SSL.'
const message = err instanceof ApiError ? err.message : 'Unable to ensure SSL.'
setError(message)
showToast(message, 'error')
} finally {
@@ -468,7 +433,7 @@ export function WebsitesPage() {
onClick={() => void handleIssueWebsiteSsl()}
disabled={issuingWebsiteSsl || syncingSsl}
aria-busy={issuingWebsiteSsl}
title="Issue Let's Encrypt certs for storefront domains missing SSL"
title="Probe storefront HTTPS and issue Let's Encrypt where the cert is missing or wrong"
>
{issuingWebsiteSsl ? (
<Loader2 size={16} className={styles.spin} aria-hidden />
@@ -665,46 +630,31 @@ export function WebsitesPage() {
)}
</button>
) : null}
{domain.deploySlug && !domain.sslEnabled ? (
<button
type="button"
className={`${tableStyles.controlBtn} ${
issuingSslId === domain.id ? styles.deployBusy : ''
}`}
onClick={() => void handleIssueDomainSsl(domain)}
disabled={issuingSslId === domain.id || issuingWebsiteSsl}
title={
issuingSslId === domain.id
? 'Issuing SSL…'
: 'Issue SSL certificate'
}
aria-label={
issuingSslId === domain.id
? 'Issuing SSL'
: 'Issue SSL certificate'
}
aria-busy={issuingSslId === domain.id}
>
{issuingSslId === domain.id ? (
<Loader2 size={16} className={styles.spin} aria-hidden />
) : (
<ShieldPlus size={16} />
)}
</button>
) : (
<button
type="button"
className={tableStyles.controlBtn}
onClick={() => void handleToggleSsl(domain, !domain.sslEnabled)}
disabled={togglingSslId === domain.id}
title={domain.sslEnabled ? 'Mark SSL invalid' : 'Mark SSL valid'}
aria-label={
domain.sslEnabled ? 'Mark SSL invalid' : 'Mark SSL valid'
}
>
{domain.sslEnabled ? <Lock size={16} /> : <Unlock size={16} />}
</button>
)}
<button
type="button"
className={`${tableStyles.controlBtn} ${
issuingSslId === domain.id ? styles.deployBusy : ''
}`}
onClick={() => void handleIssueDomainSsl(domain)}
disabled={issuingSslId === domain.id || issuingWebsiteSsl || syncingSsl}
title={
issuingSslId === domain.id
? 'Checking / issuing SSL…'
: 'Check & issue SSL for domain, business.*, customer.*'
}
aria-label={
issuingSslId === domain.id
? 'Ensuring SSL'
: 'Check and issue SSL'
}
aria-busy={issuingSslId === domain.id}
>
{issuingSslId === domain.id ? (
<Loader2 size={16} className={styles.spin} aria-hidden />
) : (
<Lock size={16} />
)}
</button>
<button
type="button"
className={tableStyles.controlBtn}
@@ -82,7 +82,7 @@ export async function issueWebsiteSsl() {
})
}
/** Issue Let's Encrypt for one storefront domain. */
/** Ensure SSL for apex + business.* + customer.* (probe, skip if valid, else issue). */
export async function issueDomainSsl(domainId: number | string) {
return apiRequest<IssueDomainSslResponse>(`/domains/${domainId}/issue-ssl`, {
method: 'POST',
+14 -1
View File
@@ -43,9 +43,22 @@ export interface IssueWebsiteSslResponse {
failed: Array<{ host: string; error: string }>
}
export interface IssueDomainSslHostResult {
host: string
status: 'ok' | 'issued' | 'failed' | 'skipped'
detail?: string
}
export interface IssueDomainSslResponse {
status: 'issued'
status: 'ok' | 'partial'
host: string
sslEnabled: boolean
hosts: {
apex: IssueDomainSslHostResult
business: IssueDomainSslHostResult
customer: IssueDomainSslHostResult
}
issued: string[]
failed: Array<{ host: string; error: string }>
message: string
}