Files
yakpanel-core/YakPanel-server/frontend/src/pages/DomainsPage.tsx
2026-04-07 12:00:10 +05:30

275 lines
10 KiB
TypeScript

import { useEffect, useState } from 'react'
import Modal from 'react-bootstrap/Modal'
import { apiRequest } from '../api/client'
import { PageHeader, AdminButton, AdminAlert } from '../components/admin'
interface Domain {
id: number
name: string
port: string
site_id: number
site_name: string
site_path: string
}
interface Certificate {
name: string
path: string
}
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[]
/** Something accepted TCP when connecting to 127.0.0.1:443 from the panel process */
localhost_443_accepts_tcp: boolean
/** ss/netstat reported :443 in listen table, or null if probe unavailable */
ss_reports_443_listen: boolean | null
hints: string[]
}
export function DomainsPage() {
const [domains, setDomains] = useState<Domain[]>([])
const [certificates, setCertificates] = useState<Certificate[]>([])
const [diag, setDiag] = useState<SslDiagnostics | null>(null)
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'),
apiRequest<SslDiagnostics>('/ssl/diagnostics').catch(() => null),
])
.then(([d, c, di]) => {
setDomains(d)
setCertificates(c.certificates || [])
setDiag(di)
})
.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 + ' '))
if (loading) {
return (
<>
<PageHeader title="Domains & SSL" />
<p className="text-secondary">Loading</p>
</>
)
}
return (
<>
<PageHeader title="Domains & SSL" />
{error ? <AdminAlert className="mb-3">{error}</AdminAlert> : null}
<div className="alert alert-warning small mb-4">
Request Let&apos;s Encrypt certificates for your site domains. Requires certbot and nginx configured for the domain.
</div>
{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>
<div className="mt-3 small">
<strong>Port 443 on this machine</strong>
<ul className="mb-0 ps-3 mt-1">
<li>
Localhost TCP (127.0.0.1:443):{' '}
{diag.localhost_443_accepts_tcp ? (
<span className="text-success">accepting connections</span>
) : (
<span className="text-danger">refused / nothing listening</span>
)}
</li>
<li>
Kernel listen table (ss):{' '}
{diag.ss_reports_443_listen === null ? (
<span className="text-secondary">not checked</span>
) : diag.ss_reports_443_listen ? (
<span className="text-success">443 in LISTEN</span>
) : (
<span className="text-danger">no 443 in LISTEN</span>
)}
</li>
</ul>
<p className="text-secondary mt-2 mb-0">
The panel cannot see your cloud provider firewall. If localhost shows open but browsers off-network fail, open TCP 443 in the VPS control panel (security group) and OS firewall.
</p>
</div>
{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}
<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>
</div>
))
)}
</div>
</div>
</div>
<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>
</div>
</div>
</div>
<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" />
</div>
<div className="mb-3">
<label className="form-label">Webroot (site path)</label>
<input
type="text"
value={requestDomain.site_path}
readOnly
className="form-control-plaintext border rounded px-3 py-2 bg-body-secondary"
/>
</div>
<div className="mb-0">
<label className="form-label">Email (for Let&apos;s Encrypt)</label>
<input
type="email"
value={requestEmail}
onChange={(e) => setRequestEmail(e.target.value)}
placeholder="admin@example.com"
className="form-control"
required
/>
</div>
</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>
</>
)
}