mirror of
https://github.com/boringproxy/boringproxy.git
synced 2025-02-25 18:55:29 -06:00
Implement first draft of HTML UI
The UI was originally written using fancy HTML checkbox toggle hacks in order to make the UI very fast. It's cool but complicated and difficult to change. In order to make updates to the UI more quickly, I'm changing it to use traditional HTML with full page reloads for navigation. It's not as fast but much simpler.
This commit is contained in:
16
go.mod
16
go.mod
@@ -1,6 +1,6 @@
|
||||
module github.com/boringproxy/boringproxy
|
||||
|
||||
go 1.15
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/GeertJohan/go.rice v1.0.0
|
||||
@@ -8,3 +8,17 @@ require (
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/daaku/go.zipexe v1.0.0 // indirect
|
||||
github.com/klauspost/cpuid v1.2.5 // indirect
|
||||
github.com/libdns/libdns v0.1.0 // indirect
|
||||
github.com/mholt/acmez v0.1.1 // indirect
|
||||
github.com/miekg/dns v1.1.30 // indirect
|
||||
go.uber.org/atomic v1.6.0 // indirect
|
||||
go.uber.org/multierr v1.5.0 // indirect
|
||||
go.uber.org/zap v1.15.0 // indirect
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381 // indirect
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd // indirect
|
||||
golang.org/x/text v0.3.0 // indirect
|
||||
)
|
||||
|
||||
9
go.sum
9
go.sum
@@ -1,10 +1,8 @@
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/GeertJohan/go.incremental v1.0.0 h1:7AH+pY1XUgQE4Y1HcXYaMqAI0m9yrFqo/jt0CW30vsg=
|
||||
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
|
||||
github.com/GeertJohan/go.rice v1.0.0 h1:KkI6O9uMaQU3VEKaj01ulavtF7o1fWT7+pk/4voiMLQ=
|
||||
github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
|
||||
github.com/akavel/rsrc v0.8.0 h1:zjWn7ukO9Kc5Q62DOJCcxGpXC18RawVtYAGdz2aLlfw=
|
||||
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
|
||||
github.com/caddyserver/certmagic v0.12.0 h1:1f7kxykaJkOVVpXJ8ZrC6RAO5F6+kKm9U7dBFbLNeug=
|
||||
github.com/caddyserver/certmagic v0.12.0/go.mod h1:tr26xh+9fY5dN0J6IPAlMj07qpog22PJKa7Nw7j835U=
|
||||
@@ -14,15 +12,12 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/cpuid v1.2.5 h1:VBd9MyVIiJHzzgnrLQG5Bcv75H4YaWrlKqWHjurxCGo=
|
||||
github.com/klauspost/cpuid v1.2.5/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/libdns/libdns v0.1.0 h1:0ctCOrVJsVzj53mop1angHp/pE3hmAhP7KiHvR0HD04=
|
||||
github.com/libdns/libdns v0.1.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
|
||||
@@ -30,7 +25,6 @@ github.com/mholt/acmez v0.1.1 h1:KQODCqk+hBn3O7qfCRPj6L96uG65T5BSS95FKNEqtdA=
|
||||
github.com/mholt/acmez v0.1.1/go.mod h1:8qnn8QA/Ewx8E3ZSsmscqsIjhhpxuy9vqdgbX2ceceM=
|
||||
github.com/miekg/dns v1.1.30 h1:Qww6FseFn8PRfw07jueqIXqodm0JKiiKuK0DeXSqfyo=
|
||||
github.com/miekg/dns v1.1.30/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229 h1:E2B8qYyeSgv5MXpmzZXRNp8IAQ4vjxIjhpAf5hv/tAg=
|
||||
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -43,9 +37,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8=
|
||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||
go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
@@ -89,7 +81,6 @@ golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapK
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
|
||||
58
templates/edit_tunnel.tmpl
Normal file
58
templates/edit_tunnel.tmpl
Normal file
@@ -0,0 +1,58 @@
|
||||
{{ template "header.tmpl" . }}
|
||||
<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="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>
|
||||
{{ template "footer.tmpl" . }}
|
||||
5
templates/footer.tmpl
Normal file
5
templates/footer.tmpl
Normal file
@@ -0,0 +1,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
36
templates/header.tmpl
Normal file
36
templates/header.tmpl
Normal file
@@ -0,0 +1,36 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<title>boringproxy</title>
|
||||
|
||||
<link rel="icon" href="/logo.png">
|
||||
|
||||
<style>
|
||||
{{ template "styles.tmpl" }}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main>
|
||||
<input type='checkbox' id='menu-toggle'/>
|
||||
<label id='menu-label' for='menu-toggle'>Menu</label>
|
||||
|
||||
<div class='page'>
|
||||
<div class='menu'>
|
||||
<a class='menu-item' href='/tunnels'>Tunnels</a>
|
||||
<a class='menu-item' href='/edit-tunnel'>Add Tunnel</a>
|
||||
<a class='menu-item' href='/tokens'>Tokens</a>
|
||||
{{ if $.User.IsAdmin }}
|
||||
<a class='menu-item' href='/users'>Users</a>
|
||||
{{ end }}
|
||||
<a class='menu-item' href='/confirm-logout'>Logout</a>
|
||||
</div>
|
||||
|
||||
<div class='content'>
|
||||
|
||||
309
templates/styles.tmpl
Normal file
309
templates/styles.tmpl
Normal file
@@ -0,0 +1,309 @@
|
||||
:root {
|
||||
--main-color: #555555;
|
||||
--background-color: #fff;
|
||||
--hover-color: #ddd;
|
||||
--menu-label-height: 60px;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
font-family: Arial;
|
||||
}
|
||||
|
||||
html {
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Helvetica;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
main {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tn-tunnel-list-table {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tn-tunnel-list-item {
|
||||
padding: 10px;
|
||||
border: 1px solid #000;
|
||||
}
|
||||
|
||||
.tn-attribute {
|
||||
padding: 5px;
|
||||
}
|
||||
.tn-attribute__name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tn-tunnel-table, .tn-tunnel-table__cell {
|
||||
border: 1px solid black;
|
||||
border-collapse: collapse;
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.tn-tunnel-table {
|
||||
width: 100%;
|
||||
}
|
||||
.tn-tunnel-table__link {
|
||||
display: contents;
|
||||
}
|
||||
.tn-tunnel-table__row:hover {
|
||||
background: var(--hover-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
.tn-tunnel-table__cell {
|
||||
max-width: 256px;
|
||||
}
|
||||
|
||||
.tn-tunnel-link {
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
}
|
||||
.tn-tunnel-link:hover {
|
||||
background: var(--hover-color);
|
||||
}
|
||||
|
||||
#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;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.qr-code {
|
||||
width: 8em;
|
||||
height: 8em;
|
||||
}
|
||||
|
||||
.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: 960px) {
|
||||
|
||||
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%;
|
||||
}
|
||||
|
||||
.tn-tunnel-list-table {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tn-tunnel-list {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
27
templates/tokens.tmpl
Normal file
27
templates/tokens.tmpl
Normal file
@@ -0,0 +1,27 @@
|
||||
{{ template "header.tmpl" . }}
|
||||
<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>
|
||||
{{ template "footer.tmpl" . }}
|
||||
20
templates/tunnel.tmpl
Normal file
20
templates/tunnel.tmpl
Normal file
@@ -0,0 +1,20 @@
|
||||
{{ template "header.tmpl" . }}
|
||||
|
||||
<div class='tn-attribute'>
|
||||
<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'>Client:</div>
|
||||
<div class='tn-attribute__value'>{{$.Tunnel.ClientName}}</div>
|
||||
</div>
|
||||
<div class='tn-attribute'>
|
||||
<div class='tn-attribute__name'>Target:</div>
|
||||
<div class='tn-attribute__value'>{{$.Tunnel.ClientAddress}}:{{$.Tunnel.ClientPort}}</div>
|
||||
</div>
|
||||
|
||||
<a href="/confirm-delete-tunnel?domain={{$.Tunnel.Domain}}">
|
||||
<button class='button'>Delete</button>
|
||||
</a>
|
||||
|
||||
{{ template "footer.tmpl" . }}
|
||||
54
templates/tunnels.tmpl
Normal file
54
templates/tunnels.tmpl
Normal file
@@ -0,0 +1,54 @@
|
||||
{{ template "header.tmpl" . }}
|
||||
<div class='tn-tunnel-list'>
|
||||
{{ range $domain, $tunnel:= .Tunnels }}
|
||||
<div class='tn-tunnel-list-item'>
|
||||
<div class='tn-attribute'>
|
||||
<div class='tn-attribute__name'>Domain:</div>
|
||||
<div class='tn-attribute__value'>{{$domain}}</div>
|
||||
</div>
|
||||
<div class='tn-attribute'>
|
||||
<div class='tn-attribute__name'>Client:</div>
|
||||
<div class='tn-attribute__value'>{{$tunnel.ClientName}}</div>
|
||||
</div>
|
||||
<div class='tn-attribute'>
|
||||
<div class='tn-attribute__name'>Target:</div>
|
||||
<div class='tn-attribute__value'>{{$tunnel.ClientAddress}}:{{$tunnel.ClientPort}}</div>
|
||||
</div>
|
||||
<div class='button-row'>
|
||||
<a class='button' href="/tunnels/{{$domain}}">View</a>
|
||||
<a class='button' href="/confirm-delete-tunnel?domain={{$domain}}">Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<div class='tn-tunnel-list-table'>
|
||||
<table class='tn-tunnel-table'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class='tn-tunnel-table__cell'>Domain</th>
|
||||
<th class='tn-tunnel-table__cell'>Client</th>
|
||||
<th class='tn-tunnel-table__cell'>Target</th>
|
||||
<th class='tn-tunnel-table__cell'>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range $domain, $tunnel:= .Tunnels}}
|
||||
<tr>
|
||||
<td class='tn-tunnel-table__cell'>
|
||||
{{$domain}}
|
||||
</td>
|
||||
<td class='tn-tunnel-table__cell'>{{$tunnel.ClientName}}</td>
|
||||
<td class='tn-tunnel-table__cell'>{{$tunnel.ClientAddress}}:{{$tunnel.ClientPort}}</td>
|
||||
<td class='tn-tunnel-table__cell'>
|
||||
<div class='button-row'>
|
||||
<a class='button' href="/tunnels/{{$domain}}">View</a>
|
||||
<a class='button' href="/confirm-delete-tunnel?domain={{$domain}}">Delete</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{ template "footer.tmpl" . }}
|
||||
21
templates/users.tmpl
Normal file
21
templates/users.tmpl
Normal file
@@ -0,0 +1,21 @@
|
||||
{{ template "header.tmpl" . }}
|
||||
<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>
|
||||
{{ template "footer.tmpl" . }}
|
||||
246
ui_handler.go
246
ui_handler.go
@@ -11,8 +11,12 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"embed"
|
||||
)
|
||||
|
||||
//go:embed templates
|
||||
var fs embed.FS
|
||||
|
||||
type WebUiHandler struct {
|
||||
config *Config
|
||||
db *Database
|
||||
@@ -21,6 +25,7 @@ type WebUiHandler struct {
|
||||
tunMan *TunnelManager
|
||||
box *rice.Box
|
||||
headHtml template.HTML
|
||||
tmpl *template.Template
|
||||
pendingRequests map[string]chan ReqResult
|
||||
mutex *sync.Mutex
|
||||
}
|
||||
@@ -103,6 +108,13 @@ 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 {
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Note: h.box and h.headHtml need to be ready before pretty much
|
||||
// everything else, including sendLoginPage
|
||||
|
||||
@@ -162,7 +174,7 @@ func (h *WebUiHandler) handleWebUiRequest(w http.ResponseWriter, r *http.Request
|
||||
case "/login":
|
||||
h.handleLogin(w, r)
|
||||
case "/users":
|
||||
h.handleUsers(w, r, tokenData)
|
||||
h.handleUsers(w, r, tokenData, user)
|
||||
|
||||
case "/confirm-delete-user":
|
||||
h.confirmDeleteUser(w, r)
|
||||
@@ -253,7 +265,7 @@ func (h *WebUiHandler) handleWebUiRequest(w http.ResponseWriter, r *http.Request
|
||||
}
|
||||
|
||||
case "/tunnels":
|
||||
h.handleTunnels(w, r, tokenData)
|
||||
h.handleTunnels(w, r, tokenData, user)
|
||||
case "/confirm-delete-tunnel":
|
||||
|
||||
r.ParseForm()
|
||||
@@ -276,11 +288,41 @@ func (h *WebUiHandler) handleWebUiRequest(w http.ResponseWriter, r *http.Request
|
||||
Head: h.headHtml,
|
||||
Message: fmt.Sprintf("Are you sure you want to delete %s?", domain),
|
||||
ConfirmUrl: fmt.Sprintf("/delete-tunnel?domain=%s", domain),
|
||||
CancelUrl: "/#/tunnels",
|
||||
CancelUrl: "/tunnels",
|
||||
}
|
||||
|
||||
tmpl.Execute(w, data)
|
||||
|
||||
case "/edit-tunnel":
|
||||
r.ParseForm()
|
||||
|
||||
var users map[string]User
|
||||
|
||||
// TODO: handle security checks in api
|
||||
if user.IsAdmin {
|
||||
users = h.db.GetUsers()
|
||||
} else {
|
||||
users = make(map[string]User)
|
||||
users[tokenData.Owner] = user
|
||||
}
|
||||
|
||||
templateData := struct {
|
||||
UserId string
|
||||
User User
|
||||
Users map[string]User
|
||||
}{
|
||||
UserId: tokenData.Owner,
|
||||
User: user,
|
||||
Users: users,
|
||||
}
|
||||
|
||||
err := h.tmpl.ExecuteTemplate(w, "edit_tunnel.tmpl", templateData)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
io.WriteString(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
case "/delete-tunnel":
|
||||
|
||||
r.ParseForm()
|
||||
@@ -292,6 +334,8 @@ func (h *WebUiHandler) handleWebUiRequest(w http.ResponseWriter, r *http.Request
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/tunnels", 303)
|
||||
|
||||
case "/tunnel-private-key":
|
||||
|
||||
r.ParseForm()
|
||||
@@ -356,30 +400,124 @@ func (h *WebUiHandler) handleWebUiRequest(w http.ResponseWriter, r *http.Request
|
||||
case "/loading":
|
||||
h.handleLoading(w, r)
|
||||
default:
|
||||
w.WriteHeader(404)
|
||||
h.alertDialog(w, r, "Unknown page "+r.URL.Path, "/#/tunnels")
|
||||
return
|
||||
if strings.HasPrefix(r.URL.Path, "/tunnels/") {
|
||||
|
||||
r.ParseForm()
|
||||
|
||||
parts := strings.Split(r.URL.Path, "/")
|
||||
|
||||
if len(parts) != 3 {
|
||||
w.WriteHeader(400)
|
||||
h.alertDialog(w, r, "Invalid path", "/#/tunnels")
|
||||
return
|
||||
}
|
||||
|
||||
domain := parts[2]
|
||||
|
||||
r.Form.Set("domain", domain)
|
||||
|
||||
tunnel, err := h.api.GetTunnel(tokenData, r.Form)
|
||||
if err != nil {
|
||||
w.WriteHeader(400)
|
||||
h.alertDialog(w, r, err.Error(), "/#/tunnels")
|
||||
return
|
||||
}
|
||||
|
||||
templateData := struct {
|
||||
Tunnel Tunnel
|
||||
}{
|
||||
Tunnel: tunnel,
|
||||
}
|
||||
|
||||
err = h.tmpl.ExecuteTemplate(w, "tunnel.tmpl", templateData)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
io.WriteString(w, err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
w.WriteHeader(404)
|
||||
h.alertDialog(w, r, "Unknown page "+r.URL.Path, "/#/tunnels")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *WebUiHandler) handleTokens(w http.ResponseWriter, r *http.Request, user User, tokenData TokenData) {
|
||||
|
||||
if r.Method != "POST" {
|
||||
r.ParseForm()
|
||||
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
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)
|
||||
}
|
||||
|
||||
templateData := struct {
|
||||
Tokens map[string]TokenData
|
||||
User User
|
||||
Users map[string]User
|
||||
QrCodes map[string]template.URL
|
||||
}{
|
||||
Tokens: tokens,
|
||||
User: user,
|
||||
Users: users,
|
||||
QrCodes: qrCodes,
|
||||
}
|
||||
|
||||
err := h.tmpl.ExecuteTemplate(w, "tokens.tmpl", templateData)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
io.WriteString(w, err.Error())
|
||||
return
|
||||
}
|
||||
case "POST":
|
||||
_, err := h.api.CreateToken(tokenData, r.Form)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
h.alertDialog(w, r, err.Error(), "/#/tokens")
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/#/tokens", 303)
|
||||
default:
|
||||
w.WriteHeader(405)
|
||||
h.alertDialog(w, r, "Invalid method for tokens", "/#/tokens")
|
||||
return
|
||||
}
|
||||
|
||||
r.ParseForm()
|
||||
|
||||
_, err := h.api.CreateToken(tokenData, r.Form)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
h.alertDialog(w, r, err.Error(), "/#/tokens")
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/#/tokens", 303)
|
||||
}
|
||||
|
||||
func (h *WebUiHandler) handleSshKeys(w http.ResponseWriter, r *http.Request, user User, tokenData TokenData) {
|
||||
@@ -460,11 +598,28 @@ func (h *WebUiHandler) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *WebUiHandler) handleTunnels(w http.ResponseWriter, r *http.Request, tokenData TokenData) {
|
||||
func (h *WebUiHandler) handleTunnels(w http.ResponseWriter, r *http.Request, tokenData TokenData, user User) {
|
||||
|
||||
switch r.Method {
|
||||
case "POST":
|
||||
h.handleCreateTunnel(w, r, tokenData)
|
||||
case "GET":
|
||||
tunnels := h.api.GetTunnels(tokenData)
|
||||
|
||||
templateData := struct {
|
||||
User User
|
||||
Tunnels map[string]Tunnel
|
||||
}{
|
||||
User: user,
|
||||
Tunnels: tunnels,
|
||||
}
|
||||
|
||||
err := h.tmpl.ExecuteTemplate(w, "tunnels.tmpl", templateData)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
io.WriteString(w, err.Error())
|
||||
return
|
||||
}
|
||||
default:
|
||||
w.WriteHeader(405)
|
||||
w.Write([]byte("Invalid method for /#/tunnels"))
|
||||
@@ -553,24 +708,49 @@ func (h *WebUiHandler) sendLoginPage(w http.ResponseWriter, r *http.Request, cod
|
||||
loginTemplate.Execute(w, loginData)
|
||||
}
|
||||
|
||||
func (h *WebUiHandler) handleUsers(w http.ResponseWriter, r *http.Request, tokenData TokenData) {
|
||||
|
||||
if r.Method != "POST" {
|
||||
w.WriteHeader(405)
|
||||
h.alertDialog(w, r, "Invalid method for users", "/#/users")
|
||||
return
|
||||
}
|
||||
func (h *WebUiHandler) handleUsers(w http.ResponseWriter, r *http.Request, tokenData TokenData, user User) {
|
||||
|
||||
r.ParseForm()
|
||||
|
||||
err := h.api.CreateUser(tokenData, r.Form)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
h.alertDialog(w, r, err.Error(), "/#/users")
|
||||
return
|
||||
}
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
var users map[string]User
|
||||
|
||||
http.Redirect(w, r, "/#/users", 303)
|
||||
// TODO: handle security checks in api
|
||||
if user.IsAdmin {
|
||||
users = h.db.GetUsers()
|
||||
} else {
|
||||
users = make(map[string]User)
|
||||
users[tokenData.Owner] = user
|
||||
}
|
||||
|
||||
templateData := struct {
|
||||
User User
|
||||
Users map[string]User
|
||||
}{
|
||||
User: user,
|
||||
Users: users,
|
||||
}
|
||||
|
||||
err := h.tmpl.ExecuteTemplate(w, "users.tmpl", templateData)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
io.WriteString(w, err.Error())
|
||||
return
|
||||
}
|
||||
case "POST":
|
||||
err := h.api.CreateUser(tokenData, r.Form)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
h.alertDialog(w, r, err.Error(), "/#/users")
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/#/users", 303)
|
||||
default:
|
||||
w.WriteHeader(405)
|
||||
h.alertDialog(w, r, "Invalid method for users", "/#/users")
|
||||
}
|
||||
}
|
||||
|
||||
func (h *WebUiHandler) confirmDeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user