Template
feat: Let's Encrypt SSL on site creation
This commit is contained in:
@@ -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))
|
||||
}
|
||||
@@ -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='Сайт создан';
|
||||
|
||||
@@ -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"})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user