mirror of
https://git.meshkee.com/Meshkee/dashboards.git
synced 2026-08-11 22:30:58 +04:30
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:
co-authored by
Cursor
parent
aeaad0f645
commit
0212148a73
@@ -2,7 +2,8 @@
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 24px;
|
bottom: 24px;
|
||||||
right: 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;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
|
|||||||
@@ -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() {
|
async function submitDomain() {
|
||||||
if (!domainBusiness) return
|
if (!domainBusiness) return
|
||||||
const host = domainHost.trim()
|
const host = domainHost.trim()
|
||||||
if (!host) return
|
if (!host) return
|
||||||
const gitRepoUrl = domainGitRepoUrl.trim()
|
const gitRepoUrl = domainGitRepoUrl.trim()
|
||||||
|
const businessId = domainBusiness.id
|
||||||
|
const submitId = ++domainSubmitGenRef.current
|
||||||
|
|
||||||
setDomainSubmitting(true)
|
setDomainSubmitting(true)
|
||||||
setDomainError('')
|
setDomainError('')
|
||||||
try {
|
try {
|
||||||
if (domainId) {
|
if (domainId) {
|
||||||
const updated = (await updateBusinessDomain(domainBusiness.id, domainId, {
|
const updated = (await updateBusinessDomain(businessId, domainId, {
|
||||||
host,
|
host,
|
||||||
...(gitRepoUrl ? { gitRepoUrl } : {}),
|
...(gitRepoUrl ? { gitRepoUrl } : {}),
|
||||||
})) as {
|
})) as {
|
||||||
id: number
|
id: number
|
||||||
host: string
|
host: string
|
||||||
deploySlug?: string | null
|
deploySlug?: string | null
|
||||||
|
gitRepoUrl?: string | null
|
||||||
provisionError?: string | null
|
provisionError?: string | null
|
||||||
}
|
}
|
||||||
setData((prev) => {
|
setData((prev) => {
|
||||||
@@ -716,7 +732,18 @@ export function BusinessesPage() {
|
|||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
items: prev.items.map((item) =>
|
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')
|
showToast(`Domain updated to "${host}".`, 'success')
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const created = (await addBusinessDomain(domainBusiness.id, {
|
const created = (await addBusinessDomain(businessId, {
|
||||||
host,
|
host,
|
||||||
isPrimary: true,
|
isPrimary: true,
|
||||||
...(gitRepoUrl ? { gitRepoUrl } : {}),
|
...(gitRepoUrl ? { gitRepoUrl } : {}),
|
||||||
@@ -741,6 +768,7 @@ export function BusinessesPage() {
|
|||||||
host: string
|
host: string
|
||||||
sslEnabled: boolean
|
sslEnabled: boolean
|
||||||
deploySlug?: string | null
|
deploySlug?: string | null
|
||||||
|
gitRepoUrl?: string | null
|
||||||
provisionError?: string | null
|
provisionError?: string | null
|
||||||
}
|
}
|
||||||
setData((prev) => {
|
setData((prev) => {
|
||||||
@@ -748,12 +776,18 @@ export function BusinessesPage() {
|
|||||||
return {
|
return {
|
||||||
...prev,
|
...prev,
|
||||||
items: prev.items.map((item) =>
|
items: prev.items.map((item) =>
|
||||||
item.id === domainBusiness.id
|
item.id === businessId
|
||||||
? {
|
? {
|
||||||
...item,
|
...item,
|
||||||
domainId: created.id,
|
domainId: created.id,
|
||||||
domain: created.host,
|
domain: created.host,
|
||||||
sslEnabled: created.sslEnabled ?? false,
|
sslEnabled: created.sslEnabled ?? false,
|
||||||
|
...(gitRepoUrl && !created.provisionError
|
||||||
|
? {
|
||||||
|
gitRepoUrl: created.gitRepoUrl ?? gitRepoUrl,
|
||||||
|
deploySlug: created.deploySlug ?? null,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
}
|
}
|
||||||
: item,
|
: item,
|
||||||
),
|
),
|
||||||
@@ -771,16 +805,22 @@ export function BusinessesPage() {
|
|||||||
showToast(`Domain "${host}" added.`, 'success')
|
showToast(`Domain "${host}" added.`, 'success')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (submitId !== domainSubmitGenRef.current) return
|
||||||
setDomainOpen(false)
|
setDomainOpen(false)
|
||||||
setDomainBusiness(null)
|
setDomainBusiness(null)
|
||||||
setDomainId(null)
|
setDomainId(null)
|
||||||
|
setDomainHost('')
|
||||||
setDomainGitRepoUrl('')
|
setDomainGitRepoUrl('')
|
||||||
|
setDomainError('')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (submitId !== domainSubmitGenRef.current) return
|
||||||
setDomainError(err instanceof ApiError ? err.message : 'Unable to save domain.')
|
setDomainError(err instanceof ApiError ? err.message : 'Unable to save domain.')
|
||||||
} finally {
|
} finally {
|
||||||
|
if (submitId === domainSubmitGenRef.current) {
|
||||||
setDomainSubmitting(false)
|
setDomainSubmitting(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function resetCreateForm() {
|
function resetCreateForm() {
|
||||||
setCreateName('')
|
setCreateName('')
|
||||||
@@ -1270,19 +1310,18 @@ export function BusinessesPage() {
|
|||||||
<Modal
|
<Modal
|
||||||
open={domainOpen}
|
open={domainOpen}
|
||||||
title={domainId ? 'Edit domain' : 'Add domain'}
|
title={domainId ? 'Edit domain' : 'Add domain'}
|
||||||
onClose={() => {
|
onClose={closeDomainModal}
|
||||||
setDomainOpen(false)
|
|
||||||
setDomainBusiness(null)
|
|
||||||
setDomainId(null)
|
|
||||||
setDomainGitRepoUrl('')
|
|
||||||
setDomainError('')
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{domainError ? (
|
{domainError ? (
|
||||||
<p className={styles.alertError} role="alert">
|
<p className={styles.alertError} role="alert">
|
||||||
{domainError}
|
{domainError}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : 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}>
|
<div className={styles.field}>
|
||||||
<label htmlFor="domain-host">Domain</label>
|
<label htmlFor="domain-host">Domain</label>
|
||||||
<input
|
<input
|
||||||
@@ -1315,8 +1354,7 @@ export function BusinessesPage() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`${styles.btn} ${styles.btnGhost}`}
|
className={`${styles.btn} ${styles.btnGhost}`}
|
||||||
onClick={() => setDomainOpen(false)}
|
onClick={closeDomainModal}
|
||||||
disabled={domainSubmitting}
|
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
@@ -1326,7 +1364,7 @@ export function BusinessesPage() {
|
|||||||
onClick={() => void submitDomain()}
|
onClick={() => void submitDomain()}
|
||||||
disabled={domainSubmitting || !domainHost.trim()}
|
disabled={domainSubmitting || !domainHost.trim()}
|
||||||
>
|
>
|
||||||
Save
|
{domainSubmitting ? 'Saving…' : 'Save'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
RotateCcw,
|
RotateCcw,
|
||||||
Search,
|
Search,
|
||||||
ShieldPlus,
|
ShieldPlus,
|
||||||
Unlock,
|
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal'
|
import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal'
|
||||||
import { DomainLinksOverflowMenu } from '../components/DomainLinksOverflowMenu'
|
import { DomainLinksOverflowMenu } from '../components/DomainLinksOverflowMenu'
|
||||||
@@ -26,7 +25,6 @@ import {
|
|||||||
listDomains,
|
listDomains,
|
||||||
removeDomain,
|
removeDomain,
|
||||||
setDomainActive,
|
setDomainActive,
|
||||||
setDomainSsl,
|
|
||||||
syncSsl,
|
syncSsl,
|
||||||
updateDomain,
|
updateDomain,
|
||||||
} from '../services/domainService'
|
} from '../services/domainService'
|
||||||
@@ -105,7 +103,6 @@ export function WebsitesPage() {
|
|||||||
|
|
||||||
const [removeTarget, setRemoveTarget] = useState<DomainListItem | null>(null)
|
const [removeTarget, setRemoveTarget] = useState<DomainListItem | null>(null)
|
||||||
const [togglingActiveId, setTogglingActiveId] = useState<number | 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 [deployingId, setDeployingId] = useState<DomainListItem['id'] | null>(null)
|
||||||
const [syncingSsl, setSyncingSsl] = useState(false)
|
const [syncingSsl, setSyncingSsl] = useState(false)
|
||||||
const [issuingWebsiteSsl, setIssuingWebsiteSsl] = 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() {
|
async function handleSyncSsl() {
|
||||||
if (syncingSsl || issuingWebsiteSsl) return
|
if (syncingSsl || issuingWebsiteSsl) return
|
||||||
|
|
||||||
@@ -309,18 +274,9 @@ export function WebsitesPage() {
|
|||||||
} else {
|
} else {
|
||||||
showToast(result.message || 'Website SSL updated.', 'success')
|
showToast(result.message || 'Website SSL updated.', 'success')
|
||||||
}
|
}
|
||||||
if (result.issued.length > 0) {
|
// Re-load so ssl_enabled matches live probes (including corrected false flags).
|
||||||
setData((prev) => {
|
const refreshed = await listDomains({ page, pageSize: PAGE_SIZE, ...appliedFilters })
|
||||||
if (!prev) return prev
|
setData(refreshed)
|
||||||
const issued = new Set(result.issued)
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
items: prev.items.map((item) =>
|
|
||||||
issued.has(item.host) ? { ...item, sslEnabled: true } : item,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof ApiError ? err.message : 'Unable to issue website SSL.'
|
const message = err instanceof ApiError ? err.message : 'Unable to issue website SSL.'
|
||||||
setError(message)
|
setError(message)
|
||||||
@@ -331,13 +287,16 @@ export function WebsitesPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleIssueDomainSsl(domain: DomainListItem) {
|
async function handleIssueDomainSsl(domain: DomainListItem) {
|
||||||
if (!domain.deploySlug || issuingSslId != null) return
|
if (issuingSslId != null) return
|
||||||
|
|
||||||
flushSync(() => {
|
flushSync(() => {
|
||||||
setIssuingSslId(domain.id)
|
setIssuingSslId(domain.id)
|
||||||
})
|
})
|
||||||
setError('')
|
setError('')
|
||||||
showToast(`Issuing SSL for "${domain.host}"…`, 'info')
|
showToast(
|
||||||
|
`Checking SSL for ${domain.host}, business.${domain.host}, customer.${domain.host}…`,
|
||||||
|
'info',
|
||||||
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await issueDomainSsl(domain.id)
|
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) {
|
} 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)
|
setError(message)
|
||||||
showToast(message, 'error')
|
showToast(message, 'error')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -468,7 +433,7 @@ export function WebsitesPage() {
|
|||||||
onClick={() => void handleIssueWebsiteSsl()}
|
onClick={() => void handleIssueWebsiteSsl()}
|
||||||
disabled={issuingWebsiteSsl || syncingSsl}
|
disabled={issuingWebsiteSsl || syncingSsl}
|
||||||
aria-busy={issuingWebsiteSsl}
|
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 ? (
|
{issuingWebsiteSsl ? (
|
||||||
<Loader2 size={16} className={styles.spin} aria-hidden />
|
<Loader2 size={16} className={styles.spin} aria-hidden />
|
||||||
@@ -665,46 +630,31 @@ export function WebsitesPage() {
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
{domain.deploySlug && !domain.sslEnabled ? (
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`${tableStyles.controlBtn} ${
|
className={`${tableStyles.controlBtn} ${
|
||||||
issuingSslId === domain.id ? styles.deployBusy : ''
|
issuingSslId === domain.id ? styles.deployBusy : ''
|
||||||
}`}
|
}`}
|
||||||
onClick={() => void handleIssueDomainSsl(domain)}
|
onClick={() => void handleIssueDomainSsl(domain)}
|
||||||
disabled={issuingSslId === domain.id || issuingWebsiteSsl}
|
disabled={issuingSslId === domain.id || issuingWebsiteSsl || syncingSsl}
|
||||||
title={
|
title={
|
||||||
issuingSslId === domain.id
|
issuingSslId === domain.id
|
||||||
? 'Issuing SSL…'
|
? 'Checking / issuing SSL…'
|
||||||
: 'Issue SSL certificate'
|
: 'Check & issue SSL for domain, business.*, customer.*'
|
||||||
}
|
}
|
||||||
aria-label={
|
aria-label={
|
||||||
issuingSslId === domain.id
|
issuingSslId === domain.id
|
||||||
? 'Issuing SSL'
|
? 'Ensuring SSL'
|
||||||
: 'Issue SSL certificate'
|
: 'Check and issue SSL'
|
||||||
}
|
}
|
||||||
aria-busy={issuingSslId === domain.id}
|
aria-busy={issuingSslId === domain.id}
|
||||||
>
|
>
|
||||||
{issuingSslId === domain.id ? (
|
{issuingSslId === domain.id ? (
|
||||||
<Loader2 size={16} className={styles.spin} aria-hidden />
|
<Loader2 size={16} className={styles.spin} aria-hidden />
|
||||||
) : (
|
) : (
|
||||||
<ShieldPlus size={16} />
|
<Lock size={16} />
|
||||||
)}
|
)}
|
||||||
</button>
|
</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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={tableStyles.controlBtn}
|
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) {
|
export async function issueDomainSsl(domainId: number | string) {
|
||||||
return apiRequest<IssueDomainSslResponse>(`/domains/${domainId}/issue-ssl`, {
|
return apiRequest<IssueDomainSslResponse>(`/domains/${domainId}/issue-ssl`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|||||||
@@ -43,9 +43,22 @@ export interface IssueWebsiteSslResponse {
|
|||||||
failed: Array<{ host: string; error: string }>
|
failed: Array<{ host: string; error: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IssueDomainSslHostResult {
|
||||||
|
host: string
|
||||||
|
status: 'ok' | 'issued' | 'failed' | 'skipped'
|
||||||
|
detail?: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface IssueDomainSslResponse {
|
export interface IssueDomainSslResponse {
|
||||||
status: 'issued'
|
status: 'ok' | 'partial'
|
||||||
host: string
|
host: string
|
||||||
sslEnabled: boolean
|
sslEnabled: boolean
|
||||||
|
hosts: {
|
||||||
|
apex: IssueDomainSslHostResult
|
||||||
|
business: IssueDomainSslHostResult
|
||||||
|
customer: IssueDomainSslHostResult
|
||||||
|
}
|
||||||
|
issued: string[]
|
||||||
|
failed: Array<{ host: string; error: string }>
|
||||||
message: string
|
message: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
/**
|
/**
|
||||||
* Tiny HTTP agent on the dashboards VPS.
|
* Tiny HTTP agent on the dashboards VPS.
|
||||||
* Super Admin → Nest API → POST here → runs ssl-sync.sh in the background.
|
* Super Admin → Nest API → POST here → runs ssl-sync.sh
|
||||||
*
|
*
|
||||||
* Env (/etc/meshkee/ssl-sync-agent.env):
|
* Env (/etc/meshkee/ssl-sync-agent.env):
|
||||||
* SSL_SYNC_AGENT_TOKEN=...
|
* SSL_SYNC_AGENT_TOKEN=...
|
||||||
* SSL_SYNC_SCRIPT=/opt/meshkee/dashboards/deploy/ssl-sync.sh
|
* SSL_SYNC_SCRIPT=/opt/meshkee/dashboards/deploy/ssl-sync.sh
|
||||||
* PORT=9051
|
* PORT=9051
|
||||||
* BIND=0.0.0.0
|
* BIND=0.0.0.0
|
||||||
|
*
|
||||||
|
* POST /ssl-sync
|
||||||
|
* Body (optional JSON): { "wait": true }
|
||||||
|
* wait=false (default): accept and run in background → 202
|
||||||
|
* wait=true: run script and wait for exit → 200 / 500
|
||||||
*/
|
*/
|
||||||
import { createServer } from 'node:http'
|
import { createServer } from 'node:http'
|
||||||
import { spawn } from 'node:child_process'
|
import { spawn } from 'node:child_process'
|
||||||
@@ -42,7 +47,24 @@ function json(res, status, body) {
|
|||||||
res.end(payload)
|
res.end(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
function startSync() {
|
function readJson(req) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const chunks = []
|
||||||
|
req.on('data', (c) => chunks.push(c))
|
||||||
|
req.on('end', () => {
|
||||||
|
const raw = Buffer.concat(chunks).toString('utf8').trim()
|
||||||
|
if (!raw) return resolve({})
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(raw))
|
||||||
|
} catch (err) {
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
req.on('error', reject)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function startSyncDetached() {
|
||||||
running = true
|
running = true
|
||||||
const child = spawn(SCRIPT, [], {
|
const child = spawn(SCRIPT, [], {
|
||||||
detached: true,
|
detached: true,
|
||||||
@@ -62,7 +84,45 @@ function startSync() {
|
|||||||
child.unref()
|
child.unref()
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = createServer((req, res) => {
|
function runSyncAndWait() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
running = true
|
||||||
|
const child = spawn(SCRIPT, [], {
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
env: process.env,
|
||||||
|
})
|
||||||
|
let stdout = ''
|
||||||
|
let stderr = ''
|
||||||
|
child.stdout.on('data', (d) => {
|
||||||
|
stdout += d.toString()
|
||||||
|
})
|
||||||
|
child.stderr.on('data', (d) => {
|
||||||
|
stderr += d.toString()
|
||||||
|
})
|
||||||
|
child.on('error', (err) => {
|
||||||
|
running = false
|
||||||
|
resolve({
|
||||||
|
ok: false,
|
||||||
|
code: 1,
|
||||||
|
log: err.message,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
child.on('exit', (code, signal) => {
|
||||||
|
running = false
|
||||||
|
const log = `${stdout}${stderr}`.trim().slice(-4000)
|
||||||
|
console.log(
|
||||||
|
`${new Date().toISOString()} ssl-sync waited code=${code} signal=${signal ?? ''}`,
|
||||||
|
)
|
||||||
|
resolve({
|
||||||
|
ok: code === 0,
|
||||||
|
code: code ?? 1,
|
||||||
|
log,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = createServer(async (req, res) => {
|
||||||
if (req.method === 'GET' && req.url === '/health') {
|
if (req.method === 'GET' && req.url === '/health') {
|
||||||
return json(res, 200, { ok: true, running })
|
return json(res, 200, { ok: true, running })
|
||||||
}
|
}
|
||||||
@@ -80,7 +140,34 @@ const server = createServer((req, res) => {
|
|||||||
return json(res, 409, { error: 'ssl sync already running' })
|
return json(res, 409, { error: 'ssl sync already running' })
|
||||||
}
|
}
|
||||||
|
|
||||||
startSync()
|
let body = {}
|
||||||
|
try {
|
||||||
|
body = await readJson(req)
|
||||||
|
} catch {
|
||||||
|
return json(res, 400, { error: 'invalid json' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const wait = body && body.wait === true
|
||||||
|
|
||||||
|
if (wait) {
|
||||||
|
console.log(`${new Date().toISOString()} ssl-sync wait start`)
|
||||||
|
const result = await runSyncAndWait()
|
||||||
|
if (!result.ok) {
|
||||||
|
return json(res, 500, {
|
||||||
|
status: 'failed',
|
||||||
|
message: 'SSL sync script failed',
|
||||||
|
code: result.code,
|
||||||
|
log: result.log,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return json(res, 200, {
|
||||||
|
status: 'ok',
|
||||||
|
message: 'SSL sync completed',
|
||||||
|
log: result.log,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
startSyncDetached()
|
||||||
console.log(`${new Date().toISOString()} ssl-sync accepted`)
|
console.log(`${new Date().toISOString()} ssl-sync accepted`)
|
||||||
return json(res, 202, {
|
return json(res, 202, {
|
||||||
status: 'accepted',
|
status: 'accepted',
|
||||||
|
|||||||
@@ -58,6 +58,10 @@ SSL_SYNC_AGENT_TOKEN=<same secret>
|
|||||||
|
|
||||||
Endpoint used by the UI: `POST /api/v1/domains/ssl-sync` (super-admin JWT).
|
Endpoint used by the UI: `POST /api/v1/domains/ssl-sync` (super-admin JWT).
|
||||||
|
|
||||||
|
Body on the agent (optional): `{ "wait": true }` — run `ssl-sync.sh` and wait for exit (used by per-row Ensure SSL). Default is fire-and-forget `202`.
|
||||||
|
|
||||||
|
Per-row Ensure SSL: `POST /api/v1/domains/:id/issue-ssl` probes `host`, `business.host`, `customer.host` and issues only failures.
|
||||||
|
|
||||||
## Redeploy frontends
|
## Redeploy frontends
|
||||||
|
|
||||||
From your laptop (rsync source tree, then build on server):
|
From your laptop (rsync source tree, then build on server):
|
||||||
|
|||||||
@@ -514,7 +514,7 @@ Migration: `database/migrations/033_business_favicon.sql`
|
|||||||
| API | `https://api.meshkee.com/api/v1` (host `185.164.72.119`) |
|
| API | `https://api.meshkee.com/api/v1` (host `185.164.72.119`) |
|
||||||
| Docs | `docs/DEPLOY.md` |
|
| Docs | `docs/DEPLOY.md` |
|
||||||
|
|
||||||
SSL: Certbot cert `meshkee-dashboards` + cron `ssl-sync.sh` every 2h + Super Admin **Sync dashboard SSL** (`POST /domains/ssl-sync` → dashboards agent). Storefront SSL: **Issue website SSL** (`POST /domains/website-ssl`) + per-row issue (`POST /domains/:id/issue-ssl`) → websites agent `/ssl` (certbot).
|
SSL: Certbot cert `meshkee-dashboards` + cron `ssl-sync.sh` every 2h + Super Admin **Sync dashboard SSL** (`POST /domains/ssl-sync` → dashboards agent). Storefront SSL: **Issue website SSL** (`POST /domains/website-ssl`) → websites agent `/ssl`. Per-row lock: **Ensure SSL** (`POST /domains/:id/issue-ssl`) probes apex + `business.*` + `customer.*`, issues only what’s missing (websites agent for apex when `deploy_slug` set; blocking dashboard ssl-sync when business/customer need certs).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user