move ui to using trunk

cargo install trunk
This commit is contained in:
Mikaël Cluseau
2026-01-22 17:54:31 +01:00
parent 2af7ff85c1
commit 3085dac359
15 changed files with 109 additions and 155 deletions

View File

@ -1,44 +0,0 @@
package main
import (
"net/http"
"os"
"path/filepath"
restful "github.com/emicklei/go-restful"
yaml "gopkg.in/yaml.v2"
)
type SSH_ACL struct {
Keys []string
Clusters []string
Groups []string
Hosts []string
}
func loadSSH_ACLs() (acls []SSH_ACL, err error) {
f, err := os.Open(filepath.Join(*dataDir, "ssh-acls.yaml"))
if err != nil {
return
}
defer f.Close()
err = yaml.NewDecoder(f).Decode(&acls)
return
}
func wsSSH_ACL_List(req *restful.Request, resp *restful.Response) {
// TODO
http.NotFound(resp.ResponseWriter, req.Request)
}
func wsSSH_ACL_Get(req *restful.Request, resp *restful.Response) {
// TODO
http.NotFound(resp.ResponseWriter, req.Request)
}
func wsSSH_ACL_Set(req *restful.Request, resp *restful.Response) {
// TODO
http.NotFound(resp.ResponseWriter, req.Request)
}

View File

@ -4,9 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"log" "log"
"net"
"net/http" "net/http"
"strings"
"text/template" "text/template"
cfsslconfig "github.com/cloudflare/cfssl/config" cfsslconfig "github.com/cloudflare/cfssl/config"
@ -152,10 +150,6 @@ func registerWS(rest *restful.Container) {
ws.Route(ws.GET("/hosts").To(wsListHosts). ws.Route(ws.GET("/hosts").To(wsListHosts).
Doc("List hosts")) Doc("List hosts"))
ws.Route(ws.GET("/ssh-acls").To(wsSSH_ACL_List))
ws.Route(ws.GET("/ssh-acls/{acl-name}").To(wsSSH_ACL_Get))
ws.Route(ws.PUT("/ssh-acls/{acl-name}").To(wsSSH_ACL_Set))
rest.Add(ws) rest.Add(ws)
// Hosts API // Hosts API
@ -176,19 +170,6 @@ func registerWS(rest *restful.Container) {
rest.Add(ws) rest.Add(ws)
// Detected host API
ws = (&restful.WebService{}).
Filter(requireSecStore).
Path("/me").
Param(ws.HeaderParameter("Authorization", "Host or admin bearer token"))
(&wsHost{
hostDoc: "detected host",
getHost: detectHost,
}).register(ws, func(rb *restful.RouteBuilder) {
rb.Notes("In this case, the host is detected from the remote IP")
})
// Hosts by token API // Hosts by token API
ws = (&restful.WebService{}). ws = (&restful.WebService{}).
Filter(requireSecStore). Filter(requireSecStore).
@ -229,41 +210,6 @@ func requireSecStore(req *restful.Request, resp *restful.Response, chain *restfu
chain.ProcessFilter(req, resp) chain.ProcessFilter(req, resp)
} }
func detectHost(req *restful.Request) (hostName string, err error) {
if !*allowDetectedHost {
return
}
r := req.Request
remoteAddr := r.RemoteAddr
if *trustXFF {
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
remoteAddr = strings.Split(xff, ",")[0]
}
}
hostIP, _, err := net.SplitHostPort(remoteAddr)
if err != nil {
hostIP = remoteAddr
}
cfg, err := readConfig()
if err != nil {
return
}
host := cfg.HostByIP(hostIP)
if host == nil {
log.Print("no host found for IP ", hostIP)
return
}
return host.Name, nil
}
func wsReadConfig(resp *restful.Response) *localconfig.Config { func wsReadConfig(resp *restful.Response) *localconfig.Config {
cfg, err := readConfig() cfg, err := readConfig()
if err != nil { if err != nil {

View File

@ -2,5 +2,5 @@ package dlshtml
import "embed" import "embed"
//go:embed favicon.ico ui //go:embed ui
var FS embed.FS var FS embed.FS

24
ui/Trunk.toml Normal file
View File

@ -0,0 +1,24 @@
[build]
public_url = "/ui"
dist = "../html/ui"
[[proxy]]
backend = "http://localhost:7606/public-state"
[[proxy]]
backend = "http://localhost:7606/state"
[[proxy]]
backend = "http://localhost:7606/public"
[[proxy]]
backend = "http://localhost:7606/store"
[[proxy]]
backend = "http://localhost:7606/authorize-download"
[[proxy]]
backend = "http://localhost:7606/sign-download-set"
[[proxy]]
backend = "http://localhost:7606/configs"
[[proxy]]
backend = "http://localhost:7606/clusters"
[[proxy]]
backend = "http://localhost:7606/hosts-from-template"
[[proxy]]
backend = "http://localhost:7606/hosts"

View File

@ -27,3 +27,16 @@
color: var(--link); color: var(--link);
} }
} }
.text-and-file {
position:relative;
textarea {
width: 64em;
}
input[type="file"] {
position:absolute;
bottom:0;right:0;
}
}

