feat: auth, site creation with PHP version selection
This commit is contained in:
+117
-93
@@ -1,8 +1,10 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/panelhosting/panel/internal/middleware"
|
||||
"github.com/panelhosting/panel/internal/setup"
|
||||
)
|
||||
|
||||
@@ -12,107 +14,129 @@ func Health(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = w.Write([]byte(`{"status":"ok"}`))
|
||||
}
|
||||
|
||||
func IndexPage(svc *setup.Service) http.HandlerFunc {
|
||||
func IndexPage(setupSvc *setup.Service) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
required, err := svc.Status(r.Context())
|
||||
required, err := setupSvc.Status(r.Context())
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "internal error")
|
||||
return
|
||||
}
|
||||
index(required)(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func index(setupRequired bool) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if setupRequired {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(setupPageHTML))
|
||||
if required {
|
||||
serveHTML(w, setupPageHTML)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{
|
||||
"name": "PanelHosting",
|
||||
"version": "0.1.0",
|
||||
"status": "running",
|
||||
})
|
||||
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; }
|
||||
`
|
||||
|
||||
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>
|
||||
* { box-sizing: border-box; }
|
||||
body { font-family: system-ui, sans-serif; background: #0f172a; color: #e2e8f0; margin: 0; min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 1rem; }
|
||||
.card { background: #1e293b; border-radius: 12px; padding: 2rem; width: 100%; max-width: 420px; box-shadow: 0 20px 40px rgba(0,0,0,.3); }
|
||||
h1 { margin: 0 0 .5rem; font-size: 1.5rem; }
|
||||
p { color: #94a3b8; margin: 0 0 1.5rem; font-size: .95rem; }
|
||||
label { display: block; margin-bottom: .35rem; font-size: .85rem; color: #cbd5e1; }
|
||||
input { width: 100%; padding: .65rem .75rem; margin-bottom: 1rem; border: 1px solid #334155; border-radius: 8px; background: #0f172a; color: #f8fafc; font-size: 1rem; }
|
||||
input:focus { outline: 2px solid #3b82f6; border-color: transparent; }
|
||||
button { width: 100%; padding: .75rem; background: #3b82f6; color: #fff; border: none; border-radius: 8px; font-size: 1rem; font-weight: 600; cursor: pointer; }
|
||||
button:hover { background: #2563eb; }
|
||||
button:disabled { opacity: .6; cursor: not-allowed; }
|
||||
.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; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>PanelHosting</h1>
|
||||
<p>Создайте учётную запись суперадминистратора</p>
|
||||
<form id="form">
|
||||
<label for="email">Email</label>
|
||||
<input id="email" name="email" type="email" required autocomplete="email">
|
||||
<label for="username">Логин</label>
|
||||
<input id="username" name="username" type="text" required pattern="[a-zA-Z0-9_]{3,64}" autocomplete="username">
|
||||
<label for="password">Пароль (мин. 8 символов)</label>
|
||||
<input id="password" name="password" type="password" required minlength="8" autocomplete="new-password">
|
||||
<button type="submit" id="btn">Создать администратора</button>
|
||||
</form>
|
||||
<div id="msg" class="msg"></div>
|
||||
</div>
|
||||
<script>
|
||||
const form = document.getElementById('form');
|
||||
const msg = document.getElementById('msg');
|
||||
const btn = document.getElementById('btn');
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
btn.disabled = true;
|
||||
msg.className = 'msg';
|
||||
msg.textContent = '';
|
||||
const body = {
|
||||
email: form.email.value.trim(),
|
||||
username: form.username.value.trim(),
|
||||
password: form.password.value
|
||||
};
|
||||
try {
|
||||
const res = await fetch('/api/v1/setup/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
msg.className = 'msg error';
|
||||
msg.textContent = data.error || 'Ошибка регистрации';
|
||||
btn.disabled = false;
|
||||
return;
|
||||
}
|
||||
msg.className = 'msg ok';
|
||||
msg.textContent = 'Администратор создан. Панель готова к работе.';
|
||||
form.reset();
|
||||
} catch {
|
||||
msg.className = 'msg error';
|
||||
msg.textContent = 'Сетевая ошибка';
|
||||
btn.disabled = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</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');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: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>`
|
||||
|
||||
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>
|
||||
<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>
|
||||
<tbody id="sites"></tbody></table></div>
|
||||
</div>
|
||||
<script>
|
||||
async function loadPHP(){const r=await fetch('/api/v1/php-versions');const d=await r.json();const s=document.getElementById('php_version');s.innerHTML='';
|
||||
(d.versions||[]).forEach(v=>{const o=document.createElement('option');o.value=v;o.textContent='PHP '+v;s.appendChild(o);});}
|
||||
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>';
|
||||
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');btn.disabled=true;msg.className='msg';
|
||||
const res=await fetch('/api/v1/sites',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({name:name.value.trim().toLowerCase(),domain:domain.value.trim().toLowerCase(),php_version:php_version.value})});
|
||||
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='Сайт создан';siteForm.reset();btn.disabled=false;await loadPHP();loadSites();};
|
||||
loadPHP();loadSites();
|
||||
</script></body></html>`, baseCSS, username)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user