164 lines
10 KiB
Go
164 lines
10 KiB
Go
package handler
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/panelhosting/panel/internal/middleware"
|
|
"github.com/panelhosting/panel/internal/setup"
|
|
)
|
|
|
|
func Health(w http.ResponseWriter, _ *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(`{"status":"ok"}`))
|
|
}
|
|
|
|
func IndexPage(setupSvc *setup.Service) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
required, err := setupSvc.Status(r.Context())
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "internal error")
|
|
return
|
|
}
|
|
if required {
|
|
serveHTML(w, setupPageHTML)
|
|
return
|
|
}
|
|
if user, ok := middleware.UserFromContext(r.Context()); ok {
|
|
serveHTML(w, dashboardPageHTML(user.Username))
|
|
return
|
|
}
|
|
serveHTML(w, loginPageHTML)
|
|
}
|
|
}
|
|
|
|
func serveHTML(w http.ResponseWriter, html string) {
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(html))
|
|
}
|
|
|
|
const baseCSS = `
|
|
* { box-sizing: border-box; }
|
|
body { font-family: system-ui, sans-serif; background: #0f172a; color: #e2e8f0; margin: 0; min-height: 100vh; }
|
|
a { color: #60a5fa; }
|
|
.wrap { max-width: 960px; margin: 0 auto; padding: 1.5rem; }
|
|
.card { background: #1e293b; border-radius: 12px; padding: 1.5rem; box-shadow: 0 12px 32px rgba(0,0,0,.25); margin-bottom: 1rem; }
|
|
h1 { margin: 0 0 .5rem; font-size: 1.5rem; }
|
|
h2 { margin: 0 0 1rem; font-size: 1.1rem; color: #cbd5e1; }
|
|
p.sub { color: #94a3b8; margin: 0 0 1.25rem; }
|
|
label { display: block; margin-bottom: .35rem; font-size: .85rem; color: #cbd5e1; }
|
|
input, select { width: 100%; padding: .65rem .75rem; margin-bottom: 1rem; border: 1px solid #334155; border-radius: 8px; background: #0f172a; color: #f8fafc; font-size: 1rem; }
|
|
input:focus, select:focus { outline: 2px solid #3b82f6; border-color: transparent; }
|
|
button { padding: .65rem 1rem; background: #3b82f6; color: #fff; border: none; border-radius: 8px; font-size: .95rem; font-weight: 600; cursor: pointer; }
|
|
button:hover { background: #2563eb; }
|
|
button:disabled { opacity: .6; cursor: not-allowed; }
|
|
button.secondary { background: #334155; }
|
|
button.secondary:hover { background: #475569; }
|
|
.row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
|
|
@media (max-width: 640px) { .row { grid-template-columns: 1fr; } }
|
|
.msg { margin-top: 1rem; padding: .75rem; border-radius: 8px; font-size: .9rem; display: none; }
|
|
.msg.error { display: block; background: #7f1d1d; color: #fecaca; }
|
|
.msg.ok { display: block; background: #14532d; color: #bbf7d0; }
|
|
.topbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; }
|
|
table { width: 100%; border-collapse: collapse; font-size: .9rem; }
|
|
th, td { text-align: left; padding: .6rem .5rem; border-bottom: 1px solid #334155; }
|
|
th { color: #94a3b8; font-weight: 500; }
|
|
.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-none { background: #334155; color: #cbd5e1; }
|
|
.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>
|
|
<html lang="ru"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>PanelHosting — настройка</title><style>` + baseCSS + `</style></head>
|
|
<body><div class="center-card"><div class="card">
|
|
<h1>PanelHosting</h1><p class="sub">Создайте суперадминистратора</p>
|
|
<form id="form">
|
|
<label>Email</label><input id="email" type="email" required>
|
|
<label>Логин</label><input id="username" type="text" required pattern="[a-zA-Z0-9_]{3,64}">
|
|
<label>Пароль</label><input id="password" type="password" required minlength="8">
|
|
<button type="submit" id="btn">Создать</button>
|
|
</form><div id="msg" class="msg"></div>
|
|
</div></div>
|
|
<script>
|
|
document.getElementById('form').onsubmit=async e=>{e.preventDefault();const btn=document.getElementById('btn'),msg=document.getElementById('msg');btn.disabled=true;msg.className='msg';
|
|
const res=await fetch('/api/v1/setup/register',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({email:email.value.trim(),username:username.value.trim(),password:password.value})});
|
|
const d=await res.json().catch(()=>({}));if(!res.ok){msg.className='msg error';msg.textContent=d.error||'Ошибка';btn.disabled=false;return;}
|
|
location.reload();};
|
|
</script></body></html>`
|
|
|
|
const loginPageHTML = `<!DOCTYPE html>
|
|
<html lang="ru"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>PanelHosting — вход</title><style>` + baseCSS + `</style></head>
|
|
<body><div class="center-card"><div class="card">
|
|
<h1>PanelHosting</h1><p class="sub">Войдите в панель</p>
|
|
<form id="form">
|
|
<label>Логин</label><input id="username" type="text" required autocomplete="username">
|
|
<label>Пароль</label><input id="password" type="password" required autocomplete="current-password">
|
|
<button type="submit" id="btn">Войти</button>
|
|
</form><div id="msg" class="msg"></div>
|
|
</div></div>
|
|
<script>
|
|
document.getElementById('form').onsubmit=async e=>{e.preventDefault();const btn=document.getElementById('btn'),msg=document.getElementById('msg');const userEl=document.getElementById('username'),passEl=document.getElementById('password');btn.disabled=true;msg.className='msg';
|
|
const res=await fetch('/api/v1/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:userEl.value.trim(),password:passEl.value})});
|
|
const d=await res.json().catch(()=>({}));if(!res.ok){msg.className='msg error';msg.textContent=d.error||'Неверный логин или пароль';btn.disabled=false;return;}
|
|
location.reload();};
|
|
</script></body></html>`
|
|
|
|
func dashboardPageHTML(username string) string {
|
|
return fmt.Sprintf(`<!DOCTYPE html>
|
|
<html lang="ru"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>PanelHosting</title><style>%s</style></head>
|
|
<body><div class="wrap">
|
|
<div class="topbar"><div><h1>PanelHosting</h1><p class="sub">%s</p></div>
|
|
<button class="secondary" id="logout">Выйти</button></div>
|
|
<div class="card"><h2>Новый сайт</h2>
|
|
<form id="siteForm"><div class="row">
|
|
<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>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');const ssl=site.ssl_status||'none';
|
|
const sslCls=ssl==='active'?'ssl-active':(ssl==='error'?'ssl-error':(ssl==='pending'?'ssl-pending':'ssl-none'));
|
|
const needSSL=ssl!=='active';
|
|
const actions=needSSL?'<button class="secondary btn-ssl" type="button">Выпустить 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(needSSL){const btn=tr.querySelector('.btn-ssl');btn.onclick=async()=>{btn.disabled=true;btn.textContent='Выпускается...';const res=await fetch('/api/v1/sites/'+site.id+'/ssl/issue',{method:'POST'});if(!res.ok){const err=await res.json().catch(()=>({}));btn.textContent=err.error||'Ошибка';btn.disabled=false;return;}setTimeout(loadSites,1500);};}
|
|
tb.appendChild(tr);});}
|
|
document.getElementById('logout').onclick=async()=>{await fetch('/api/v1/auth/logout',{method:'POST'});location.reload();};
|
|
document.getElementById('siteForm').onsubmit=async e=>{
|
|
e.preventDefault();
|
|
const btn=document.getElementById('createBtn'),msg=document.getElementById('formMsg');
|
|
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,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='Сайт создан';
|
|
document.getElementById('siteForm').reset();
|
|
await loadPHP();loadSites();
|
|
}catch(err){msg.className='msg error';msg.textContent='Сетевая ошибка';}
|
|
btn.disabled=false;
|
|
};
|
|
loadPHP();loadSites();
|
|
</script></body></html>`, baseCSS, username)
|
|
}
|