View File

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@ -2,18 +2,24 @@
<html> <html>
<head> <head>
<title>Direktil Local Server</title> <title>Direktil Local Server</title>
<style> <base data-trunk-public-url />
@import url('./style.css'); <link data-trunk rel="copy-file" href="favicon.ico" />
@import url('./app.css'); <link rel="icon" href="favicon.ico" />
</style> <link data-trunk rel="copy-file" href="js/vue.esm-browser.js" />
<script src="js/jsonpatch.min.js" crossorigin="anonymous"></script> <link data-trunk rel="inline" href="style.css" />
<script src="js/app.js" type="module" defer></script> <link data-trunk rel="inline" href="app.css" />
<script data-trunk src="js/jsonpatch.min.js"></script>
<script data-trunk src="js/app.js" type="module" defer></script>
<script data-trunk src="js/Downloads.js"></script>
<script data-trunk src="js/GetCopy.js"></script>
<script data-trunk src="js/Cluster.js"></script>
<script data-trunk src="js/Host.js"></script>
<body> <body>
<div id="app"> <div id="app">
<header> <header>
<div id="logo"> <div id="logo">
<img src="/favicon.ico" /> <img src="favicon.ico" />
<span>Direktil Local Server</span> <span>Direktil Local Server</span>
</div> </div>
<div class="utils"> <div class="utils">
@ -134,6 +140,5 @@
</div> </div>
</template> </template>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,8 +1,4 @@
const Cluster = {
import Downloads from './Downloads.js';
import GetCopy from './GetCopy.js';
export default {
components: { Downloads, GetCopy }, components: { Downloads, GetCopy },
props: [ 'cluster', 'token', 'state' ], props: [ 'cluster', 'token', 'state' ],
data() { data() {
@ -52,8 +48,61 @@ export default {
}) })
.catch((e) => { alert('failed to sign: '+e); }) .catch((e) => { alert('failed to sign: '+e); })
}, },
readFile(e, onload) {
const file = e.target.files[0];
if (!file) { return; }
const reader = new FileReader();
reader.onload = () => { onload(reader.result) };
reader.onerror = () => { alert("error reading file"); };
reader.readAsText(file);
},
loadPubKey(e) {
this.readFile(e, (v) => {
this.sshSignReq.PubKey = v;
});
},
loadCSR(e) {
this.readFile(e, (v) => {
this.kubeSignReq.CSR = v;
});
},
}, },
template: ` template: `
<h3>Access</h3>
<p>Allow cluster access from a public key</p>
<h4>Grant SSH access</h4>
<p>Validity: <input type="text" v-model="signReqValidity"/> <small>time range, ie: -5m:1w, 5m, 1M, 1y, 1d-1s, etc.</small></p>
<p>User: <input type="text" v-model="sshSignReq.Principal"/></p>
<p>Public key (OpenSSH format):<br/>
<span class="text-and-file"><textarea v-model="sshSignReq.PubKey" style="height:3lh"></textarea>
<input type="file" accept=".pub" @change="loadPubKey" /></span>
</p>
<p><button @click="sshCASign">Sign SSH access</button>
<template v-if="sshUserCert">
=&gt; <a :href="sshUserCert" download="ssh-cert.pub">Get certificate</a>
</template>
</p>
<h4>Grant Kubernetes API access</h4>
<p>Validity: <input type="text" v-model="signReqValidity"/> <small>time range, ie: -5m:1w, 5m, 1M, 1y, 1d-1s, etc.</small></p>
<p>User: <input type="text" v-model="kubeSignReq.User"/> (by default, from the CSR)</p>
<p>Group: <input type="text" v-model="kubeSignReq.Group"/></p>
<p>Certificate signing request (PEM format):<br/>
<span class="text-and-file"><textarea v-model="kubeSignReq.CSR" style="height:7lh;"></textarea>
<input type="file" accept=".csr" @change="loadCSR" /></span>
</p>
<p><button @click="kubeCASign">Sign Kubernetes API access</button>
<template v-if="kubeUserCert">
=&gt; <a :href="kubeUserCert" download="kube-cert.pub">Get certificate</a>
</template>
</p>
<h3>Tokens</h3> <h3>Tokens</h3>
<section class="links"> <section class="links">
<GetCopy v-for="n in cluster.Tokens" :token="token" :name="n" :href="'/clusters/'+cluster.Name+'/tokens/'+n" /> <GetCopy v-for="n in cluster.Tokens" :token="token" :name="n" :href="'/clusters/'+cluster.Name+'/tokens/'+n" />
@ -78,36 +127,5 @@ export default {
</template></td> </template></td>
</tr></table> </tr></table>
<h3>Access</h3>
<p>Allow cluster access from a public key</p>
<p>Certificate time validity: <input type="text" v-model="signReqValidity"/> <small>ie: -5m:1w, 5m, 1M, 1y, 1d-1s, etc.</p>
<h4>Grant SSH access</h4>
<p>Public key (OpenSSH format):<br/>
<textarea v-model="sshSignReq.PubKey" style="width:64em;height:2lh"></textarea>
</p>
<p>User: <input type="text" v-model="sshSignReq.Principal"/></p>
<p><button @click="sshCASign">Sign SSH access (validity: {{signReqValidity}})</button>
<template v-if="sshUserCert">
=&gt; <a :href="sshUserCert" download="ssh-cert.pub">Get certificate</a>
</template>
</p>
<h4>Grant Kubernetes API access</h4>
<p>Certificate signing request (PEM format):<br/>
<textarea v-model="kubeSignReq.CSR" style="width:64em;height:7lh;"></textarea>
</p>
<p>User: <input type="text" v-model="kubeSignReq.User"/></p>
<p>Group: <input type="text" v-model="kubeSignReq.Group"/></p>
<p><button @click="kubeCASign">Sign Kubernetes API access (validity: {{signReqValidity}})</button>
<template v-if="kubeUserCert">
=&gt; <a :href="kubeUserCert" download="kube-cert.pub">Get certificate</a>
</template>
</p>
` `
} }

