feat: ent ORM, admin UI, client auth, Fyne GUI, Windows/MSI packaging
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"html/template"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:embed templates/*.html
|
||||
var templateFS embed.FS
|
||||
|
||||
var tmpl *template.Template
|
||||
|
||||
func init() {
|
||||
funcMap := template.FuncMap{
|
||||
"lower": strings.ToLower,
|
||||
}
|
||||
tmpl = template.Must(template.New("").Funcs(funcMap).ParseFS(templateFS, "templates/*.html"))
|
||||
}
|
||||
|
||||
func renderTemplate(w io.Writer, name string, data map[string]any) error {
|
||||
return tmpl.ExecuteTemplate(w, name, data)
|
||||
}
|
||||
|
||||
func renderToString(name string, data map[string]any) string {
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.ExecuteTemplate(&buf, name, data); err != nil {
|
||||
return "<p>Error rendering template</p>"
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func renderLayout(w io.Writer, title string, content template.HTML) error {
|
||||
return tmpl.ExecuteTemplate(w, "layout.html", map[string]any{
|
||||
"Title": title,
|
||||
"Content": content,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<div class="toolbar">
|
||||
<h1>Client: {{.Client.Name}}</h1>
|
||||
<div class="spacer"></div>
|
||||
<a href="/admin/clients/{{.Client.ID}}/provision" class="btn btn-sm" hx-get="/admin/clients/{{.Client.ID}}/provision" hx-target="#main-content" hx-push-url="true">Provision</a>
|
||||
<a href="/admin/clients" class="btn btn-sm btn-outline" hx-get="/admin/clients" hx-target="#main-content" hx-push-url="true">Back</a>
|
||||
</div>
|
||||
<div class="grid-2 mb-16">
|
||||
<div class="card">
|
||||
<h3>Details</h3>
|
||||
<table style="margin-top:8px">
|
||||
<tbody>
|
||||
<tr><td style="width:120px;color:var(--fg2);font-size:12px">Status</td><td>{{if eq .Client.Status "online"}}<span style="font-weight:600">Online</span>{{else}}<span style="color:var(--fg2)">Offline</span>{{end}}</td></tr>
|
||||
<tr><td style="color:var(--fg2);font-size:12px">Address</td><td class="text-mono">{{if .Client.Addr}}{{.Client.Addr}}:{{.Client.Port}}{{else}}—{{end}}</td></tr>
|
||||
<tr><td style="color:var(--fg2);font-size:12px">Auth Key</td><td class="text-mono">{{.Client.Key}}</td></tr>
|
||||
<tr><td style="color:var(--fg2);font-size:12px">Created</td><td>{{.Client.CreatedAt}}</td></tr>
|
||||
<tr><td style="color:var(--fg2);font-size:12px">Last Seen</td><td>{{if .Client.LastSeen}}{{.Client.LastSeen}}{{else}}—{{end}}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>One-Time Token</h3>
|
||||
<p style="font-size:12px;color:var(--fg2);margin-bottom:8px">Generate a one-time token for client auth:</p>
|
||||
<button class="btn btn-sm" hx-post="/admin/clients/{{.Client.ID}}/generate-token" hx-target="#token-result" hx-swap="innerHTML">Generate Token</button>
|
||||
<div id="token-result" style="margin-top:8px"></div>
|
||||
</div>
|
||||
</div>
|
||||
<h2 style="font-size:16px;font-weight:600;margin-bottom:12px">Proxies</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Name</th><th>Type</th><th>Local</th><th>Remote</th><th>Status</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Proxies}}
|
||||
<tr>
|
||||
<td><a href="/admin/proxies/{{.ID}}" hx-get="/admin/proxies/{{.ID}}" hx-target="#main-content" hx-push-url="true">{{.Name}}</a></td>
|
||||
<td style="color:var(--fg2)">{{.ProxyType}}</td>
|
||||
<td class="text-mono">{{.LocalIP}}:{{.LocalPort}}</td>
|
||||
<td class="text-mono">{{if .RemotePort}}{{.RemotePort}}{{else}}—{{end}}</td>
|
||||
<td>{{if eq .Status "active"}}<span style="font-weight:600">active</span>{{else}}<span style="color:var(--fg2)">{{.Status}}</span>{{end}}</td>
|
||||
<td><a href="/admin/proxies/{{.ID}}" class="btn btn-sm btn-outline" hx-get="/admin/proxies/{{.ID}}" hx-target="#main-content" hx-push-url="true">View</a></td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td colspan="6"><div class="empty">No proxies for this client. <a href="/admin/proxies/new?client_id={{$.Client.ID}}" hx-get="/admin/proxies/new?client_id={{$.Client.ID}}" hx-target="#main-content" hx-push-url="true">Create one</a>.</div></td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -0,0 +1,18 @@
|
||||
<div class="toolbar">
|
||||
<h1>New Client</h1>
|
||||
</div>
|
||||
|
||||
{{if .Error}}
|
||||
<div style="margin-bottom:16px;padding:12px;border:1px solid var(--border);font-size:13px">{{.Error}}</div>
|
||||
{{end}}
|
||||
|
||||
<form hx-post="/admin/clients/new" hx-target="body" hx-push-url="true" style="max-width:400px">
|
||||
<div style="margin-bottom:16px">
|
||||
<label style="display:block;font-size:12px;color:var(--fg2);margin-bottom:4px">Client Name</label>
|
||||
<input type="text" name="name" required autofocus placeholder="my-server">
|
||||
</div>
|
||||
<div class="toolbar">
|
||||
<button type="submit">Create Client</button>
|
||||
<a href="/admin/clients" class="btn btn-outline" hx-get="/admin/clients" hx-target="#main-content" hx-push-url="true">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,39 @@
|
||||
<div class="toolbar">
|
||||
<h1>Provision: {{.Client.Name}}</h1>
|
||||
<div class="spacer"></div>
|
||||
<a href="/admin/clients/{{.Client.ID}}" class="btn btn-sm btn-outline" hx-get="/admin/clients/{{.Client.ID}}" hx-target="#main-content" hx-push-url="true">Back</a>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid var(--border);padding:20px;margin-bottom:16px">
|
||||
<h2 style="font-size:12px;font-weight:600;letter-spacing:.3px;color:var(--fg2);margin-bottom:12px;text-transform:uppercase">Client Details</h2>
|
||||
<table>
|
||||
<tr><td style="width:120px;color:var(--fg2);font-size:12px">Name</td><td>{{.Client.Name}}</td></tr>
|
||||
<tr><td style="color:var(--fg2);font-size:12px">Key</td><td class="text-mono">{{.Client.Key}}</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid var(--border);padding:20px;margin-bottom:16px">
|
||||
<h2 style="font-size:12px;font-weight:600;letter-spacing:.3px;color:var(--fg2);margin-bottom:12px;text-transform:uppercase">One-Liner Setup</h2>
|
||||
<p style="font-size:12px;color:var(--fg2);margin-bottom:8px">Run this on your client machine. It downloads frpc and auto-updates from this server:</p>
|
||||
<pre class="text-mono" style="padding:12px;background:var(--bg2);border:1px solid var(--border);overflow-x:auto;white-space:pre-wrap;word-break:break-all;font-size:12px">curl -sL https://github.com/fatedier/frp/releases/latest/download/frp_linux_amd64.tar.gz | tar xz \
|
||||
&& ./frp_*/frpc --server-config http://{{.ServerAddr}}:{{.ServerPort}}/admin/api/frpc/proxy-config/{{.Client.Key}}</pre>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid var(--border);padding:20px;margin-bottom:16px">
|
||||
<h2 style="font-size:12px;font-weight:600;letter-spacing:.3px;color:var(--fg2);margin-bottom:12px;text-transform:uppercase">Download Install Script</h2>
|
||||
<p style="font-size:12px;color:var(--fg2);margin-bottom:8px">Download a standalone provisioning script:</p>
|
||||
<a href="/admin/clients/{{.Client.ID}}/provision.sh" class="btn btn-sm" download>Download provision-{{.Client.Name}}.sh</a>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid var(--border);padding:20px;margin-bottom:16px">
|
||||
<h2 style="font-size:12px;font-weight:600;letter-spacing:.3px;color:var(--fg2);margin-bottom:12px;text-transform:uppercase">Current Config</h2>
|
||||
<pre class="text-mono" style="padding:12px;background:var(--bg2);border:1px solid var(--border);overflow-x:auto;white-space:pre-wrap;font-size:12px">{{.FrpcConfig}}</pre>
|
||||
<div class="toolbar" style="margin-top:12px">
|
||||
<a href="/admin/clients/{{.Client.ID}}/config" class="btn btn-sm" download>Download frpc.toml</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid var(--border);padding:20px;margin-bottom:16px">
|
||||
<h2 style="font-size:12px;font-weight:600;letter-spacing:.3px;color:var(--fg2);margin-bottom:12px;text-transform:uppercase">Add Proxies</h2>
|
||||
<p style="font-size:13px;color:var(--fg2);margin-bottom:4px">Create proxies from the <a href="/admin/proxies/new?client_id={{.Client.ID}}" hx-get="/admin/proxies/new?client_id={{.Client.ID}}" hx-target="#main-content" hx-push-url="true">new proxy page</a>. The client will auto-reload within 30 seconds.</p>
|
||||
</div>
|
||||
@@ -0,0 +1,24 @@
|
||||
<div class="toolbar">
|
||||
<h1>Clients</h1>
|
||||
<div class="spacer"></div>
|
||||
<a href="/admin/clients/new" class="btn btn-sm" hx-get="/admin/clients/new" hx-target="#main-content" hx-push-url="true">New Client</a>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Name</th><th>Address</th><th>Status</th><th>Key</th><th>Last Seen</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Clients}}
|
||||
<tr>
|
||||
<td><a href="/admin/clients/{{.ID}}" hx-get="/admin/clients/{{.ID}}" hx-target="#main-content" hx-push-url="true">{{.Name}}</a></td>
|
||||
<td class="text-mono">{{if .Addr}}{{.Addr}}:{{.Port}}{{else}}—{{end}}</td>
|
||||
<td>{{if eq .Status "online"}}<span style="font-weight:600">online</span>{{else}}<span style="color:var(--fg2)">offline</span>{{end}}</td>
|
||||
<td class="text-mono" style="max-width:120px;overflow:hidden;text-overflow:ellipsis">{{.Key}}</td>
|
||||
<td>{{if .LastSeen}}{{.LastSeen}}{{else}}—{{end}}</td>
|
||||
<td><a href="/admin/clients/{{.ID}}/provision" class="btn btn-sm btn-outline" hx-get="/admin/clients/{{.ID}}/provision" hx-target="#main-content" hx-push-url="true">Provision</a></td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td colspan="6"><div class="empty">No clients registered. <a href="/admin/clients/new" hx-get="/admin/clients/new" hx-target="#main-content" hx-push-url="true">Create one</a>.</div></td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -0,0 +1,36 @@
|
||||
<h1>Dashboard</h1>
|
||||
<div class="grid-4">
|
||||
<div class="card">
|
||||
<h3>Active Proxies</h3>
|
||||
<div class="value">{{.ActiveProxies}}</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Clients</h3>
|
||||
<div class="value">{{.TotalClients}}</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Users</h3>
|
||||
<div class="value">{{.TotalUsers}}</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Uptime</h3>
|
||||
<div class="value" style="font-size:20px">{{.Uptime}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-2">
|
||||
<div class="card">
|
||||
<h3>Clients by Status</h3>
|
||||
<table style="margin-top:8px">
|
||||
<tr><td style="width:100px">Online</td><td>{{.OnlineClients}}</td></tr>
|
||||
<tr><td>Offline</td><td>{{.OfflineClients}}</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Proxies by Type</h3>
|
||||
<table style="margin-top:8px">
|
||||
{{range .ProxyBreakdown}}
|
||||
<tr><td style="width:100px;text-transform:capitalize">{{.Type}}</td><td>{{.Count}}</td></tr>
|
||||
{{end}}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,87 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>{{if .Title}}{{.Title}} — {{end}}frp Admin</title>
|
||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{--bg:#fff;--bg2:#f5f5f5;--fg:#000;--fg2:#888;--border:#ddd;--accent:#000;--font:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;--radius:0}
|
||||
@media(prefers-color-scheme:dark){
|
||||
:root{--bg:#000;--bg2:#111;--fg:#fff;--fg2:#666;--border:#333;--accent:#fff}
|
||||
}
|
||||
html,body{height:100%}
|
||||
body{font-family:var(--font);background:var(--bg);color:var(--fg);font-size:14px;line-height:1.5}
|
||||
a{color:var(--fg);text-decoration:underline}
|
||||
a:hover{text-decoration:none}
|
||||
table{width:100%;border-collapse:collapse}
|
||||
th,td{padding:10px 12px;text-align:left;border-bottom:1px solid var(--border);font-size:13px}
|
||||
th{font-weight:600;font-size:11px;letter-spacing:.3px;color:var(--fg2)}
|
||||
tr:hover td{background:var(--bg2)}
|
||||
input,select,textarea{padding:8px 12px;border:1px solid var(--border);background:var(--bg);color:var(--fg);font-size:13px;border-radius:var(--radius);width:100%;-webkit-appearance:none;appearance:none}
|
||||
input:focus,select:focus,textarea:focus{outline:1px solid var(--fg)}
|
||||
button,.btn{padding:8px 16px;border:1px solid var(--fg);background:var(--fg);color:var(--bg);font-size:13px;cursor:pointer;border-radius:var(--radius);font-family:var(--font)}
|
||||
button:hover,.btn:hover{opacity:.8}
|
||||
.btn-outline{background:transparent;color:var(--fg)}
|
||||
.btn-outline:hover{background:var(--fg);color:var(--bg)}
|
||||
.btn-sm{padding:4px 10px;font-size:12px}
|
||||
.card{background:var(--bg2);border:1px solid var(--border);padding:20px}
|
||||
.card h3{margin-bottom:8px;font-size:12px;font-weight:600;letter-spacing:.3px;color:var(--fg2)}
|
||||
.card .value{font-size:28px;font-weight:700;color:var(--fg)}
|
||||
.layout{display:flex;min-height:100vh}
|
||||
.sidebar{width:220px;background:var(--bg2);border-right:1px solid var(--border);padding:24px 0;flex-shrink:0}
|
||||
.sidebar .brand{padding:0 20px 24px;font-size:16px;font-weight:700;letter-spacing:-.5px;border-bottom:1px solid var(--border);margin-bottom:8px}
|
||||
.sidebar nav a{display:block;padding:8px 20px;color:var(--fg2);font-size:13px;text-decoration:none}
|
||||
.sidebar nav a:hover{color:var(--fg);background:var(--border)}
|
||||
.main{flex:1;padding:24px 32px;max-width:1200px}
|
||||
.main>h1{font-size:22px;font-weight:700;margin-bottom:24px}
|
||||
.login-page{display:flex;align-items:center;justify-content:center;min-height:100vh;background:var(--bg2)}
|
||||
.login-card{width:360px;padding:32px;background:var(--bg);border:1px solid var(--border)}
|
||||
.login-card h1{font-size:20px;margin-bottom:24px;text-align:center;letter-spacing:-.5px}
|
||||
.login-card .field{margin-bottom:16px}
|
||||
.login-card .field label{display:block;font-size:12px;color:var(--fg2);margin-bottom:4px}
|
||||
.login-card .error{color:var(--fg);font-size:13px;margin-bottom:12px;text-align:center}
|
||||
.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:24px}
|
||||
.grid-3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;margin-bottom:24px}
|
||||
.grid-4{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:24px}
|
||||
.toolbar{display:flex;gap:8px;margin-bottom:16px;align-items:center}
|
||||
.toolbar .spacer{flex:1}
|
||||
.flex{display:flex;gap:8px;align-items:center}
|
||||
.text-right{text-align:right}
|
||||
.text-mono{font-family:"SFMono-Regular",Consolas,"Liberation Mono",monospace;font-size:12px}
|
||||
.mb-8{margin-bottom:8px;margin-top:0!important}
|
||||
.mt-8{margin-top:8px}
|
||||
.mb-16{margin-bottom:16px}
|
||||
.empty{padding:40px;text-align:center;color:var(--fg2);font-size:14px}
|
||||
@media(max-width:768px){
|
||||
.sidebar{width:56px;overflow:hidden}
|
||||
.sidebar .brand{font-size:0;padding:12px 16px}
|
||||
.sidebar .brand::after{content:"F";font-size:20px}
|
||||
.sidebar nav a{font-size:0;padding:12px 16px;text-align:center}
|
||||
.sidebar nav a::after{content:attr(data-label);font-size:13px;display:block;text-align:center}
|
||||
.sidebar nav a[data-label]::first-letter{font-size:14px}
|
||||
.main{padding:16px}
|
||||
.grid-2,.grid-3,.grid-4{grid-template-columns:1fr}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="layout">
|
||||
<aside class="sidebar">
|
||||
<div class="brand">frp Admin</div>
|
||||
<nav>
|
||||
<a href="/admin/dashboard" data-label="Dashboard" hx-get="/admin/dashboard" hx-target="#main-content" hx-push-url="true">Dashboard</a>
|
||||
<a href="/admin/clients" data-label="Clients" hx-get="/admin/clients" hx-target="#main-content" hx-push-url="true">Clients</a>
|
||||
<a href="/admin/proxies" data-label="Proxies" hx-get="/admin/proxies" hx-target="#main-content" hx-push-url="true">Proxies</a>
|
||||
<a href="/admin/settings" data-label="Settings" hx-get="/admin/settings" hx-target="#main-content" hx-push-url="true">Settings</a>
|
||||
<hr style="border:none;border-top:1px solid var(--border);margin:8px 20px">
|
||||
<a href="/admin/logout" data-label="Logout" hx-post="/admin/logout" hx-trigger="click" hx-target="body" hx-push-url="true">Logout</a>
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="main" id="main-content">
|
||||
{{.Content}}
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,45 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Login — frp Admin</title>
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{--bg:#fff;--bg2:#f5f5f5;--fg:#000;--fg2:#888;--border:#ddd;--accent:#000;--font:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;--radius:0}
|
||||
@media(prefers-color-scheme:dark){
|
||||
:root{--bg:#000;--bg2:#111;--fg:#fff;--fg2:#666;--border:#333;--accent:#fff}
|
||||
}
|
||||
html,body{height:100%}
|
||||
body{font-family:var(--font);background:var(--bg2);color:var(--fg);font-size:14px;display:flex;align-items:center;justify-content:center;min-height:100vh}
|
||||
.login-card{width:360px;padding:32px;background:var(--bg);border:1px solid var(--border)}
|
||||
.login-card h1{font-size:20px;margin-bottom:24px;text-align:center;letter-spacing:-.5px}
|
||||
.login-card .field{margin-bottom:16px}
|
||||
.login-card .field label{display:block;font-size:12px;color:var(--fg2);margin-bottom:4px}
|
||||
.login-card .error{font-size:13px;margin-bottom:12px;text-align:center}
|
||||
input{padding:8px 12px;border:1px solid var(--border);background:var(--bg);color:var(--fg);font-size:13px;width:100%;-webkit-appearance:none;appearance:none}
|
||||
input:focus{outline:1px solid var(--fg)}
|
||||
button{padding:8px 16px;border:1px solid var(--fg);background:var(--fg);color:var(--bg);font-size:13px;cursor:pointer;font-family:var(--font);width:100%}
|
||||
button:hover{opacity:.8}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-card">
|
||||
<h1>frp Admin</h1>
|
||||
{{if .Error}}
|
||||
<div class="error">{{.Error}}</div>
|
||||
{{end}}
|
||||
<form method="post" action="/admin/login">
|
||||
<div class="field">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" required autofocus>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
<button type="submit">Sign In</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,25 @@
|
||||
<div class="toolbar">
|
||||
<h1>Proxies</h1>
|
||||
<div class="spacer"></div>
|
||||
<a href="/admin/proxies/new" class="btn btn-sm" hx-get="/admin/proxies/new" hx-target="#main-content" hx-push-url="true">New Proxy</a>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Name</th><th>Type</th><th>Client</th><th>Local</th><th>Remote</th><th>Status</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Proxies}}
|
||||
<tr>
|
||||
<td><a href="/admin/proxies/{{.ID}}" hx-get="/admin/proxies/{{.ID}}" hx-target="#main-content" hx-push-url="true">{{.Name}}</a></td>
|
||||
<td style="color:var(--fg2)">{{.ProxyType}}</td>
|
||||
<td>{{.ClientName}}</td>
|
||||
<td class="text-mono">{{.LocalIP}}:{{.LocalPort}}</td>
|
||||
<td class="text-mono">{{if .RemotePort}}{{.RemotePort}}{{else}}—{{end}}</td>
|
||||
<td>{{if eq .Status "active"}}<span style="font-weight:600">active</span>{{else if eq .Status "inactive"}}<span style="color:var(--fg2)">inactive</span>{{else}}<span style="color:var(--fg2)">{{.Status}}</span>{{end}}</td>
|
||||
<td><a href="/admin/proxies/{{.ID}}" class="btn btn-sm btn-outline" hx-get="/admin/proxies/{{.ID}}" hx-target="#main-content" hx-push-url="true">View</a></td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td colspan="7"><div class="empty">No proxies found. <a href="/admin/proxies/new" hx-get="/admin/proxies/new" hx-target="#main-content" hx-push-url="true">Create one</a>.</div></td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -0,0 +1,35 @@
|
||||
<div class="toolbar">
|
||||
<h1>Proxy: {{.Proxy.Name}}</h1>
|
||||
<div class="spacer"></div>
|
||||
<a href="/admin/proxies" class="btn btn-sm btn-outline" hx-get="/admin/proxies" hx-target="#main-content" hx-push-url="true">Back</a>
|
||||
</div>
|
||||
<div class="grid-2">
|
||||
<div class="card">
|
||||
<h3>Configuration</h3>
|
||||
<table style="margin-top:8px">
|
||||
<tbody>
|
||||
<tr><td style="width:120px;color:var(--fg2);font-size:12px">Type</td><td style="color:var(--fg2)">{{.Proxy.ProxyType}}</td></tr>
|
||||
<tr><td style="color:var(--fg2);font-size:12px">Status</td><td>{{if eq .Proxy.Status "active"}}<span style="font-weight:600">Active</span>{{else}}<span style="color:var(--fg2)">{{.Proxy.Status}}</span>{{end}}</td></tr>
|
||||
<tr><td style="color:var(--fg2);font-size:12px">Local</td><td class="text-mono">{{.Proxy.LocalIP}}:{{.Proxy.LocalPort}}</td></tr>
|
||||
<tr><td style="color:var(--fg2);font-size:12px">Remote Port</td><td class="text-mono">{{if .Proxy.RemotePort}}{{.Proxy.RemotePort}}{{else}}—{{end}}</td></tr>
|
||||
<tr><td style="color:var(--fg2);font-size:12px">Custom Domains</td><td>{{if .Proxy.CustomDomains}}{{.Proxy.CustomDomains}}{{else}}—{{end}}</td></tr>
|
||||
<tr><td style="color:var(--fg2);font-size:12px">Created</td><td>{{.Proxy.CreatedAt}}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Client</h3>
|
||||
<table style="margin-top:8px">
|
||||
<tbody>
|
||||
<tr><td style="width:120px;color:var(--fg2);font-size:12px">Client ID</td><td class="text-mono">{{.Proxy.ClientID}}</td></tr>
|
||||
{{if .ClientName}}
|
||||
<tr><td style="color:var(--fg2);font-size:12px">Name</td><td><a href="/admin/clients/{{.Proxy.ClientID}}" hx-get="/admin/clients/{{.Proxy.ClientID}}" hx-target="#main-content" hx-push-url="true">{{.ClientName}}</a></td></tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mt-8">
|
||||
<h3>Metadata</h3>
|
||||
<pre class="text-mono" style="margin-top:8px;white-space:pre-wrap;font-size:12px">{{if .Proxy.Metadata}}{{.Proxy.Metadata}}{{else}}None{{end}}</pre>
|
||||
</div>
|
||||
@@ -0,0 +1,61 @@
|
||||
<div class="toolbar">
|
||||
<h1>New Proxy</h1>
|
||||
</div>
|
||||
|
||||
{{if .Error}}
|
||||
<div style="margin-bottom:16px;padding:12px;border:1px solid var(--border);font-size:13px">{{.Error}}</div>
|
||||
{{end}}
|
||||
|
||||
<form hx-post="/admin/proxies/new" hx-target="body" hx-push-url="true" style="max-width:500px">
|
||||
<div style="margin-bottom:16px">
|
||||
<label style="display:block;font-size:12px;color:var(--fg2);margin-bottom:4px">Client</label>
|
||||
<select name="client_id" required>
|
||||
<option value="">— Select Client —</option>
|
||||
{{range .Clients}}
|
||||
<option value="{{.ID}}"{{if eq .ID $.SelectedClient}} selected{{end}}>{{.Name}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:16px">
|
||||
<label style="display:block;font-size:12px;color:var(--fg2);margin-bottom:4px">Proxy Type</label>
|
||||
<select name="proxy_type">
|
||||
<option value="tcp">TCP</option>
|
||||
<option value="udp">UDP</option>
|
||||
<option value="http">HTTP</option>
|
||||
<option value="https">HTTPS</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom:16px">
|
||||
<label style="display:block;font-size:12px;color:var(--fg2);margin-bottom:4px">Name</label>
|
||||
<input type="text" name="name" required autofocus placeholder="ssh-proxy">
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:16px">
|
||||
<div>
|
||||
<label style="display:block;font-size:12px;color:var(--fg2);margin-bottom:4px">Local IP</label>
|
||||
<input type="text" name="local_ip" value="127.0.0.1" required>
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block;font-size:12px;color:var(--fg2);margin-bottom:4px">Local Port</label>
|
||||
<input type="number" name="local_port" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:16px">
|
||||
<div>
|
||||
<label style="display:block;font-size:12px;color:var(--fg2);margin-bottom:4px">Remote Port</label>
|
||||
<input type="number" name="remote_port" placeholder="0 = auto">
|
||||
</div>
|
||||
<div>
|
||||
<label style="display:block;font-size:12px;color:var(--fg2);margin-bottom:4px">Custom Domains</label>
|
||||
<input type="text" name="custom_domains" placeholder="example.com">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toolbar" style="margin-top:24px">
|
||||
<button type="submit">Create Proxy</button>
|
||||
<a href="/admin/proxies" class="btn btn-outline" hx-get="/admin/proxies" hx-target="#main-content" hx-push-url="true">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,107 @@
|
||||
<div class="toolbar">
|
||||
<h1>Settings</h1>
|
||||
<div class="spacer"></div>
|
||||
{{if .Saved}}
|
||||
<span style="font-size:12px;color:var(--fg2)">Saved. Restart to apply.</span>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<form hx-post="/admin/settings" hx-target="body" hx-push-url="true">
|
||||
<div style="border:1px solid var(--border);padding:20px;margin-bottom:16px">
|
||||
<h2 style="font-size:12px;font-weight:600;letter-spacing:.3px;color:var(--fg2);margin-bottom:16px;text-transform:uppercase">Server</h2>
|
||||
<div class="grid-2" style="margin-bottom:0">
|
||||
<div><label style="display:block;font-size:12px;color:var(--fg2);margin-bottom:4px">Bind Address</label><input type="text" name="bind_addr" value="{{.Config.BindAddr}}"></div>
|
||||
<div><label style="display:block;font-size:12px;color:var(--fg2);margin-bottom:4px">Bind Port</label><input type="number" name="bind_port" value="{{.Config.BindPort}}"></div>
|
||||
<div><label style="display:block;font-size:12px;color:var(--fg2);margin-bottom:4px">Proxy Bind Address</label><input type="text" name="proxy_bind_addr" value="{{.Config.ProxyBindAddr}}"></div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid var(--border);padding:20px;margin-bottom:16px">
|
||||
<h2 style="font-size:12px;font-weight:600;letter-spacing:.3px;color:var(--fg2);margin-bottom:16px;text-transform:uppercase">Dashboard</h2>
|
||||
<div class="grid-2" style="margin-bottom:0">
|
||||
<div><label style="display:block;font-size:12px;color:var(--fg2);margin-bottom:4px">Address</label><input type="text" name="dashboard_addr" value="{{.Config.WebServer.Addr}}"></div>
|
||||
<div><label style="display:block;font-size:12px;color:var(--fg2);margin-bottom:4px">Port</label><input type="number" name="dashboard_port" value="{{.Config.WebServer.Port}}"></div>
|
||||
<div><label style="display:block;font-size:12px;color:var(--fg2);margin-bottom:4px">Username</label><input type="text" name="dashboard_user" value="{{.Config.WebServer.User}}" autocomplete="off"></div>
|
||||
<div><label style="display:block;font-size:12px;color:var(--fg2);margin-bottom:4px">Password</label><input type="text" name="dashboard_password" value="{{.Config.WebServer.Password}}" autocomplete="off"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid var(--border);padding:20px;margin-bottom:16px">
|
||||
<h2 style="font-size:12px;font-weight:600;letter-spacing:.3px;color:var(--fg2);margin-bottom:16px;text-transform:uppercase">Authentication</h2>
|
||||
<div class="grid-2" style="margin-bottom:0">
|
||||
<div><label style="display:block;font-size:12px;color:var(--fg2);margin-bottom:4px">Auth Method</label>
|
||||
<select name="auth_method">
|
||||
<option value="token"{{if eq .Config.Auth.Method "token"}} selected{{end}}>Token</option>
|
||||
<option value="oidc"{{if eq .Config.Auth.Method "oidc"}} selected{{end}}>OIDC</option>
|
||||
</select>
|
||||
</div>
|
||||
<div><label style="display:block;font-size:12px;color:var(--fg2);margin-bottom:4px">Token</label><input type="text" name="auth_token" value="{{.Config.Auth.Token}}" autocomplete="off"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid var(--border);padding:20px;margin-bottom:16px">
|
||||
<h2 style="font-size:12px;font-weight:600;letter-spacing:.3px;color:var(--fg2);margin-bottom:16px;text-transform:uppercase">HTTP / VHost</h2>
|
||||
<div class="grid-2" style="margin-bottom:0">
|
||||
<div><label style="display:block;font-size:12px;color:var(--fg2);margin-bottom:4px">VHost HTTP Port</label><input type="number" name="vhost_http_port" value="{{.Config.VhostHTTPPort}}" placeholder="0 = disabled"></div>
|
||||
<div><label style="display:block;font-size:12px;color:var(--fg2);margin-bottom:4px">VHost HTTPS Port</label><input type="number" name="vhost_https_port" value="{{.Config.VhostHTTPSPort}}" placeholder="0 = disabled"></div>
|
||||
<div><label style="display:block;font-size:12px;color:var(--fg2);margin-bottom:4px">Subdomain Host</label><input type="text" name="subdomain_host" value="{{.Config.SubDomainHost}}"></div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid var(--border);padding:20px;margin-bottom:16px">
|
||||
<h2 style="font-size:12px;font-weight:600;letter-spacing:.3px;color:var(--fg2);margin-bottom:16px;text-transform:uppercase">Transport</h2>
|
||||
<div class="grid-2" style="margin-bottom:0">
|
||||
<div style="display:flex;align-items:center;gap:8px;padding-top:20px">
|
||||
<input type="checkbox" name="tcp_mux" value="true" style="width:auto"{{if .Config.Transport.TCPMux}} checked{{end}}>
|
||||
<label style="font-size:13px">TCP Multiplexing</label>
|
||||
</div>
|
||||
<div><label style="display:block;font-size:12px;color:var(--fg2);margin-bottom:4px">Max Pool Count</label><input type="number" name="max_pool_count" value="{{.Config.Transport.MaxPoolCount}}" placeholder="0 = unlimited"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid var(--border);padding:20px;margin-bottom:16px">
|
||||
<h2 style="font-size:12px;font-weight:600;letter-spacing:.3px;color:var(--fg2);margin-bottom:16px;text-transform:uppercase">Logging</h2>
|
||||
<div class="grid-2" style="margin-bottom:0">
|
||||
<div><label style="display:block;font-size:12px;color:var(--fg2);margin-bottom:4px">Output</label>
|
||||
<select name="log_to">
|
||||
<option value="console"{{if eq .Config.Log.To "console"}} selected{{end}}>Console</option>
|
||||
<option value="file"{{if eq .Config.Log.To "file"}} selected{{end}}>File</option>
|
||||
</select>
|
||||
</div>
|
||||
<div><label style="display:block;font-size:12px;color:var(--fg2);margin-bottom:4px">Level</label>
|
||||
<select name="log_level">
|
||||
<option value="trace"{{if eq .Config.Log.Level "trace"}} selected{{end}}>Trace</option>
|
||||
<option value="debug"{{if eq .Config.Log.Level "debug"}} selected{{end}}>Debug</option>
|
||||
<option value="info"{{if eq .Config.Log.Level "info"}} selected{{end}}>Info</option>
|
||||
<option value="warn"{{if eq .Config.Log.Level "warn"}} selected{{end}}>Warn</option>
|
||||
<option value="error"{{if eq .Config.Log.Level "error"}} selected{{end}}>Error</option>
|
||||
</select>
|
||||
</div>
|
||||
<div><label style="display:block;font-size:12px;color:var(--fg2);margin-bottom:4px">Max Days</label><input type="number" name="log_max_days" value="{{.Config.Log.MaxDays}}"></div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid var(--border);padding:20px;margin-bottom:16px">
|
||||
<h2 style="font-size:12px;font-weight:600;letter-spacing:.3px;color:var(--fg2);margin-bottom:16px;text-transform:uppercase">Extra Ports</h2>
|
||||
<div class="grid-2" style="margin-bottom:0">
|
||||
<div><label style="display:block;font-size:12px;color:var(--fg2);margin-bottom:4px">KCP Bind Port</label><input type="number" name="kcp_bind_port" value="{{.Config.KCPBindPort}}" placeholder="0 = disabled"></div>
|
||||
<div><label style="display:block;font-size:12px;color:var(--fg2);margin-bottom:4px">QUIC Bind Port</label><input type="number" name="quic_bind_port" value="{{.Config.QUICBindPort}}" placeholder="0 = disabled"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="border:1px solid var(--border);padding:20px;margin-bottom:16px">
|
||||
<h2 style="font-size:12px;font-weight:600;letter-spacing:.3px;color:var(--fg2);margin-bottom:16px;text-transform:uppercase">Misc</h2>
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:16px">
|
||||
<input type="checkbox" name="enable_prometheus" value="true" style="width:auto"{{if .Config.EnablePrometheus}} checked{{end}}>
|
||||
<label style="font-size:13px">Enable Prometheus Metrics</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toolbar" style="margin-top:24px">
|
||||
<button type="submit">Save Configuration</button>
|
||||
<a href="/admin/dashboard" class="btn btn-outline" hx-get="/admin/dashboard" hx-target="#main-content" hx-push-url="true">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
@@ -0,0 +1,87 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Setup — frp Admin</title>
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{--bg:#fff;--bg2:#f5f5f5;--fg:#000;--fg2:#888;--border:#ddd;--accent:#000;--font:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;--radius:0}
|
||||
@media(prefers-color-scheme:dark){
|
||||
:root{--bg:#000;--bg2:#111;--fg:#fff;--fg2:#666;--border:#333;--accent:#fff}
|
||||
}
|
||||
html,body{height:100%}
|
||||
body{font-family:var(--font);background:var(--bg2);color:var(--fg);font-size:14px;display:flex;align-items:center;justify-content:center;min-height:100vh;padding:20px}
|
||||
.setup-card{width:520px;padding:32px;background:var(--bg);border:1px solid var(--border)}
|
||||
.setup-card h1{font-size:20px;margin-bottom:4px;text-align:center;letter-spacing:-.5px}
|
||||
.setup-card p{font-size:13px;color:var(--fg2);text-align:center;margin-bottom:24px}
|
||||
.setup-card .error{font-size:13px;margin-bottom:12px;text-align:center}
|
||||
.setup-card .section-title{font-size:12px;font-weight:600;letter-spacing:.3px;color:var(--fg2);text-transform:uppercase;margin-bottom:12px;padding-top:8px;border-top:1px solid var(--border)}
|
||||
.setup-card .field{margin-bottom:16px}
|
||||
.setup-card .field label{display:block;font-size:12px;color:var(--fg2);margin-bottom:4px}
|
||||
input{padding:8px 12px;border:1px solid var(--border);background:var(--bg);color:var(--fg);font-size:13px;width:100%;-webkit-appearance:none;appearance:none}
|
||||
input:focus{outline:1px solid var(--fg)}
|
||||
button{padding:8px 16px;border:1px solid var(--fg);background:var(--fg);color:var(--bg);font-size:13px;cursor:pointer;font-family:var(--font);width:100%;margin-top:8px}
|
||||
button:hover{opacity:.8}
|
||||
.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="setup-card">
|
||||
<h1>frp Admin Setup</h1>
|
||||
<p>Create an admin account and configure your server.</p>
|
||||
{{if .Error}}
|
||||
<div class="error">{{.Error}}</div>
|
||||
{{end}}
|
||||
<form method="post" action="/admin/setup">
|
||||
<div class="section-title">Admin Account</div>
|
||||
<div class="grid-2">
|
||||
<div class="field">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" required autofocus>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="name">Display Name</label>
|
||||
<input type="text" id="name" name="name" placeholder="optional">
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-2">
|
||||
<div class="field">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="confirm_password">Confirm Password</label>
|
||||
<input type="password" id="confirm_password" name="confirm_password" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-title">Server Configuration</div>
|
||||
<div class="grid-2">
|
||||
<div class="field">
|
||||
<label for="bind_addr">Bind Address</label>
|
||||
<input type="text" id="bind_addr" name="bind_addr" value="0.0.0.0">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="bind_port">Bind Port</label>
|
||||
<input type="number" id="bind_port" name="bind_port" value="7000">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="dashboard_addr">Dashboard Address</label>
|
||||
<input type="text" id="dashboard_addr" name="dashboard_addr" value="0.0.0.0">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="dashboard_port">Dashboard Port</label>
|
||||
<input type="number" id="dashboard_port" name="dashboard_port" value="7500">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="auth_token">Auth Token</label>
|
||||
<input type="text" id="auth_token" name="auth_token" placeholder="optional">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit">Complete Setup</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user