feat: Let's Encrypt SSL on site creation

This commit is contained in:
orohi
2026-06-17 05:57:01 +03:00
parent 9b7eb99d20
commit 00d6789897
18 changed files with 703 additions and 51 deletions
+32
View File
@@ -0,0 +1,32 @@
package handler
import (
"net/http"
"github.com/panelhosting/panel/internal/ssl"
)
type ACMEHandler struct {
store *ssl.ChallengeStore
}
func NewACMEHandler(store *ssl.ChallengeStore) *ACMEHandler {
return &ACMEHandler{store: store}
}
func (h *ACMEHandler) Challenge(w http.ResponseWriter, r *http.Request) {
token := r.PathValue("token")
if token == "" {
http.NotFound(w, r)
return
}
keyAuth, ok := h.store.Get(token)
if !ok {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "text/plain")
_, _ = w.Write([]byte(keyAuth))
}
+12 -4
View File
@@ -68,6 +68,11 @@ const baseCSS = `
.badge { display: inline-block; padding: .15rem .5rem; border-radius: 999px; font-size: .75rem; background: #14532d; color: #bbf7d0; }
.center-card { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 1rem; }
.center-card .card { width: 100%; max-width: 420px; }
label.check { display: flex; align-items: center; gap: .5rem; margin-bottom: 1rem; font-size: .9rem; color: #cbd5e1; }
label.check input { width: auto; margin: 0; }
.badge.ssl-pending { background: #713f12; color: #fde68a; }
.badge.ssl-active { background: #14532d; color: #bbf7d0; }
.badge.ssl-error { background: #7f1d1d; color: #fecaca; }
`
const setupPageHTML = `<!DOCTYPE html>
@@ -119,17 +124,20 @@ func dashboardPageHTML(username string) string {
<div><label>Имя сайта</label><input id="name" required pattern="[a-z0-9][a-z0-9-]{1,62}[a-z0-9]" placeholder="mysite"></div>
<div><label>Домен</label><input id="domain" required placeholder="example.com"></div>
</div><label>PHP версия</label><select id="php_version" required></select>
<label class="check"><input type="checkbox" id="issue_ssl" checked> Выпустить SSL (Let's Encrypt)</label>
<button type="submit" id="createBtn">Создать сайт</button></form>
<div id="formMsg" class="msg"></div></div>
<div class="card"><h2>Сайты</h2>
<table><thead><tr><th>Имя</th><th>Домен</th><th>PHP</th><th>Статус</th><th>Путь</th></tr></thead>
<table><thead><tr><th>Имя</th><th>Домен</th><th>PHP</th><th>SSL</th><th>Статус</th><th>Путь</th><th></th></tr></thead>
<tbody id="sites"></tbody></table></div>
</div>
<script>
async function loadPHP(){const fallback=['8.1','8.2','8.3','8.4'];let versions=fallback;try{const r=await fetch('/api/v1/php-versions');const d=await r.json();if(r.ok&&d.versions&&d.versions.length)versions=d.versions;}catch(e){}const s=document.getElementById('php_version');s.innerHTML='';versions.forEach(v=>{const o=document.createElement('option');o.value=v;o.textContent='PHP '+v;s.appendChild(o);});if(versions.length)s.value=versions[versions.length-1];}
async function loadSites(){const r=await fetch('/api/v1/sites');const d=await r.json();const tb=document.getElementById('sites');tb.innerHTML='';
(d.sites||[]).forEach(site=>{const tr=document.createElement('tr');
tr.innerHTML='<td>'+site.name+'</td><td>'+site.primary_domain+'</td><td>'+(site.php_version||'-')+'</td><td><span class="badge">'+site.status+'</span></td><td><code>'+site.document_root+'</code></td>';
(d.sites||[]).forEach(site=>{const tr=document.createElement('tr');const ssl=site.ssl_status||'none';const sslCls=ssl==='active'?'ssl-active':(ssl==='error'?'ssl-error':'ssl-pending');
let actions='';if(ssl==='error'||ssl==='pending'){actions='<button class="secondary" data-id="'+site.id+'">SSL</button>';}
tr.innerHTML='<td>'+site.name+'</td><td>'+site.primary_domain+'</td><td>'+(site.php_version||'-')+'</td><td><span class="badge '+sslCls+'">'+ssl+'</span></td><td><span class="badge">'+site.status+'</span></td><td><code>'+site.document_root+'</code></td><td>'+actions+'</td>';
if(actions){tr.querySelector('button').onclick=async()=>{await fetch('/api/v1/sites/'+site.id+'/ssl/issue',{method:'POST'});setTimeout(loadSites,2000);};}
tb.appendChild(tr);});}
document.getElementById('logout').onclick=async()=>{await fetch('/api/v1/auth/logout',{method:'POST'});location.reload();};
document.getElementById('siteForm').onsubmit=async e=>{
@@ -138,7 +146,7 @@ const btn=document.getElementById('createBtn'),msg=document.getElementById('form
const siteName=document.getElementById('name'),siteDomain=document.getElementById('domain'),phpSelect=document.getElementById('php_version');
btn.disabled=true;msg.className='msg';msg.textContent='';
try{
const res=await fetch('/api/v1/sites',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:siteName.value.trim().toLowerCase(),domain:siteDomain.value.trim().toLowerCase(),php_version:phpSelect.value})});
const res=await fetch('/api/v1/sites',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:siteName.value.trim().toLowerCase(),domain:siteDomain.value.trim().toLowerCase(),php_version:phpSelect.value,issue_ssl:document.getElementById('issue_ssl').checked})});
const d=await res.json().catch(()=>({}));
if(!res.ok){msg.className='msg error';msg.textContent=d.error||'Ошибка';btn.disabled=false;return;}
msg.className='msg ok';msg.textContent='Сайт создан';
+25
View File
@@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"net/http"
"strconv"
"github.com/panelhosting/panel/internal/middleware"
"github.com/panelhosting/panel/internal/sitesvc"
@@ -22,6 +23,7 @@ type createSiteRequest struct {
Domain string `json:"domain"`
PHPVersion string `json:"php_version"`
ServerID int64 `json:"server_id,omitempty"`
IssueSSL *bool `json:"issue_ssl"`
}
func (h *SiteHandler) List(w http.ResponseWriter, r *http.Request) {
@@ -52,6 +54,11 @@ func (h *SiteHandler) Create(w http.ResponseWriter, r *http.Request) {
return
}
issueSSL := true
if req.IssueSSL != nil {
issueSSL = *req.IssueSSL
}
site, err := h.svc.Create(r.Context(), sitesvc.CreateInput{
Name: req.Name,
Domain: req.Domain,
@@ -59,6 +66,7 @@ func (h *SiteHandler) Create(w http.ResponseWriter, r *http.Request) {
ServerID: req.ServerID,
OwnerID: user.ID,
IsAdmin: middleware.IsAdmin(user),
IssueSSL: issueSSL,
})
if err != nil {
switch {
@@ -83,3 +91,20 @@ func (h *SiteHandler) PHPVersions(w http.ResponseWriter, r *http.Request) {
}
writeJSON(w, http.StatusOK, map[string]any{"versions": versions})
}
func (h *SiteHandler) ReissueSSL(w http.ResponseWriter, r *http.Request) {
_, ok := middleware.UserFromContext(r.Context())
if !ok {
writeError(w, http.StatusUnauthorized, "unauthorized")
return
}
siteID, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
if err != nil {
writeError(w, http.StatusBadRequest, "invalid site id")
return
}
h.svc.ReissueSSLAsync(siteID)
writeJSON(w, http.StatusOK, map[string]string{"message": "ssl issuance started"})
}