mirror of
https://github.com/boringproxy/boringproxy.git
synced 2025-02-25 18:55:29 -06:00
Remove old CSS UI
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
{{.Head}}
|
||||
|
||||
<style>
|
||||
|
||||
{{ template "styles.tmpl" }}
|
||||
|
||||
.dialog {
|
||||
display: block;
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
{{.Head}}
|
||||
|
||||
{{ template "head_common.tmpl" }}
|
||||
<style>
|
||||
.dialog {
|
||||
display: block;
|
||||
@@ -6,6 +6,5 @@
|
||||
<link rel="icon" href="/logo.png">
|
||||
|
||||
<style>
|
||||
{{.Styles}}
|
||||
{{ template "styles.tmpl" }}
|
||||
</style>
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="refresh" content="0; URL='{{$.TargetUrl}}'" />
|
||||
{{.Head}}
|
||||
|
||||
<style>
|
||||
{{ template "styles.tmpl" }}
|
||||
.dialog {
|
||||
display: block;
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
{{.Head}}
|
||||
|
||||
<style>
|
||||
{{ template "styles.tmpl" }}
|
||||
.dialog {
|
||||
display: block;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
340
ui_handler.go
340
ui_handler.go
@@ -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
|
||||
}
|
||||
|
||||
281
webui/index.tmpl
281
webui/index.tmpl
@@ -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>
|
||||
258
webui/styles.css
258
webui/styles.css
@@ -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%;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user