From 3ec21ce44e29ca84b6a87c44af12ff630d6516fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Cluseau?= Date: Mon, 13 Feb 2023 13:03:42 +0100 Subject: [PATCH] store download & add key --- .gitignore | 3 +- cmd/dkl-local-server/cas-cleaner.go | 8 +-- cmd/dkl-local-server/httperr.go | 5 +- cmd/dkl-local-server/main.go | 2 +- cmd/dkl-local-server/secret-store.go | 51 ++++++++++-------- cmd/dkl-local-server/state.go | 4 ++ cmd/dkl-local-server/token.go | 24 +++++++++ cmd/dkl-local-server/ws-public.go | 77 +++++++++++++++++++++++++++- cmd/dkl-local-server/ws-store.go | 27 ++++++++++ cmd/dkl-local-server/ws.go | 30 +++++++++-- go.mod | 12 ++--- go.sum | 13 +++++ html/ui/app.css | 18 +++++++ html/ui/index.html | 19 +++++-- html/ui/js/app.js | 5 ++ 15 files changed, 256 insertions(+), 42 deletions(-) create mode 100644 cmd/dkl-local-server/token.go create mode 100644 cmd/dkl-local-server/ws-store.go diff --git a/.gitignore b/.gitignore index 9c22b8c..9f22a70 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.sw[po] modd-local.conf /tmp -/go.work /dist +/go.work +/go.work.sum diff --git a/cmd/dkl-local-server/cas-cleaner.go b/cmd/dkl-local-server/cas-cleaner.go index 39220f3..d8b46a5 100644 --- a/cmd/dkl-local-server/cas-cleaner.go +++ b/cmd/dkl-local-server/cas-cleaner.go @@ -12,13 +12,15 @@ var ( ) func casCleaner() { - for { + for range time.Tick(*cacheCleanDelay) { + if !wPublicState.Get().Store.Open { + continue + } + err := cleanCAS() if err != nil { log.Print("warn: couldn't clean cache: ", err) } - - time.Sleep(*cacheCleanDelay) } } diff --git a/cmd/dkl-local-server/httperr.go b/cmd/dkl-local-server/httperr.go index 277b1f6..1a8fdfd 100644 --- a/cmd/dkl-local-server/httperr.go +++ b/cmd/dkl-local-server/httperr.go @@ -6,4 +6,7 @@ import ( "m.cluseau.fr/go/httperr" ) -var ErrNotFound = httperr.NewStd(404, http.StatusNotFound, "not found") +var ( + ErrNotFound = httperr.NewStd(404, http.StatusNotFound, "not found") + ErrInvalidToken = httperr.NewStd(403, http.StatusForbidden, "invalid token") +) diff --git a/cmd/dkl-local-server/main.go b/cmd/dkl-local-server/main.go index 2da49b3..82a7002 100644 --- a/cmd/dkl-local-server/main.go +++ b/cmd/dkl-local-server/main.go @@ -53,7 +53,7 @@ func main() { if autoUnlock != "" { log.Printf("auto-unlocking the store") err := unlockSecretStore([]byte(autoUnlock)) - if err != nil { + if err.Any() { log.Fatal(err) } diff --git a/cmd/dkl-local-server/secret-store.go b/cmd/dkl-local-server/secret-store.go index 3ed6b86..b1f9733 100644 --- a/cmd/dkl-local-server/secret-store.go +++ b/cmd/dkl-local-server/secret-store.go @@ -1,14 +1,12 @@ package main import ( - "crypto/rand" - "encoding/base32" "encoding/json" "log" "net/http" "os" "path/filepath" - "sort" + "sort" "strings" "sync" @@ -20,7 +18,8 @@ import ( var secStore *secretstore.Store -func secStorePath(name string) string { return filepath.Join(*dataDir, "secrets", name) } +func secStoreRoot() string { return filepath.Join(*dataDir, "secrets") } +func secStorePath(name string) string { return filepath.Join(secStoreRoot(), name) } func secKeysStorePath() string { return secStorePath(".keys") } func openSecretStore() { @@ -64,7 +63,7 @@ var ( ErrInvalidPassphrase = httperr.NewStd(http.StatusBadRequest, 2, "invalid passphrase") ) -func unlockSecretStore(passphrase []byte) *httperr.Error { +func unlockSecretStore(passphrase []byte) (err httperr.Error) { unlockMutex.Lock() defer unlockMutex.Unlock() @@ -75,7 +74,7 @@ func unlockSecretStore(passphrase []byte) *httperr.Error { if secStore.IsNew() { err := secStore.Init(passphrase) if err != nil { - return httperr.New(http.StatusInternalServerError, err) + return httperr.Internal(err) } err = secStore.SaveTo(secKeysStorePath()) @@ -83,7 +82,7 @@ func unlockSecretStore(passphrase []byte) *httperr.Error { log.Print("secret store save error: ", err) secStore.Close() - return httperr.New(http.StatusInternalServerError, err) + return httperr.Internal(err) } } else { @@ -98,25 +97,21 @@ func unlockSecretStore(passphrase []byte) *httperr.Error { log.Print("failed to read admin token: ", err) secStore.Close() - return httperr.New(http.StatusInternalServerError, err) + return httperr.Internal(err) } - randBytes := make([]byte, 32) - _, err := rand.Read(randBytes) + token, err = newToken(32) if err != nil { - log.Print("rand read error: ", err) secStore.Close() - - return httperr.New(http.StatusInternalServerError, err) + return httperr.Internal(err) } - token = base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(randBytes) err = writeSecret("admin-token", token) if err != nil { log.Print("write error: ", err) secStore.Close() - return httperr.New(http.StatusInternalServerError, err) + return httperr.Internal(err) } log.Print("wrote new admin token") @@ -124,6 +119,18 @@ func unlockSecretStore(passphrase []byte) *httperr.Error { *adminToken = token + { + token, err := newToken(16) + if err != nil { + secStore.Close() + return httperr.Internal(err) + } + + wState.Change(func(v *State) { + v.Store.DownloadToken = token + }) + } + wPublicState.Change(func(v *PublicState) { v.Store.New = false v.Store.Open = true @@ -132,7 +139,7 @@ func unlockSecretStore(passphrase []byte) *httperr.Error { go updateState() go migrateSecrets() - return nil + return } func readSecret(name string, value any) (err error) { @@ -179,12 +186,12 @@ func writeSecret(name string, value any) (err error) { } err = os.Rename(f.Name(), secStorePath(name+".data")) - if err != nil { - return - } + if err != nil { + return + } - go updateState() - return + go updateState() + return } var secL sync.Mutex @@ -255,7 +262,7 @@ func (s KVSecrets[T]) Keys(prefix string) (keys []string, err error) { keys = append(keys, k[len(prefix):]) } - sort.Strings(keys) + sort.Strings(keys) return } diff --git a/cmd/dkl-local-server/state.go b/cmd/dkl-local-server/state.go index 487ddbe..4c4bbd9 100644 --- a/cmd/dkl-local-server/state.go +++ b/cmd/dkl-local-server/state.go @@ -20,6 +20,10 @@ var wPublicState = watchable.New[PublicState]() type State struct { HasConfig bool + Store struct { + DownloadToken string + } + Clusters []ClusterState Hosts []HostState Config *localconfig.Config diff --git a/cmd/dkl-local-server/token.go b/cmd/dkl-local-server/token.go new file mode 100644 index 0000000..83e2507 --- /dev/null +++ b/cmd/dkl-local-server/token.go @@ -0,0 +1,24 @@ +package main + +import ( + "crypto/rand" + "encoding/base32" + "log" + "net/http" + + "m.cluseau.fr/go/httperr" +) + +func newToken(sizeInBytes int) (token string, err error) { + randBytes := make([]byte, sizeInBytes) + + _, err = rand.Read(randBytes) + if err != nil { + log.Print("rand read error: ", err) + err = httperr.New(http.StatusInternalServerError, err) + return + } + + token = base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(randBytes) + return +} diff --git a/cmd/dkl-local-server/ws-public.go b/cmd/dkl-local-server/ws-public.go index 1b498be..006428f 100644 --- a/cmd/dkl-local-server/ws-public.go +++ b/cmd/dkl-local-server/ws-public.go @@ -1,7 +1,12 @@ package main import ( + "archive/tar" + "bytes" + "io" + "io/fs" "net/http" + "os" restful "github.com/emicklei/go-restful" ) @@ -14,10 +19,80 @@ func wsUnlockStore(req *restful.Request, resp *restful.Response) { return } - if err := unlockSecretStore([]byte(passphrase)); err != nil { + if err := unlockSecretStore([]byte(passphrase)); err.Any() { err.WriteJSON(resp.ResponseWriter) return } resp.WriteEntity(*adminToken) } + +func wsStoreDownload(req *restful.Request, resp *restful.Response) { + token := req.QueryParameter("token") + if token != wState.Get().Store.DownloadToken { + wsError(resp, ErrInvalidToken) + return + } + + buf := new(bytes.Buffer) + arch := tar.NewWriter(buf) + + root := os.DirFS(secStoreRoot()) + + err := fs.WalkDir(root, ".", func(path string, d fs.DirEntry, readErr error) (err error) { + if readErr != nil { + err = readErr + return + } + + if path == "." { + return + } + + fi, err := d.Info() + if err != nil { + return + } + + hdr, err := tar.FileInfoHeader(fi, "") + if err != nil { + return + } + + hdr.Name = path + hdr.Uid = 0 + hdr.Gid = 0 + + err = arch.WriteHeader(hdr) + if err != nil { + return + } + + if fi.IsDir() { + return + } + + f, err := root.Open(path) + if err != nil { + return + } + defer f.Close() + + io.Copy(arch, f) + + return + }) + + if err != nil { + wsError(resp, err) + return + } + + err = arch.Close() + if err != nil { + wsError(resp, err) + return + } + + buf.WriteTo(resp) +} diff --git a/cmd/dkl-local-server/ws-store.go b/cmd/dkl-local-server/ws-store.go new file mode 100644 index 0000000..12b4923 --- /dev/null +++ b/cmd/dkl-local-server/ws-store.go @@ -0,0 +1,27 @@ +package main + +import ( + restful "github.com/emicklei/go-restful" +) + +func wsStoreAddKey(req *restful.Request, resp *restful.Response) { + var passphrase string + + err := req.ReadEntity(&passphrase) + if err != nil { + wsBadRequest(resp, err.Error()) + return + } + + if len(passphrase) == 0 { + wsBadRequest(resp, "no passphrase given") + return + } + + secStore.AddKey([]byte(passphrase)) + err = secStore.SaveTo(secKeysStorePath()) + if err != nil { + wsError(resp, err) + return + } +} diff --git a/cmd/dkl-local-server/ws.go b/cmd/dkl-local-server/ws.go index 503fa9d..b0a7d8f 100644 --- a/cmd/dkl-local-server/ws.go +++ b/cmd/dkl-local-server/ws.go @@ -1,6 +1,7 @@ package main import ( + "errors" "fmt" "log" "net" @@ -10,6 +11,7 @@ import ( cfsslconfig "github.com/cloudflare/cfssl/config" "github.com/emicklei/go-restful" + "m.cluseau.fr/go/httperr" "novit.tech/direktil/pkg/localconfig" @@ -28,6 +30,10 @@ func registerWS(rest *restful.Container) { Reads(""). Writes(""). Doc("Try to unlock the store")). + Route(ws.GET("/store.tar").To(wsStoreDownload). + Produces(mime.TAR). + Param(ws.QueryParameter("token", "the download token")). + Doc("Fetch the encrypted store")). Route(ws.GET("/downloads/{token}/{asset}").To(wsDownload). Param(ws.PathParameter("token", "the download token")). Param(ws.PathParameter("asset", "the requested asset")). @@ -42,12 +48,21 @@ func registerWS(rest *restful.Container) { Filter(adminAuth). HeaderParameter("Authorization", "Admin bearer token") + // - store management + ws.Route(ws.POST("/store/add-key").To(wsStoreAddKey). + Consumes("application/json").Reads(""). + Produces("application/json"). + Doc("Add an unlock key to the store")) + // - downloads ws.Route(ws.POST("/authorize-download").To(wsAuthorizeDownload). + Consumes("application/json").Reads(DownloadSpec{}). + Produces("application/json"). Doc("Create a download token for the given download")) // - configs API ws.Route(ws.POST("/configs").To(wsUploadConfig). + Consumes(mime.YAML). Doc("Upload a new current configuration, archiving the previous one")) // - clusters API @@ -180,11 +195,20 @@ func wsNotFound(req *restful.Request, resp *restful.Response) { http.NotFound(resp.ResponseWriter, req.Request) } +func wsBadRequest(resp *restful.Response, err string) { + httperr.New(http.StatusBadRequest, errors.New(err)).WriteJSON(resp.ResponseWriter) +} + func wsError(resp *restful.Response, err error) { log.Output(2, fmt.Sprint("request failed: ", err)) - resp.WriteErrorString( - http.StatusInternalServerError, - http.StatusText(http.StatusInternalServerError)) + + switch err := err.(type) { + case httperr.Error: + err.WriteJSON(resp.ResponseWriter) + + default: + httperr.Internal(err).WriteJSON(resp.ResponseWriter) + } } func wsRender(resp *restful.Response, sslCfg *cfsslconfig.Config, tmplStr string, value interface{}) { diff --git a/go.mod b/go.mod index 31ca910..f9f1e25 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/miolini/datacounter v1.0.3 github.com/oklog/ulid v1.3.1 github.com/pierrec/lz4 v2.6.1+incompatible - golang.org/x/crypto v0.5.0 + golang.org/x/crypto v0.6.0 gopkg.in/src-d/go-billy.v4 v4.3.2 gopkg.in/src-d/go-git.v4 v4.13.1 gopkg.in/yaml.v2 v2.4.0 @@ -57,15 +57,15 @@ require ( github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/zmap/zcrypto v0.0.0-20230205235340-d51ce4775101 // indirect github.com/zmap/zlint/v3 v3.1.0 // indirect - golang.org/x/mod v0.7.0 // indirect - golang.org/x/net v0.5.0 // indirect + golang.org/x/mod v0.8.0 // indirect + golang.org/x/net v0.6.0 // indirect golang.org/x/sys v0.5.0 // indirect - golang.org/x/text v0.6.0 // indirect - golang.org/x/tools v0.5.0 // indirect + golang.org/x/text v0.7.0 // indirect + golang.org/x/tools v0.6.0 // indirect gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog/v2 v2.90.0 // indirect - k8s.io/utils v0.0.0-20230202215443-34013725500c // indirect + k8s.io/utils v0.0.0-20230209194617-a36077c30491 // indirect ) diff --git a/go.sum b/go.sum index a427613..6380fa3 100644 --- a/go.sum +++ b/go.sum @@ -847,6 +847,8 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220824171710-5757bc0c5503/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -885,6 +887,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -939,6 +943,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1039,6 +1045,7 @@ golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXR golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg= +golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1050,6 +1057,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1123,6 +1132,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.5.0 h1:+bSpV5HIeWkuvgaMfI3UmKRThoTA5ODJTUd8T17NO+4= golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k= +golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 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= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1312,6 +1323,8 @@ k8s.io/utils v0.0.0-20230115233650-391b47cb4029 h1:L8zDtT4jrxj+TaQYD0k8KNlr556Wa k8s.io/utils v0.0.0-20230115233650-391b47cb4029/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= k8s.io/utils v0.0.0-20230202215443-34013725500c h1:YVqDar2X7YiQa/DVAXFMDIfGF8uGrHQemlrwRU5NlVI= k8s.io/utils v0.0.0-20230202215443-34013725500c/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/utils v0.0.0-20230209194617-a36077c30491 h1:r0BAOLElQnnFhE/ApUsg3iHdVYYPBjNSSOMowRZxxsY= +k8s.io/utils v0.0.0-20230209194617-a36077c30491/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= m.cluseau.fr/go v0.0.0-20230205163051-2d3050134ad5 h1:ss7MlpIRrkji6QhhRn4sTkOf44Y6f3roO+7kFXs0Q0U= m.cluseau.fr/go v0.0.0-20230205163051-2d3050134ad5/go.mod h1:1DYAavEumEsHasA2RbRUExaEodBB2NAYD+IcwmgVkRM= m.cluseau.fr/go v0.0.0-20230206224905-5322a9bff2ec h1:Wc4FfO2RPm4N51mc/QY+S0yQJ9KEPE8UyjWZpeA5s/M= diff --git a/html/ui/app.css b/html/ui/app.css index c27d4b6..af19ca9 100644 --- a/html/ui/app.css +++ b/html/ui/app.css @@ -17,3 +17,21 @@ max-height: 100pt; overflow: auto; } + +#store-infos { + display: flex; + flex-flow: row wrap; + align-content: center; + justify-content: flex-start; + border-bottom: dashed 1pt; + margin-bottom: 1ex; +} +#store-infos > * { + display: block; + font-size: medium; + padding: 2pt 1ex; + margin: 0 0 0 1ex; +} +#store-infos > *:first-child { + margin-left: 0; +} diff --git a/html/ui/index.html b/html/ui/index.html index ed58edc..7c437a2 100644 --- a/html/ui/index.html +++ b/html/ui/index.html @@ -41,15 +41,15 @@ @@ -58,12 +58,23 @@

Invalid token

- +