Remove old CSS UI

This commit is contained in:
Anders Pitman
2021-12-14 14:44:04 -07:00
parent 60fbfac081
commit 94a3316e2f
9 changed files with 93 additions and 806 deletions

View File

@@ -1,9 +1,10 @@
<!doctype html>
<html>
<head>
{{.Head}}
<style>
{{ template "styles.tmpl" }}
.dialog {
display: block;
}

View File

@@ -1,8 +1,7 @@
<!doctype html>
<html>
<head>
{{.Head}}
{{ template "head_common.tmpl" }}
<style>
.dialog {
display: block;

View File

@@ -6,6 +6,5 @@
<link rel="icon" href="/logo.png">
<style>
{{.Styles}}
{{ template "styles.tmpl" }}
</style>

View File

@@ -2,9 +2,9 @@
<html>
<head>
<meta http-equiv="refresh" content="0; URL='{{$.TargetUrl}}'" />
{{.Head}}
<style>
{{ template "styles.tmpl" }}
.dialog {
display: block;
}

View File

@@ -1,9 +1,8 @@
<!doctype html>
<html>
<head>
{{.Head}}
<style>
{{ template "styles.tmpl" }}
.dialog {
display: block;
}

View File

@@ -4,6 +4,10 @@
<div class='tn-attribute__name'>Domain:</div>
<div class='tn-attribute__value'>{{$.Tunnel.Domain}}</div>
</div>
<div class='tn-attribute'>
<div class='tn-attribute__name'>Owner:</div>
<div class='tn-attribute__value'>{{$.Tunnel.Owner}}</div>
</div>
<div class='tn-attribute'>
<div class='tn-attribute__name'>Client:</div>
<div class='tn-attribute__value'>{{$.Tunnel.ClientName}}</div>

View File

@@ -3,7 +3,6 @@ package boringproxy
import (
"encoding/base64"
"fmt"
"github.com/GeertJohan/go.rice"
qrcode "github.com/skip2/go-qrcode"
"html/template"
"io"
@@ -23,7 +22,6 @@ type WebUiHandler struct {
api *Api
auth *Auth
tunMan *TunnelManager
box *rice.Box
headHtml template.HTML
tmpl *template.Template
pendingRequests map[string]chan ReqResult
@@ -35,22 +33,6 @@ type ReqResult struct {
redirectUrl string
}
type IndexData struct {
Head template.HTML
Tunnels map[string]Tunnel
Tokens map[string]TokenData
SshKeys map[string]SshKey
Users map[string]User
UserId string
IsAdmin bool
QrCodes map[string]template.URL
}
type TunnelsData struct {
Head template.HTML
Tunnels map[string]Tunnel
}
type ConfirmData struct {
Head template.HTML
Message string
@@ -73,24 +55,6 @@ type LoginData struct {
Head template.HTML
}
type HeadData struct {
Styles template.CSS
}
type MenuData struct {
IsAdmin bool
}
type UsersData struct {
Head template.HTML
Users map[string]User
}
type TokensData struct {
Head template.HTML
Tokens map[string]TokenData
Users map[string]User
}
func NewWebUiHandler(config *Config, db *Database, api *Api, auth *Auth, tunMan *TunnelManager) *WebUiHandler {
return &WebUiHandler{
@@ -106,8 +70,6 @@ func NewWebUiHandler(config *Config, db *Database, api *Api, auth *Auth, tunMan
func (h *WebUiHandler) handleWebUiRequest(w http.ResponseWriter, r *http.Request) {
homePath := "/#/tunnel"
var err error
h.tmpl, err = template.ParseFS(fs, "templates/*.tmpl")
if err != nil {
@@ -115,39 +77,6 @@ func (h *WebUiHandler) handleWebUiRequest(w http.ResponseWriter, r *http.Request
return
}
// Note: h.box and h.headHtml need to be ready before pretty much
// everything else, including sendLoginPage
box, err := rice.FindBox("webui")
if err != nil {
w.WriteHeader(500)
io.WriteString(w, "Error opening webui")
return
}
h.box = box
stylesText, err := box.String("styles.css")
if err != nil {
w.WriteHeader(500)
io.WriteString(w, "Error reading styles.css")
return
}
headTmplStr, err := box.String("head.tmpl")
if err != nil {
w.WriteHeader(500)
io.WriteString(w, "Error reading head.tmpl")
return
}
headTmpl, err := template.New("head").Parse(headTmplStr)
if err != nil {
w.WriteHeader(500)
io.WriteString(w, "Error compiling head.tmpl")
return
}
var headBuilder strings.Builder
headTmpl.Execute(&headBuilder, HeadData{Styles: template.CSS(stylesText)})
h.headHtml = template.HTML(headBuilder.String())
token, err := extractToken("access_token", r)
if err != nil {
h.sendLoginPage(w, r, 401)
@@ -182,88 +111,20 @@ func (h *WebUiHandler) handleWebUiRequest(w http.ResponseWriter, r *http.Request
h.deleteUser(w, r, tokenData)
case "/logo.png":
logoPngBytes, err := box.Bytes("logo.png")
if err != nil {
w.WriteHeader(500)
h.alertDialog(w, r, err.Error(), homePath)
return
}
//logoPngBytes, err := box.Bytes("logo.png")
//if err != nil {
// w.WriteHeader(500)
// h.alertDialog(w, r, err.Error(), homePath)
// return
//}
w.Header()["Content-Type"] = []string{"image/png"}
w.Header()["Cache-Control"] = []string{"max-age=86400"}
//w.Header()["Content-Type"] = []string{"image/png"}
//w.Header()["Cache-Control"] = []string{"max-age=86400"}
w.Write(logoPngBytes)
//w.Write(logoPngBytes)
case "/":
indexTmplStr, err := h.box.String("index.tmpl")
if err != nil {
w.WriteHeader(500)
h.alertDialog(w, r, "Error reading index.tmpl", "/#/tunnels")
return
}
tmpl, err := template.New("index").Parse(indexTmplStr)
if err != nil {
w.WriteHeader(500)
h.alertDialog(w, r, "Error compiling index.tmpl", "/#/tunnels")
return
}
var tokens map[string]TokenData
var users map[string]User
// TODO: handle security checks in api
if user.IsAdmin {
tokens = h.db.GetTokens()
users = h.db.GetUsers()
} else {
tokens = make(map[string]TokenData)
for token, td := range h.db.GetTokens() {
if tokenData.Owner == td.Owner {
tokens[token] = td
}
}
users = make(map[string]User)
users[tokenData.Owner] = user
}
qrCodes := make(map[string]template.URL)
for token := range tokens {
loginUrl := fmt.Sprintf("https://%s/login?access_token=%s", h.config.WebUiDomain, token)
var png []byte
png, err := qrcode.Encode(loginUrl, qrcode.Medium, 256)
if err != nil {
w.WriteHeader(500)
h.alertDialog(w, r, err.Error(), "/#/tokens")
return
}
data := base64.StdEncoding.EncodeToString(png)
qrCodes[token] = template.URL("data:image/png;base64," + data)
}
indexData := IndexData{
Head: h.headHtml,
Tunnels: tunnels,
Tokens: tokens,
SshKeys: h.api.GetSshKeys(tokenData),
Users: users,
UserId: tokenData.Owner,
IsAdmin: user.IsAdmin,
QrCodes: qrCodes,
}
err = tmpl.Execute(w, indexData)
if err != nil {
w.WriteHeader(500)
h.alertDialog(w, r, err.Error(), "/#/tokens")
return
}
http.Redirect(w, r, "/tunnels", 303)
case "/tunnels":
h.handleTunnels(w, r, tokenData, user)
case "/confirm-delete-tunnel":
@@ -277,13 +138,6 @@ func (h *WebUiHandler) handleWebUiRequest(w http.ResponseWriter, r *http.Request
}
domain := r.Form["domain"][0]
tmpl, err := h.loadTemplate("confirm.tmpl")
if err != nil {
w.WriteHeader(500)
io.WriteString(w, err.Error())
return
}
data := &ConfirmData{
Head: h.headHtml,
Message: fmt.Sprintf("Are you sure you want to delete %s?", domain),
@@ -291,7 +145,7 @@ func (h *WebUiHandler) handleWebUiRequest(w http.ResponseWriter, r *http.Request
CancelUrl: "/tunnels",
}
tmpl.Execute(w, data)
h.tmpl.ExecuteTemplate(w, "confirm.tmpl", data)
case "/edit-tunnel":
r.ParseForm()
@@ -330,7 +184,7 @@ func (h *WebUiHandler) handleWebUiRequest(w http.ResponseWriter, r *http.Request
err := h.api.DeleteTunnel(tokenData, r.Form)
if err != nil {
w.WriteHeader(400)
h.alertDialog(w, r, err.Error(), "/#/tunnels")
h.alertDialog(w, r, err.Error(), "/tunnels")
return
}
@@ -343,7 +197,7 @@ func (h *WebUiHandler) handleWebUiRequest(w http.ResponseWriter, r *http.Request
tun, err := h.api.GetTunnel(tokenData, r.Form)
if err != nil {
w.WriteHeader(400)
h.alertDialog(w, r, err.Error(), "/#/tunnels")
h.alertDialog(w, r, err.Error(), "/tunnels")
return
}
@@ -365,28 +219,27 @@ func (h *WebUiHandler) handleWebUiRequest(w http.ResponseWriter, r *http.Request
// err := h.api.DeleteSshKey(tokenData, r.Form)
// if err != nil {
// w.WriteHeader(400)
// h.alertDialog(w, r, err.Error(), "/#/ssh-keys")
// h.alertDialog(w, r, err.Error(), "/ssh-keys")
// return
// }
// http.Redirect(w, r, "/#/ssh-keys", 303)
// http.Redirect(w, r, "/ssh-keys", 303)
case "/confirm-logout":
tmpl, err := h.loadTemplate("confirm.tmpl")
if err != nil {
w.WriteHeader(500)
h.alertDialog(w, r, err.Error(), "/#/tunnels")
return
}
data := &ConfirmData{
Head: h.headHtml,
Message: "Are you sure you want to log out?",
ConfirmUrl: "/logout",
CancelUrl: "/#/tunnels",
CancelUrl: "/",
}
tmpl.Execute(w, data)
err := h.tmpl.ExecuteTemplate(w, "confirm.tmpl", data)
if err != nil {
w.WriteHeader(500)
h.alertDialog(w, r, err.Error(), "/")
return
}
case "/logout":
cookie := &http.Cookie{
@@ -396,7 +249,7 @@ func (h *WebUiHandler) handleWebUiRequest(w http.ResponseWriter, r *http.Request
HttpOnly: true,
}
http.SetCookie(w, cookie)
http.Redirect(w, r, "/#/tunnels", 303)
http.Redirect(w, r, "/tunnels", 303)
case "/loading":
h.handleLoading(w, r)
default:
@@ -408,7 +261,7 @@ func (h *WebUiHandler) handleWebUiRequest(w http.ResponseWriter, r *http.Request
if len(parts) != 3 {
w.WriteHeader(400)
h.alertDialog(w, r, "Invalid path", "/#/tunnels")
h.alertDialog(w, r, "Invalid path", "/tunnels")
return
}
@@ -419,13 +272,15 @@ func (h *WebUiHandler) handleWebUiRequest(w http.ResponseWriter, r *http.Request
tunnel, err := h.api.GetTunnel(tokenData, r.Form)
if err != nil {
w.WriteHeader(400)
h.alertDialog(w, r, err.Error(), "/#/tunnels")
h.alertDialog(w, r, err.Error(), "/tunnels")
return
}
templateData := struct {
User User
Tunnel Tunnel
}{
User: user,
Tunnel: tunnel,
}
@@ -437,7 +292,7 @@ func (h *WebUiHandler) handleWebUiRequest(w http.ResponseWriter, r *http.Request
}
} else {
w.WriteHeader(404)
h.alertDialog(w, r, "Unknown page "+r.URL.Path, "/#/tunnels")
h.alertDialog(w, r, "Unknown page "+r.URL.Path, "/tunnels")
return
}
}
@@ -478,7 +333,7 @@ func (h *WebUiHandler) handleTokens(w http.ResponseWriter, r *http.Request, user
png, err := qrcode.Encode(loginUrl, qrcode.Medium, 256)
if err != nil {
w.WriteHeader(500)
h.alertDialog(w, r, err.Error(), "/#/tokens")
h.alertDialog(w, r, err.Error(), "/tokens")
return
}
@@ -508,14 +363,14 @@ func (h *WebUiHandler) handleTokens(w http.ResponseWriter, r *http.Request, user
_, err := h.api.CreateToken(tokenData, r.Form)
if err != nil {
w.WriteHeader(500)
h.alertDialog(w, r, err.Error(), "/#/tokens")
h.alertDialog(w, r, err.Error(), "/tokens")
return
}
http.Redirect(w, r, "/#/tokens", 303)
http.Redirect(w, r, "/tokens", 303)
default:
w.WriteHeader(405)
h.alertDialog(w, r, "Invalid method for tokens", "/#/tokens")
h.alertDialog(w, r, "Invalid method for tokens", "/tokens")
return
}
}
@@ -524,7 +379,7 @@ func (h *WebUiHandler) handleSshKeys(w http.ResponseWriter, r *http.Request, use
if r.Method != "POST" {
w.WriteHeader(405)
h.alertDialog(w, r, "Invalid method for /ssh-keys", "/#/ssh-keys")
h.alertDialog(w, r, "Invalid method for /ssh-keys", "/ssh-keys")
return
}
@@ -533,14 +388,14 @@ func (h *WebUiHandler) handleSshKeys(w http.ResponseWriter, r *http.Request, use
id := r.Form.Get("id")
if id == "" {
w.WriteHeader(400)
h.alertDialog(w, r, "Invalid id parameter", "/#/ssh-keys")
h.alertDialog(w, r, "Invalid id parameter", "/ssh-keys")
return
}
keyParam := r.Form.Get("key")
if keyParam == "" {
w.WriteHeader(400)
h.alertDialog(w, r, "Invalid key parameter", "/#/ssh-keys")
h.alertDialog(w, r, "Invalid key parameter", "/ssh-keys")
return
}
@@ -556,11 +411,11 @@ func (h *WebUiHandler) handleSshKeys(w http.ResponseWriter, r *http.Request, use
err := h.db.AddSshKey(id, key)
if err != nil {
w.WriteHeader(400)
h.alertDialog(w, r, err.Error(), "/#/ssh-keys")
h.alertDialog(w, r, err.Error(), "/ssh-keys")
return
}
http.Redirect(w, r, "/#/ssh-keys", 303)
http.Redirect(w, r, "/ssh-keys", 303)
}
func (h *WebUiHandler) handleLogin(w http.ResponseWriter, r *http.Request) {
@@ -591,7 +446,7 @@ func (h *WebUiHandler) handleLogin(w http.ResponseWriter, r *http.Request) {
MaxAge: 86400 * 365,
}
http.SetCookie(w, cookie)
http.Redirect(w, r, "/#/tunnels", 303)
http.Redirect(w, r, "/tunnels", 303)
} else {
h.sendLoginPage(w, r, 403)
return
@@ -622,7 +477,7 @@ func (h *WebUiHandler) handleTunnels(w http.ResponseWriter, r *http.Request, tok
}
default:
w.WriteHeader(405)
w.Write([]byte("Invalid method for /#/tunnels"))
w.Write([]byte("Invalid method for /tunnels"))
return
}
}
@@ -632,7 +487,7 @@ func (h *WebUiHandler) handleCreateTunnel(w http.ResponseWriter, r *http.Request
pendingId, err := genRandomCode(16)
if err != nil {
w.WriteHeader(400)
h.alertDialog(w, r, err.Error(), "/#/tunnels")
h.alertDialog(w, r, err.Error(), "/tunnels")
}
doneSignal := make(chan ReqResult)
@@ -646,7 +501,7 @@ func (h *WebUiHandler) handleCreateTunnel(w http.ResponseWriter, r *http.Request
_, err := h.api.CreateTunnel(tokenData, r.Form)
doneSignal <- ReqResult{err, "/#/tunnels"}
doneSignal <- ReqResult{err, "/tunnels"}
}()
timeout := make(chan bool, 1)
@@ -659,19 +514,17 @@ func (h *WebUiHandler) handleCreateTunnel(w http.ResponseWriter, r *http.Request
case <-timeout:
url := fmt.Sprintf("/loading?id=%s", pendingId)
tmpl, err := h.loadTemplate("loading.tmpl")
if err != nil {
w.WriteHeader(500)
h.alertDialog(w, r, err.Error(), "/#/tunnels")
return
}
data := &LoadingData{
Head: h.headHtml,
TargetUrl: url,
}
tmpl.Execute(w, data)
h.tmpl.ExecuteTemplate(w, "loading.tmpl", data)
if err != nil {
w.WriteHeader(500)
h.alertDialog(w, r, err.Error(), "/tunnels")
return
}
case result := <-doneSignal:
if result.err != nil {
@@ -686,26 +539,17 @@ func (h *WebUiHandler) handleCreateTunnel(w http.ResponseWriter, r *http.Request
func (h *WebUiHandler) sendLoginPage(w http.ResponseWriter, r *http.Request, code int) {
loginTemplateStr, err := h.box.String("login.tmpl")
if err != nil {
w.WriteHeader(500)
io.WriteString(w, "Error reading login.tmpl")
return
}
loginTemplate, err := template.New("login").Parse(loginTemplateStr)
if err != nil {
w.WriteHeader(500)
io.WriteString(w, "Error compiling login.tmpl")
return
}
loginData := LoginData{
Head: h.headHtml,
}
w.WriteHeader(code)
loginTemplate.Execute(w, loginData)
err := h.tmpl.ExecuteTemplate(w, "login.tmpl", loginData)
if err != nil {
w.WriteHeader(500)
io.WriteString(w, err.Error())
return
}
}
func (h *WebUiHandler) handleUsers(w http.ResponseWriter, r *http.Request, tokenData TokenData, user User) {
@@ -742,14 +586,14 @@ func (h *WebUiHandler) handleUsers(w http.ResponseWriter, r *http.Request, token
err := h.api.CreateUser(tokenData, r.Form)
if err != nil {
w.WriteHeader(500)
h.alertDialog(w, r, err.Error(), "/#/users")
h.alertDialog(w, r, err.Error(), "/users")
return
}
http.Redirect(w, r, "/#/users", 303)
http.Redirect(w, r, "/users", 303)
default:
w.WriteHeader(405)
h.alertDialog(w, r, "Invalid method for users", "/#/users")
h.alertDialog(w, r, "Invalid method for users", "/users")
}
}
@@ -764,21 +608,19 @@ func (h *WebUiHandler) confirmDeleteUser(w http.ResponseWriter, r *http.Request)
}
username := r.Form["username"][0]
tmpl, err := h.loadTemplate("confirm.tmpl")
data := &ConfirmData{
Head: h.headHtml,
Message: fmt.Sprintf("Are you sure you want to delete user %s?", username),
ConfirmUrl: fmt.Sprintf("/delete-user?username=%s", username),
CancelUrl: "/users",
}
err := h.tmpl.ExecuteTemplate(w, "confirm.tmpl", data)
if err != nil {
w.WriteHeader(500)
io.WriteString(w, err.Error())
return
}
data := &ConfirmData{
Head: h.headHtml,
Message: fmt.Sprintf("Are you sure you want to delete user %s?", username),
ConfirmUrl: fmt.Sprintf("/delete-user?username=%s", username),
CancelUrl: "/#/users",
}
tmpl.Execute(w, data)
}
func (h *WebUiHandler) deleteUser(w http.ResponseWriter, r *http.Request, tokenData TokenData) {
@@ -788,11 +630,11 @@ func (h *WebUiHandler) deleteUser(w http.ResponseWriter, r *http.Request, tokenD
err := h.api.DeleteUser(tokenData, r.Form)
if err != nil {
w.WriteHeader(500)
h.alertDialog(w, r, err.Error(), "/#/users")
h.alertDialog(w, r, err.Error(), "/users")
return
}
http.Redirect(w, r, "/#/users", 303)
http.Redirect(w, r, "/users", 303)
}
func (h *WebUiHandler) confirmDeleteToken(w http.ResponseWriter, r *http.Request) {
@@ -806,21 +648,19 @@ func (h *WebUiHandler) confirmDeleteToken(w http.ResponseWriter, r *http.Request
}
token := r.Form["token"][0]
tmpl, err := h.loadTemplate("confirm.tmpl")
data := &ConfirmData{
Head: h.headHtml,
Message: fmt.Sprintf("Are you sure you want to delete token %s?", token),
ConfirmUrl: fmt.Sprintf("/delete-token?token=%s", token),
CancelUrl: "/tokens",
}
err := h.tmpl.ExecuteTemplate(w, "confirm.tmpl", data)
if err != nil {
w.WriteHeader(500)
io.WriteString(w, err.Error())
return
}
data := &ConfirmData{
Head: h.headHtml,
Message: fmt.Sprintf("Are you sure you want to delete token %s?", token),
ConfirmUrl: fmt.Sprintf("/delete-token?token=%s", token),
CancelUrl: "/#/tokens",
}
tmpl.Execute(w, data)
}
func (h *WebUiHandler) deleteToken(w http.ResponseWriter, r *http.Request, tokenData TokenData) {
@@ -829,25 +669,24 @@ func (h *WebUiHandler) deleteToken(w http.ResponseWriter, r *http.Request, token
err := h.api.DeleteToken(tokenData, r.Form)
if err != nil {
w.WriteHeader(500)
h.alertDialog(w, r, err.Error(), "/#/tokens")
h.alertDialog(w, r, err.Error(), "/tokens")
return
}
http.Redirect(w, r, "/#/tokens", 303)
http.Redirect(w, r, "/tokens", 303)
}
func (h *WebUiHandler) alertDialog(w http.ResponseWriter, r *http.Request, message, redirectUrl string) error {
tmpl, err := h.loadTemplate("alert.tmpl")
if err != nil {
return err
}
tmpl.Execute(w, &AlertData{
err := h.tmpl.ExecuteTemplate(w, "alert.tmpl", &AlertData{
Head: h.headHtml,
Message: message,
RedirectUrl: redirectUrl,
})
if err != nil {
return err
}
return nil
}
@@ -855,7 +694,7 @@ func (h *WebUiHandler) handleLoading(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
w.WriteHeader(405)
h.alertDialog(w, r, "Invalid method for users", "/#/tunnels")
h.alertDialog(w, r, "Invalid method for users", "/tunnels")
}
r.ParseForm()
@@ -877,18 +716,3 @@ func (h *WebUiHandler) handleLoading(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, result.redirectUrl, 303)
}
func (h *WebUiHandler) loadTemplate(name string) (*template.Template, error) {
tmplStr, err := h.box.String(name)
if err != nil {
return nil, err
}
tmpl, err := template.New(name).Parse(tmplStr)
if err != nil {
return nil, err
}
return tmpl, nil
}

View File

@@ -1,281 +0,0 @@
<!doctype html>
<html>
<head>
{{.Head}}
<style>
{{range $domain, $tunnel:= .Tunnels}}
#toggle-tunnel-delete-dialog-{{$tunnel.CssId}} {
display: none;
}
#toggle-tunnel-delete-dialog-{{$tunnel.CssId}}:checked + .dialog {
display: block;
}
#toggle-tunnel-hide-deleted-{{$tunnel.CssId}}:checked + .list-item {
/* This is a trick to make the delete request after the delete button is
* clicked. The background will never actually be displayed, because it's
* moved offscreen. */
position: absolute;
left: -999em;
background: url("/delete-tunnel?domain={{$domain}}");
}
#toggle-tunnel-hide-deleted-{{$tunnel.CssId}}:checked ~ .dialog {
display: none;
}
{{end}}
</style>
</head>
<body>
<main>
<input type='checkbox' id='menu-toggle'/>
<label id='menu-label' for='menu-toggle'>Menu</label>
<div class='page' id='/tunnels'>
<div class='menu'>
<a class='menu-item active-tab' href='/#/tunnels'>Tunnels</a>
<a class='menu-item' href='/#/tokens'>Tokens</a>
<!--
<a class='menu-item' href='/#/ssh-keys'>SSH Keys</a>
-->
{{if .IsAdmin}}
<a class='menu-item' href='/#/users'>Users</a>
{{end}}
<a class='menu-item' href='/confirm-logout'>Logout</a>
</div>
<div class='content'>
<div class='list'>
{{range $domain, $tunnel:= .Tunnels}}
<input autocomplete='off' type='checkbox' class='toggle' id='toggle-tunnel-hide-deleted-{{$tunnel.CssId}}'>
<div class='list-item'>
<div>
<a href="https://{{$domain}}">{{$domain}}</a>:{{$tunnel.TunnelPort}} -> {{$tunnel.ClientName}} -> {{$tunnel.ClientAddress}}:{{$tunnel.ClientPort}}
</div>
<a class='button' href="/tunnel-private-key?domain={{$domain}}">Download Private Key</a>
<label class='button' for='toggle-tunnel-delete-dialog-{{$tunnel.CssId}}'>
Delete
</label>
</div>
<input autocomplete='off' type='checkbox' id='toggle-tunnel-delete-dialog-{{$tunnel.CssId}}'>
<div class='dialog'>
<label for='toggle-tunnel-delete-dialog-{{$tunnel.CssId}}' class='dialog__overlay'></label>
<div class='dialog__content'>
<p class='dialog__text'>
Are you sure you want to delete {{$domain}}?
</p>
<div class='button-row'>
<label for='toggle-tunnel-hide-deleted-{{$tunnel.CssId}}' class='button'>
Confirm
</label>
<label for='toggle-tunnel-delete-dialog-{{$tunnel.CssId}}' class='button'>
Cancel
</button>
</div>
</div>
</div>
{{end}}
</div>
<div class='tunnel-adder'>
<h1>Add Tunnel</h1>
<form action="/tunnels" method="POST">
<div class='input'>
<label for="domain">Domain:</label>
<input type="text" id="domain" name="domain" required>
<input type="hidden" id="tunnel-owner" name="owner" value="{{$.UserId}}">
</div>
<div class='input'>
<label for="tunnel-port">Tunnel Port:</label>
<input type="text" id="tunnel-port" name="tunnel-port" value="Random">
</div>
<!--
<div class='input'>
<label for="ssh-key-id-select">SSH Key:</label>
<select id="ssh-key-id-select" name="ssh-key-id">
<option value="generate">Generate</option>
{{range $id, $sshKey := $.SshKeys}}
<option value="{{$id}}">{{$id}}</option>
{{end}}
</select>
</div>
-->
<div class='input'>
<label for="client-name">Client Name:</label>
<select id="client-name" name="client-name">
<option value="none">No client</option>
{{range $id, $client := (index $.Users $.UserId).Clients}}
<option value="{{$id}}">{{$id}}</option>
{{end}}
</select>
</div>
<div class='input'>
<label for="client-addr">Client Address:</label>
<input type="text" id="client-addr" name="client-addr" value='127.0.0.1'>
</div>
<div class='input'>
<label for="client-port">Client Port:</label>
<input type="text" id="client-port" name="client-port">
</div>
<div class='input'>
<label for="allow-external-tcp">Allow External TCP:</label>
<input type="checkbox" id="allow-external-tcp" name="allow-external-tcp">
</div>
<div class='input'>
<label for="password-protect">Password Protect:</label>
<input type="checkbox" id="password-protect" name="password-protect">
<div id='login-inputs'>
<label for="username">Username:</label>
<input type="text" id="username" name="username">
<label for="password">Password:</label>
<input type="password" id="password" name="password">
</div>
</div>
<div class='input'>
<label for="tls-termination">TLS Termination:</label>
<select id="tls-termination" name="tls-termination">
<option value="server">Server</option>
<option value="client">Client</option>
<option value="passthrough">Passthrough</option>
</select>
</div>
<button class='button' type="submit">Submit</button>
</form>
</div>
</div>
</div>
<div class='page' id='/tokens'>
<div class='menu'>
<a class='menu-item' href='/#/tunnels'>Tunnels</a>
<a class='menu-item active-tab' href='/#/tokens'>Tokens</a>
<!--
<a class='menu-item' href='/#/ssh-keys'>SSH Keys</a>
-->
{{if .IsAdmin}}
<a class='menu-item' href='/#/users'>Users</a>
{{end}}
<a class='menu-item' href='/confirm-logout'>Logout</a>
</div>
<div class='content'>
<div class='list'>
{{range $token, $tokenData := .Tokens}}
<div class='list-item'>
<span class='token'>{{$token}} ({{$tokenData.Owner}})</span>
<a href='/login?access_token={{$token}}'>Login link</a>
<img class='qr-code' src='{{index $.QrCodes $token}}' width=100 height=100>
<a href="/confirm-delete-token?token={{$token}}">
<button class='button'>Delete</button>
</a>
</div>
{{end}}
</div>
<div class='token-adder'>
<form action="/tokens" method="POST">
<label for="token-owner">Owner:</label>
<select id="token-owner" name="owner">
{{range $username, $user := .Users}}
<option value="{{$username}}">{{$username}}</option>
{{end}}
</select>
<button class='button' type="submit">Add Token</button>
</form>
</div>
</div>
</div>
<!--
<div class='page' id='/ssh-keys'>
<div class='menu'>
<a class='menu-item' href='/#/tunnels'>Tunnels</a>
<a class='menu-item' href='/#/tokens'>Tokens</a>
<a class='menu-item active-tab' href='/#/ssh-keys'>SSH Keys</a>
{{if .IsAdmin}}
<a class='menu-item' href='/#/users'>Users</a>
{{end}}
<a class='menu-item' href='/confirm-logout'>Logout</a>
</div>
<div class='content'>
<div class='list'>
{{range $id, $sshKey := .SshKeys}}
<div class='list-item'>
<span class='monospace'>{{$id}} ({{$sshKey.Owner}})</span>
<div class='monospace'>{{$sshKey.Key}}</div>
<a href="/delete-ssh-key?id={{$id}}">
<button class='button'>Delete</button>
</a>
</div>
{{end}}
</div>
<div class='ssh-key-adder'>
<h1>Add SSH Key</h1>
<form action="/ssh-keys" method="POST">
<div class='input'>
<label for="ssh-key-id">SSH Key ID:</label>
<input type="text" id="ssh-key-id" name="id" required>
</div>
<div class='input'>
<label for="ssh-key-owner">Owner:</label>
<select id="ssh-key-owner" name="owner">
{{range $username, $user := .Users}}
<option value="{{$username}}">{{$username}}</option>
{{end}}
</select>
</div>
<div class='input'>
<label for="ssh-key">SSH Key:</label>
<textarea id='ssh-key' name='key' rows='8' cols='30'>
</textarea>
</div>
<button class='button' type="submit">Submit</button>
</form>
</div>
</div>
</div>
-->
{{if .IsAdmin}}
<div class='page' id='/users'>
<div class='menu'>
<a class='menu-item' href='/#/tunnels'>Tunnels</a>
<a class='menu-item' href='/#/tokens'>Tokens</a>
<a class='menu-item' href='/#/users'>Users</a>
<a class='menu-item' href='/confirm-logout'>Logout</a>
</div>
<div class='content'>
<div class='list'>
{{range $username, $user := .Users}}
<div class='list-item'>
{{$username}}
<a href="/confirm-delete-user?username={{$username}}">
<button class='button'>Delete</button>
</a>
</div>
{{end}}
</div>
<div class='user-adder'>
<form action="/users" method="POST">
<label for="username">Username:</label>
<input type="text" id="username" name="username" required>
<label for="is-admin">Is Admin:</label>
<input type="checkbox" id="is-admin" name="is-admin">
<button class='button' type="submit">Add User</button>
</form>
</div>
</div>
</div>
{{end}}
</main>
</body>
</html>

View File

@@ -1,258 +0,0 @@
:root {
--main-color: #555555;
--background-color: #fff;
--hover-color: #ddd;
--menu-label-height: 60px;
}
* {
box-sizing: border-box;
}
html {
}
body {
font-family: Helvetica;
display: flex;
justify-content: center;
margin: 0;
}
main {
width: 100%;
}
#menu-label {
position: fixed;
z-index: 1000;
width: 100vw;
height: var(--menu-label-height);
line-height: var(--menu-label-height);
color: var(--background-color);
background: var(--main-color);
font-size: 18px;
font-weight: bold;
display: block;
/*border: 2px solid var(--main-color);*/
cursor: pointer;
padding: 0 .7em;
user-select: none;
}
#menu-label:hover {
color: var(--main-color);
background: var(--background-color);
border: 2px solid var(--main-color);
}
.menu {
position: fixed;
z-index: 1000;
top: var(--menu-label-height);
display: none;
flex-direction: column;
background: #fff;
border: 1px solid var(--main-color);
/*min-width: 256px;*/
}
.menu-item {
font-weight: bold;
border-bottom: 1px solid var(--main-color);
padding: .7em 2em;
text-decoration: none;
}
.menu-item:hover {
background: var(--hover-color);
}
.menu-item:any-link {
color: var(--main-color);
}
.active-tab.menu-item:any-link {
color: #fff;
}
.active-tab.menu-item {
background: var(--main-color);
}
.toggle {
display: none;
}
#menu-toggle {
display: none;
}
#menu-toggle:checked ~ .page .menu {
display: flex;
}
#menu-toggle:checked ~ #menu-label {
}
.content {
border: 1px solid var(--main-color);
}
.button {
padding: .5em 1em;
margin: 5px;
border: 2px solid var(--main-color);
border-radius: .5em;
color: #fff;
background-color: var(--main-color);
text-decoration: none;
user-select: none;
cursor: pointer;
font-family: -system-ui, sans-serif;
font-size: 1em;
line-height: 1.2;
white-space: nowrap;
}
.button:hover {
color: #555555;
background-color: #fff;
border: 2px solid var(--main-color);
}
.button-row {
display: flex;
justify-content: center;
}
.list {
display: flex;
flex-direction: column;
}
.list-item {
padding: 5px;
border-bottom: 1px solid var(--main-color);
display: flex;
flex-wrap: wrap;
overflow-x: hidden;
justify-content: space-between;
align-items: center;
}
.tunnel:hover {
background: var(--hover-color);
}
.tunnel-adder {
padding: 5px;
}
.tunnel-adder h1 {
margin: .2em;
}
.tunnel-adder form {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.ssh-key-adder form {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.input {
padding: .7em;
margin: .2em;
border: 1px solid var(--main-color);
display: flex;
flex-direction: column;
}
.input label {
padding: .2em;
font-weight: bold;
}
#login-inputs {
display: none;
}
#password-protect:checked ~ #login-inputs {
display: block;
}
.token-adder {
padding: 5px;
display: flex;
}
.token {
font-family: Monospace;
}
.monospace {
font-family: Monospace;
}
.page {
margin-top: var(--menu-label-height);
display: none;
flex-direction: column;
}
.qr-code {
width: 8em;
height: 8em;
}
main *:target {
display: flex;
}
.dialog {
display: none;
}
.dialog__overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, .5);
z-index: 1000;
}
.dialog__content {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: var(--background-color);
padding: 1em;
border: 1px solid #ccc;
z-index: 1010;
}
@media (min-width: 640px) {
main {
display: flex;
justify-content: flex-start;
width: 900px;
}
#menu-label {
display: none;
}
.menu {
display: flex;
position: static;
}
.page {
margin-top: auto;
flex-direction: row;
width: 100%;
}
.content {
width: 100%;
}
}