View File

@ -1,5 +1,4 @@
const Downloads = {
export default {
props: [ 'kind', 'name', 'token', 'state' ], props: [ 'kind', 'name', 'token', 'state' ],
data() { data() {
return { createDisabled: false, selectedAssets: {} } return { createDisabled: false, selectedAssets: {} }

View File

@ -1,4 +1,4 @@
export default { const GetCopy = {
props: [ 'name', 'href', 'token' ], props: [ 'name', 'href', 'token' ],
data() { return {showCopied: false} }, data() { return {showCopied: false} },
template: `<span class="notif"><div v-if="showCopied">copied!</div><a :href="href" @click="fetchAndSave()">{{name}}</a>&nbsp;<a href="#" class="copy" @click="fetchAndCopy()">&#x1F5D0;</a></span>`, template: `<span class="notif"><div v-if="showCopied">copied!</div><a :href="href" @click="fetchAndSave()">{{name}}</a>&nbsp;<a href="#" class="copy" @click="fetchAndCopy()">&#x1F5D0;</a></span>`,

View File

@ -1,7 +1,4 @@
const Host = {
import Downloads from './Downloads.js';
export default {
components: { Downloads }, components: { Downloads },
props: [ 'host', 'token', 'state' ], props: [ 'host', 'token', 'state' ],
template: ` template: `

View File

@ -1,9 +1,6 @@
import { createApp } from './vue.esm-browser.js'; import { createApp } from './vue.esm-browser.js';
import Cluster from './Cluster.js';
import Host from './Host.js';
createApp({ createApp({
components: { Cluster, Host }, components: { Cluster, Host },
data() { data() {
@ -262,5 +259,4 @@ createApp({
}, },
} }
}).mount('#app') }).mount('#app');