new changes

This commit is contained in:
Niranjan
2026-04-07 14:09:55 +05:30
parent 18777560d5
commit 8fe63c7cf4
4 changed files with 529 additions and 4 deletions

View File

@@ -546,6 +546,19 @@ export async function getDashboardStats() {
}>('/dashboard/stats')
}
export async function applySiteSsl(data: {
site_id: number
domains: string[]
method: 'file' | 'dns_cloudflare'
email: string
api_token?: string
}) {
return apiRequest<{ status: boolean; msg: string; output?: string }>('/ssl/site-apply', {
method: 'POST',
body: JSON.stringify(data),
})
}
export async function getFirewallBackendStatus() {
return apiRequest<{
ufw: { detected: boolean; active: boolean | null; summary_line: string }

View File

@@ -20,6 +20,7 @@ import {
siteGitClone,
siteGitPull,
listServices,
applySiteSsl,
type SiteListItem,
} from '../api/client'
import { PageHeader, AdminButton, AdminAlert, AdminTable, EmptyState } from '../components/admin'
@@ -29,6 +30,25 @@ function formatPhpVersion(code: string): string {
return m[code] || code
}
/** Domain column may be `host` or `host:port` (match API list). */
function hostFromDomainEntry(entry: string): string {
return (entry || '').split(':')[0].trim()
}
function uniqueHostsFromSiteDomains(domains: string[] | undefined): string[] {
const seen = new Set<string>()
const out: string[] = []
for (const entry of domains || []) {
const h = hostFromDomainEntry(entry)
const k = h.toLowerCase()
if (k && !seen.has(k)) {
seen.add(k)
out.push(h)
}
}
return out
}
export function SitePage() {
const [sites, setSites] = useState<SiteListItem[]>([])
const [loading, setLoading] = useState(true)
@@ -73,6 +93,63 @@ export function SitePage() {
const [nginxStatus, setNginxStatus] = useState<string | null>(null)
const [batchLoading, setBatchLoading] = useState(false)
const [rowBackupId, setRowBackupId] = useState<number | null>(null)
const [sslSite, setSslSite] = useState<SiteListItem | null>(null)
const [sslTab, setSslTab] = useState<'current' | 'letsencrypt'>('letsencrypt')
const [sslMethod, setSslMethod] = useState<'file' | 'dns_cloudflare'>('file')
const [sslEmail, setSslEmail] = useState('')
const [sslCfToken, setSslCfToken] = useState('')
const [sslSelectedHosts, setSslSelectedHosts] = useState<Set<string>>(() => new Set())
const [sslBusy, setSslBusy] = useState(false)
const [sslError, setSslError] = useState('')
const [sslSuccess, setSslSuccess] = useState('')
const sslHostOptions = useMemo(() => (sslSite ? uniqueHostsFromSiteDomains(sslSite.domains) : []), [sslSite])
const openSslModal = (s: SiteListItem) => {
setSslSite(s)
setSslTab('letsencrypt')
setSslMethod('file')
setSslEmail('')
setSslCfToken('')
setSslSelectedHosts(new Set(uniqueHostsFromSiteDomains(s.domains)))
setSslError('')
setSslSuccess('')
setError('')
}
const toggleSslHost = (host: string, checked: boolean) => {
setSslSelectedHosts((prev) => {
const n = new Set(prev)
if (checked) n.add(host)
else n.delete(host)
return n
})
}
const sslSelectAll = (on: boolean) => {
setSslSelectedHosts(on ? new Set(sslHostOptions) : new Set())
}
const handleSiteSslApply = (e: React.FormEvent) => {
e.preventDefault()
if (!sslSite || sslSelectedHosts.size === 0 || !sslEmail.trim()) return
setSslBusy(true)
setSslError('')
setSslSuccess('')
applySiteSsl({
site_id: sslSite.id,
domains: [...sslSelectedHosts],
method: sslMethod,
email: sslEmail.trim(),
api_token: sslMethod === 'dns_cloudflare' ? sslCfToken.trim() : undefined,
})
.then((r) => {
setSslSuccess(r.msg || 'Certificate request completed.')
loadSites()
})
.catch((err) => setSslError(err.message))
.finally(() => setSslBusy(false))
}
const loadSites = () => {
setLoading(true)
@@ -671,9 +748,13 @@ export function SitePage() {
<i className="ti ti-file-text me-2" />
Logs
</Dropdown.Item>
<Dropdown.Item as={Link} to="/ssl_domain">
<Dropdown.Item onClick={() => openSslModal(s)}>
<i className="ti ti-shield-lock me-2" />
SSL / Domains
SSL (this site)
</Dropdown.Item>
<Dropdown.Item as={Link} to="/ssl_domain">
<i className="ti ti-world-www me-2" />
Domains &amp; diagnostics
</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item
@@ -1015,6 +1096,225 @@ export function SitePage() {
) : null}
</Modal>
<Modal
show={sslSite != null}
onHide={() => {
setSslSite(null)
setSslError('')
setSslSuccess('')
}}
size="lg"
centered
scrollable
>
<Modal.Header closeButton>
<Modal.Title className="d-flex align-items-center gap-2">
<i className="ti ti-shield-lock" aria-hidden />
SSL {sslSite?.name}
</Modal.Title>
</Modal.Header>
<Modal.Body>
{sslSite ? (
<>
<ul className="nav nav-tabs mb-3">
<li className="nav-item">
<button
type="button"
className={`nav-link ${sslTab === 'current' ? 'active' : ''}`}
onClick={() => setSslTab('current')}
>
Current certificate
</button>
</li>
<li className="nav-item">
<button
type="button"
className={`nav-link ${sslTab === 'letsencrypt' ? 'active' : ''}`}
onClick={() => setSslTab('letsencrypt')}
>
Let&apos;s Encrypt
</button>
</li>
</ul>
{sslTab === 'current' ? (
<div>
{(sslSite.ssl?.status ?? 'none') === 'none' ? (
<div className="alert alert-warning">
This site has no matching Let&apos;s Encrypt certificate detected on the server. Use the Let&apos;s
Encrypt tab to issue one, or open Domains for global checks.
</div>
) : sslSite.ssl?.status === 'expired' ? (
<div className="alert alert-danger">
Certificate appears <strong>expired</strong> for monitored hostnames. Renew from the Let&apos;s
Encrypt tab.
</div>
) : sslSite.ssl?.status === 'expiring' ? (
<div className="alert alert-warning">
Certificate expires in <strong>{sslSite.ssl?.days_left ?? 0}</strong> days consider renewing soon.
</div>
) : (
<div className="alert alert-success mb-0">
Certificate looks active (~{sslSite.ssl?.days_left ?? '—'} days left for matched hostname
{sslSite.ssl?.cert_name ? (
<>
: <code>{sslSite.ssl.cert_name}</code>
</>
) : null}
).
</div>
)}
<p className="small text-muted mt-3 mb-0">
<Link to="/ssl_domain">Open Domains &amp; SSL</Link> for nginx include wizard, DNS-01 helpers, and all
certificates on the server.
</p>
</div>
) : (
<form onSubmit={handleSiteSslApply}>
{(sslSite.ssl?.status ?? 'none') !== 'active' ? (
<div className="alert alert-danger d-flex flex-wrap align-items-center justify-content-between gap-2">
<span>
<strong>Tip:</strong> HTTPS is not fully active for this site&apos;s domains visitors may see
browser warnings until a certificate is deployed.
</span>
<span className="badge bg-success text-wrap">Use the form below to apply</span>
</div>
) : (
<div className="alert alert-secondary small mb-3">
A certificate is already detected as active; you can still re-issue to add names or renew early.
</div>
)}
<div className="mb-3">
<div className="fw-semibold mb-2">Validation</div>
<div className="form-check">
<input
className="form-check-input"
type="radio"
name="sslVal"
id="ssl-file"
checked={sslMethod === 'file'}
onChange={() => setSslMethod('file')}
/>
<label className="form-check-label" htmlFor="ssl-file">
File verification (HTTP-01, webroot on this site)
</label>
</div>
<div className="form-check">
<input
className="form-check-input"
type="radio"
name="sslVal"
id="ssl-dns"
checked={sslMethod === 'dns_cloudflare'}
onChange={() => setSslMethod('dns_cloudflare')}
/>
<label className="form-check-label" htmlFor="ssl-dns">
DNS verification Cloudflare (works behind CDN / orange-cloud)
</label>
</div>
</div>
<div className="mb-3">
<div className="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-2">
<label className="fw-semibold mb-0">Domains on this site</label>
{sslHostOptions.length > 1 ? (
<button
type="button"
className="btn btn-link btn-sm py-0"
onClick={() => sslSelectAll(sslSelectedHosts.size < sslHostOptions.length)}
>
{sslSelectedHosts.size < sslHostOptions.length ? 'Select all' : 'Clear all'}
</button>
) : null}
</div>
{sslHostOptions.length === 0 ? (
<p className="text-danger small mb-0">No domains on this site add domains in site settings first.</p>
) : (
<div className="border rounded p-3 bg-body-secondary bg-opacity-25">
{sslHostOptions.map((h) => (
<div className="form-check" key={h}>
<input
className="form-check-input"
type="checkbox"
id={`ssl-host-${h}`}
checked={sslSelectedHosts.has(h)}
onChange={(ev) => toggleSslHost(h, ev.target.checked)}
/>
<label className="form-check-label" htmlFor={`ssl-host-${h}`}>
{h}
</label>
</div>
))}
</div>
)}
</div>
<div className="mb-3">
<label className="form-label">Let&apos;s Encrypt email</label>
<input
type="email"
className="form-control"
value={sslEmail}
onChange={(e) => setSslEmail(e.target.value)}
placeholder="admin@example.com"
required
autoComplete="email"
/>
</div>
{sslMethod === 'dns_cloudflare' ? (
<div className="mb-3">
<label className="form-label">Cloudflare API token (DNS:Edit)</label>
<input
type="password"
className="form-control"
value={sslCfToken}
onChange={(e) => setSslCfToken(e.target.value)}
placeholder="API token"
autoComplete="off"
required
/>
</div>
) : null}
<ul className="small text-muted mb-3">
<li>
Confirm DNS A/AAAA records for every selected name point to this server before using file
verification.
</li>
<li>If you use a CDN or redirect, file validation may fail use Cloudflare DNS verification.</li>
<li>
Requires <code>certbot</code> on the server; DNS method also needs <code>certbot-dns-cloudflare</code>.
</li>
</ul>
{sslError ? <AdminAlert variant="danger">{sslError}</AdminAlert> : null}
{sslSuccess ? <AdminAlert variant="success">{sslSuccess}</AdminAlert> : null}
<div className="d-flex justify-content-end gap-2">
<button
type="button"
className="btn btn-light"
onClick={() => {
setSslSite(null)
setSslError('')
setSslSuccess('')
}}
>
Close
</button>
<button type="submit" className="btn btn-success" disabled={sslBusy || sslHostOptions.length === 0}>
{sslBusy ? 'Running certbot…' : 'Apply certificate'}
</button>
</div>
</form>
)}
</>
) : null}
</Modal.Body>
</Modal>
<Modal show={backupSiteId != null} onHide={() => setBackupSiteId(null)} size="lg" centered>
<Modal.Header closeButton>
<Modal.Title>Site Backup</Modal.Title>