2026-04-07 02:04:22 +05:30
|
|
|
import { useEffect, useState } from 'react'
|
2026-04-07 05:05:28 +05:30
|
|
|
import Modal from 'react-bootstrap/Modal'
|
2026-04-07 02:04:22 +05:30
|
|
|
import { apiRequest } from '../api/client'
|
2026-04-07 05:05:28 +05:30
|
|
|
import { PageHeader, AdminButton, AdminAlert } from '../components/admin'
|
2026-04-07 02:04:22 +05:30
|
|
|
|
|
|
|
|
interface Domain {
|
|
|
|
|
id: number
|
|
|
|
|
name: string
|
|
|
|
|
port: string
|
|
|
|
|
site_id: number
|
|
|
|
|
site_name: string
|
|
|
|
|
site_path: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface Certificate {
|
|
|
|
|
name: string
|
|
|
|
|
path: string
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 11:42:19 +05:30
|
|
|
interface SslDiagnostics {
|
|
|
|
|
vhost_dir: string
|
|
|
|
|
include_snippet: string
|
|
|
|
|
vhosts: { file: string; has_listen_80: boolean; has_listen_443: boolean; has_ssl_directives: boolean }[]
|
|
|
|
|
any_vhost_listen_ssl: boolean
|
|
|
|
|
nginx_effective_listen_443: boolean
|
|
|
|
|
panel_vhost_path_in_nginx_t: boolean
|
|
|
|
|
nginx_t_probe_errors: string[]
|
|
|
|
|
hints: string[]
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 02:04:22 +05:30
|
|
|
export function DomainsPage() {
|
|
|
|
|
const [domains, setDomains] = useState<Domain[]>([])
|
|
|
|
|
const [certificates, setCertificates] = useState<Certificate[]>([])
|
2026-04-07 11:42:19 +05:30
|
|
|
const [diag, setDiag] = useState<SslDiagnostics | null>(null)
|
2026-04-07 02:04:22 +05:30
|
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
|
const [error, setError] = useState('')
|
|
|
|
|
const [requesting, setRequesting] = useState<string | null>(null)
|
|
|
|
|
const [requestDomain, setRequestDomain] = useState<Domain | null>(null)
|
|
|
|
|
const [requestEmail, setRequestEmail] = useState('')
|
|
|
|
|
|
|
|
|
|
const load = () => {
|
|
|
|
|
setLoading(true)
|
|
|
|
|
Promise.all([
|
|
|
|
|
apiRequest<Domain[]>('/ssl/domains'),
|
|
|
|
|
apiRequest<{ certificates: Certificate[] }>('/ssl/certificates'),
|
|
|
|
|
])
|
|
|
|
|
.then(([d, c]) => {
|
|
|
|
|
setDomains(d)
|
|
|
|
|
setCertificates(c.certificates || [])
|
|
|
|
|
})
|
|
|
|
|
.catch((err) => setError(err.message))
|
|
|
|
|
.finally(() => setLoading(false))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
load()
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
const handleRequestCert = (e: React.FormEvent) => {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
if (!requestDomain) return
|
|
|
|
|
setRequesting(requestDomain.name)
|
|
|
|
|
apiRequest<{ status: boolean }>('/ssl/request', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
domain: requestDomain.name,
|
|
|
|
|
webroot: requestDomain.site_path,
|
|
|
|
|
email: requestEmail,
|
|
|
|
|
}),
|
|
|
|
|
})
|
|
|
|
|
.then(() => {
|
|
|
|
|
setRequestDomain(null)
|
|
|
|
|
load()
|
|
|
|
|
})
|
|
|
|
|
.catch((err) => setError(err.message))
|
|
|
|
|
.finally(() => setRequesting(null))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const hasCert = (domain: string) =>
|
|
|
|
|
certificates.some((c) => c.name === domain || c.name.startsWith(domain + ' '))
|
|
|
|
|
|
2026-04-07 05:05:28 +05:30
|
|
|
if (loading) {
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<PageHeader title="Domains & SSL" />
|
|
|
|
|
<p className="text-secondary">Loading…</p>
|
|
|
|
|
</>
|
|
|
|
|
)
|
|
|
|
|
}
|
2026-04-07 02:04:22 +05:30
|
|
|
|
|
|
|
|
return (
|
2026-04-07 05:05:28 +05:30
|
|
|
<>
|
|
|
|
|
<PageHeader title="Domains & SSL" />
|
2026-04-07 02:04:22 +05:30
|
|
|
|
2026-04-07 05:05:28 +05:30
|
|
|
{error ? <AdminAlert className="mb-3">{error}</AdminAlert> : null}
|
|
|
|
|
|
|
|
|
|
<div className="alert alert-warning small mb-4">
|
|
|
|
|
Request Let's Encrypt certificates for your site domains. Requires certbot and nginx configured for the domain.
|
2026-04-07 02:04:22 +05:30
|
|
|
</div>
|
|
|
|
|
|
2026-04-07 11:42:19 +05:30
|
|
|
{diag ? (
|
|
|
|
|
<div className="alert alert-info small mb-4">
|
|
|
|
|
<div className="fw-semibold mb-2">HTTPS / nginx check</div>
|
|
|
|
|
{diag.hints.length ? (
|
|
|
|
|
<ul className="mb-3 ps-3">
|
|
|
|
|
{diag.hints.map((h, i) => (
|
|
|
|
|
<li key={i} className="mb-1">
|
|
|
|
|
{h}
|
|
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
|
|
|
|
) : null}
|
|
|
|
|
<div className="text-secondary mb-1">Include YakPanel vhosts inside the <code>http</code> block of the nginx process that serves your sites:</div>
|
|
|
|
|
<code className="d-block user-select-all bg-body-secondary p-2 rounded small text-break">{diag.include_snippet}</code>
|
|
|
|
|
{diag.vhosts.length > 0 ? (
|
|
|
|
|
<div className="mt-2 text-secondary">
|
|
|
|
|
Panel configs scanned:{' '}
|
|
|
|
|
{diag.vhosts.map((v) => `${v.file}${v.has_listen_443 && v.has_ssl_directives ? ' (HTTPS block)' : ''}`).join(', ')}
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
{diag.nginx_t_probe_errors.length > 0 ? (
|
|
|
|
|
<div className="mt-2 small text-danger">
|
|
|
|
|
nginx -T probe: {diag.nginx_t_probe_errors.join(' | ')}
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
|
2026-04-07 05:05:28 +05:30
|
|
|
<div className="row g-4">
|
|
|
|
|
<div className="col-lg-6">
|
|
|
|
|
<div className="card h-100">
|
|
|
|
|
<div className="card-header">Domains (from sites)</div>
|
|
|
|
|
<div className="list-group list-group-flush overflow-auto" style={{ maxHeight: '20rem' }}>
|
|
|
|
|
{domains.length === 0 ? (
|
|
|
|
|
<div className="list-group-item text-secondary text-center py-4">No domains. Add a site first.</div>
|
|
|
|
|
) : (
|
|
|
|
|
domains.map((d) => (
|
|
|
|
|
<div
|
|
|
|
|
key={d.id}
|
|
|
|
|
className="list-group-item d-flex align-items-center justify-content-between gap-2 flex-wrap"
|
|
|
|
|
>
|
|
|
|
|
<div className="small">
|
|
|
|
|
<span className="font-monospace">{d.name}</span>
|
|
|
|
|
{d.port !== '80' ? <span className="text-secondary ms-1">:{d.port}</span> : null}
|
|
|
|
|
<span className="text-secondary ms-2">({d.site_name})</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
{hasCert(d.name) ? (
|
|
|
|
|
<span className="text-success small">
|
|
|
|
|
<i className="ti ti-shield-check me-1" aria-hidden />
|
|
|
|
|
Cert
|
|
|
|
|
</span>
|
|
|
|
|
) : (
|
|
|
|
|
<AdminButton
|
|
|
|
|
variant="outline-primary"
|
|
|
|
|
size="sm"
|
|
|
|
|
disabled={!!requesting}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setRequestDomain(d)
|
|
|
|
|
setRequestEmail('')
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{requesting === d.name ? (
|
|
|
|
|
<span className="spinner-border spinner-border-sm" role="status" />
|
|
|
|
|
) : (
|
|
|
|
|
'Request SSL'
|
|
|
|
|
)}
|
|
|
|
|
</AdminButton>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2026-04-07 02:04:22 +05:30
|
|
|
</div>
|
2026-04-07 05:05:28 +05:30
|
|
|
))
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2026-04-07 02:04:22 +05:30
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-04-07 05:05:28 +05:30
|
|
|
<div className="col-lg-6">
|
|
|
|
|
<div className="card h-100">
|
|
|
|
|
<div className="card-header">Certificates</div>
|
|
|
|
|
<div className="list-group list-group-flush overflow-auto" style={{ maxHeight: '20rem' }}>
|
|
|
|
|
{certificates.length === 0 ? (
|
|
|
|
|
<div className="list-group-item text-secondary text-center py-4">No certificates yet</div>
|
|
|
|
|
) : (
|
|
|
|
|
certificates.map((c) => (
|
|
|
|
|
<div key={c.name} className="list-group-item d-flex align-items-center gap-2">
|
|
|
|
|
<i className="ti ti-shield-check text-success flex-shrink-0" aria-hidden />
|
|
|
|
|
<span className="font-monospace small text-break">{c.name}</span>
|
|
|
|
|
</div>
|
|
|
|
|
))
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2026-04-07 02:04:22 +05:30
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-04-07 05:05:28 +05:30
|
|
|
<Modal show={!!requestDomain} onHide={() => setRequestDomain(null)} centered>
|
|
|
|
|
<Modal.Header closeButton>
|
|
|
|
|
<Modal.Title>Request SSL for {requestDomain?.name}</Modal.Title>
|
|
|
|
|
</Modal.Header>
|
|
|
|
|
{requestDomain ? (
|
|
|
|
|
<form onSubmit={handleRequestCert}>
|
|
|
|
|
<Modal.Body>
|
|
|
|
|
<div className="mb-3">
|
|
|
|
|
<label className="form-label">Domain</label>
|
|
|
|
|
<input type="text" value={requestDomain.name} readOnly className="form-control-plaintext border rounded px-3 py-2 bg-body-secondary" />
|
2026-04-07 02:04:22 +05:30
|
|
|
</div>
|
2026-04-07 05:05:28 +05:30
|
|
|
<div className="mb-3">
|
|
|
|
|
<label className="form-label">Webroot (site path)</label>
|
2026-04-07 02:04:22 +05:30
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
value={requestDomain.site_path}
|
|
|
|
|
readOnly
|
2026-04-07 05:05:28 +05:30
|
|
|
className="form-control-plaintext border rounded px-3 py-2 bg-body-secondary"
|
2026-04-07 02:04:22 +05:30
|
|
|
/>
|
|
|
|
|
</div>
|
2026-04-07 05:05:28 +05:30
|
|
|
<div className="mb-0">
|
|
|
|
|
<label className="form-label">Email (for Let's Encrypt)</label>
|
2026-04-07 02:04:22 +05:30
|
|
|
<input
|
|
|
|
|
type="email"
|
|
|
|
|
value={requestEmail}
|
|
|
|
|
onChange={(e) => setRequestEmail(e.target.value)}
|
|
|
|
|
placeholder="admin@example.com"
|
2026-04-07 05:05:28 +05:30
|
|
|
className="form-control"
|
2026-04-07 02:04:22 +05:30
|
|
|
required
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2026-04-07 05:05:28 +05:30
|
|
|
</Modal.Body>
|
|
|
|
|
<Modal.Footer>
|
|
|
|
|
<AdminButton type="button" variant="secondary" onClick={() => setRequestDomain(null)}>
|
|
|
|
|
Cancel
|
|
|
|
|
</AdminButton>
|
|
|
|
|
<AdminButton type="submit" variant="primary" disabled={!!requesting}>
|
|
|
|
|
{requesting ? 'Requesting…' : 'Request'}
|
|
|
|
|
</AdminButton>
|
|
|
|
|
</Modal.Footer>
|
|
|
|
|
</form>
|
|
|
|
|
) : null}
|
|
|
|
|
</Modal>
|
|
|
|
|
</>
|
2026-04-07 02:04:22 +05:30
|
|
|
)
|
|
|
|
|
}
|