From 6555a68fa9216992248305c4b9210d26fd946bdc Mon Sep 17 00:00:00 2001 From: Ivan Fraixedes Date: Thu, 31 Aug 2023 15:22:08 +0200 Subject: [PATCH] satellite/admin: Serve back-office static UI Serve the front-end sources of the new back-office through the current satellite admin server under the path `/back-office`. The front-end is served in the same way than the current one, which is through an indicated directory path with a configuration parameter or embed in the binary when that configuration parameter is empty. The commit also slightly changes the test that checks serving these static assets for not targeting the empty file in the build folder. build folders must remain because of the embed directive. Change-Id: I3c5af6b75ec944722dbdc4c560d0e7d907a205b8 --- Makefile | 10 ++++ satellite/admin/back-office/ui/.gitignore | 3 +- satellite/admin/back-office/ui/assets.go | 25 +++++++++ .../admin/back-office/ui/assets_noembed.go | 18 +++++++ satellite/admin/back-office/ui/build/.keep | 0 satellite/admin/back-office/ui/public/.keep | 0 .../admin/back-office/ui/src/router/index.js | 2 +- satellite/admin/back-office/ui/vite.config.js | 5 +- satellite/admin/server.go | 34 +++++++++--- satellite/admin/server_test.go | 52 ++++++++++++++----- satellite/admin/ui/.gitignore | 2 +- scripts/testdata/satellite-config.yaml.lock | 3 ++ 12 files changed, 130 insertions(+), 24 deletions(-) create mode 100644 satellite/admin/back-office/ui/assets.go create mode 100644 satellite/admin/back-office/ui/assets_noembed.go create mode 100644 satellite/admin/back-office/ui/build/.keep create mode 100644 satellite/admin/back-office/ui/public/.keep diff --git a/Makefile b/Makefile index 1b8a98f5f..0d1532c0e 100644 --- a/Makefile +++ b/Makefile @@ -73,6 +73,8 @@ build-multinode-npm: cd web/multinode && npm ci build-satellite-admin-npm: cd satellite/admin/ui && npm ci + # Temporary until the new back-office replaces the current admin API & UI + cd satellite/admin/back-office/ui && npm ci ##@ Simulator @@ -286,6 +288,14 @@ satellite-admin-ui: -u $(shell id -u):$(shell id -g) \ node:${NODE_VERSION} \ /bin/bash -c "npm ci && npm run build" + # Temporary until the new back-office replaces the current admin API & UI + docker run --rm -i \ + --mount type=bind,src="${PWD}",dst=/go/src/storj.io/storj \ + -w /go/src/storj.io/storj/satellite/admin/back-office/ui \ + -e HOME=/tmp \ + -u $(shell id -u):$(shell id -g) \ + node:${NODE_VERSION} \ + /bin/bash -c "npm ci && npm run build" .PHONY: satellite-wasm satellite-wasm: diff --git a/satellite/admin/back-office/ui/.gitignore b/satellite/admin/back-office/ui/.gitignore index 9bfa63dd5..189f54dfa 100644 --- a/satellite/admin/back-office/ui/.gitignore +++ b/satellite/admin/back-office/ui/.gitignore @@ -2,4 +2,5 @@ node_modules .DS_Store .idea package-lock.json -dist +/build/* +!/build/.keep diff --git a/satellite/admin/back-office/ui/assets.go b/satellite/admin/back-office/ui/assets.go new file mode 100644 index 000000000..11c8c2a1a --- /dev/null +++ b/satellite/admin/back-office/ui/assets.go @@ -0,0 +1,25 @@ +// Copyright (C) 2023 Storj Labs, Inc. +// See LICENSE for copying information. + +//go:build !noembed +// +build !noembed + +package backofficeui + +import ( + "embed" + "fmt" + "io/fs" +) + +//go:embed all:build/* +var assets embed.FS + +// Assets contains either the built admin/back-office/ui or it is empty. +var Assets = func() fs.FS { + build, err := fs.Sub(assets, "build") + if err != nil { + panic(fmt.Errorf("invalid embedding: %w", err)) + } + return build +}() diff --git a/satellite/admin/back-office/ui/assets_noembed.go b/satellite/admin/back-office/ui/assets_noembed.go new file mode 100644 index 000000000..47b774d73 --- /dev/null +++ b/satellite/admin/back-office/ui/assets_noembed.go @@ -0,0 +1,18 @@ +// Copyright (C) 2023 Storj Labs, Inc. +// See LICENSE for copying information. + +//go:build noembed +// +build noembed + +package backofficeui + +import "io/fs" + +// Assets contains either the built admin/back-office/ui or it is empty. +var Assets fs.FS = emptyFS{} + +// emptyFS implements an empty filesystem +type emptyFS struct{} + +// Open implements fs.FS method. +func (emptyFS) Open(name string) (fs.File, error) { return nil, fs.ErrNotExist } diff --git a/satellite/admin/back-office/ui/build/.keep b/satellite/admin/back-office/ui/build/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/satellite/admin/back-office/ui/public/.keep b/satellite/admin/back-office/ui/public/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/satellite/admin/back-office/ui/src/router/index.js b/satellite/admin/back-office/ui/src/router/index.js index def05902c..4974ab5ef 100644 --- a/satellite/admin/back-office/ui/src/router/index.js +++ b/satellite/admin/back-office/ui/src/router/index.js @@ -61,7 +61,7 @@ const routes = [ ] const router = createRouter({ - history: createWebHistory(process.env.NODE_ENV === 'production' ? '/admin-ui/' : process.env.BASE_URL), + history: createWebHistory(process.env.NODE_ENV === 'production' ? '/back-office/' : process.env.BASE_URL), routes, }) diff --git a/satellite/admin/back-office/ui/vite.config.js b/satellite/admin/back-office/ui/vite.config.js index b9573a571..6409df3a6 100644 --- a/satellite/admin/back-office/ui/vite.config.js +++ b/satellite/admin/back-office/ui/vite.config.js @@ -11,7 +11,7 @@ import { fileURLToPath, URL } from 'node:url' // https://vitejs.dev/config/ export default defineConfig({ - base: process.env.NODE_ENV === 'production' ? '/admin-ui/' : '/', + base: process.env.NODE_ENV === 'production' ? '/back-office/' : '/', plugins: [ vue({ template: { transformAssetUrls } @@ -42,4 +42,7 @@ export default defineConfig({ server: { port: 3000, }, + build: { + outDir: "build" + } }) diff --git a/satellite/admin/server.go b/satellite/admin/server.go index fee989769..7f4e76a92 100644 --- a/satellite/admin/server.go +++ b/satellite/admin/server.go @@ -20,6 +20,7 @@ import ( "storj.io/common/errs2" "storj.io/storj/satellite/accounting" + backofficeui "storj.io/storj/satellite/admin/back-office/ui" adminui "storj.io/storj/satellite/admin/ui" "storj.io/storj/satellite/attribution" "storj.io/storj/satellite/buckets" @@ -42,10 +43,11 @@ const ( // Config defines configuration for debug server. type Config struct { - Address string `help:"admin peer http listening address" releaseDefault:"" devDefault:""` - StaticDir string `help:"an alternate directory path which contains the static assets to serve. When empty, it uses the embedded assets" releaseDefault:"" devDefault:""` - AllowedOauthHost string `help:"the oauth host allowed to bypass token authentication."` - Groups Groups + Address string `help:"admin peer http listening address" releaseDefault:"" devDefault:""` + StaticDir string `help:"an alternate directory path which contains the static assets to serve. When empty, it uses the embedded assets" releaseDefault:"" devDefault:""` + StaticDirBackOffice string `help:"an alternate directory path which contains the static assets for the currently in development back-office. When empty, it uses the embedded assets" releaseDefault:"" devDefault:""` + AllowedOauthHost string `help:"the oauth host allowed to bypass token authentication."` + Groups Groups AuthorizationToken string `internal:"true"` } @@ -91,7 +93,17 @@ type Server struct { } // NewServer returns a new administration Server. -func NewServer(log *zap.Logger, listener net.Listener, db DB, buckets *buckets.Service, restKeys *restkeys.Service, freezeAccounts *console.AccountFreezeService, accounts payments.Accounts, console consoleweb.Config, config Config) *Server { +func NewServer( + log *zap.Logger, + listener net.Listener, + db DB, + buckets *buckets.Service, + restKeys *restkeys.Service, + freezeAccounts *console.AccountFreezeService, + accounts payments.Accounts, + console consoleweb.Config, + config Config, +) *Server { server := &Server{ log: log, @@ -159,6 +171,17 @@ func NewServer(log *zap.Logger, listener net.Listener, db DB, buckets *buckets.S limitUpdateAPI.HandleFunc("/projects/{project}/limit", server.getProjectLimit).Methods("GET") limitUpdateAPI.HandleFunc("/projects/{project}/limit", server.putProjectLimit).Methods("PUT", "POST") + // Temporary path until the new back-office is implemented and we can remove the current admin UI. + if config.StaticDirBackOffice == "" { + root.PathPrefix("/back-office").Handler( + http.StripPrefix("/back-office/", http.FileServer(http.FS(backofficeui.Assets))), + ).Methods("GET") + } else { + root.PathPrefix("/back-office").Handler( + http.StripPrefix("/back-office/", http.FileServer(http.Dir(config.StaticDirBackOffice))), + ).Methods("GET") + } + // This handler must be the last one because it uses the root as prefix, // otherwise will try to serve all the handlers set after this one. if config.StaticDir == "" { @@ -214,7 +237,6 @@ func (server *Server) SetAllowedOauthHost(host string) { func (server *Server) withAuth(allowedGroups []string) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Host != server.config.AllowedOauthHost { // not behind the proxy; use old authentication method. if server.config.AuthorizationToken == "" { diff --git a/satellite/admin/server_test.go b/satellite/admin/server_test.go index faca81a86..def674128 100644 --- a/satellite/admin/server_test.go +++ b/satellite/admin/server_test.go @@ -25,9 +25,10 @@ func TestBasic(t *testing.T) { StorageNodeCount: 0, UplinkCount: 0, Reconfigure: testplanet.Reconfigure{ - Satellite: func(log *zap.Logger, index int, config *satellite.Config) { + Satellite: func(_ *zap.Logger, _ int, config *satellite.Config) { config.Admin.Address = "127.0.0.1:0" - config.Admin.StaticDir = "ui/build" + config.Admin.StaticDir = "ui" + config.Admin.StaticDirBackOffice = "back-office/ui" }, }, }, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) { @@ -36,18 +37,28 @@ func TestBasic(t *testing.T) { baseURL := "http://" + address.String() t.Run("UI", func(t *testing.T) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"/.keep", nil) - require.NoError(t, err) + testUI := func(t *testing.T, baseURL string) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"/package.json", nil) + require.NoError(t, err) - response, err := http.DefaultClient.Do(req) - require.NoError(t, err) + response, err := http.DefaultClient.Do(req) + require.NoError(t, err) - require.Equal(t, http.StatusOK, response.StatusCode) + require.Equal(t, http.StatusOK, response.StatusCode) - content, err := io.ReadAll(response.Body) - require.NoError(t, response.Body.Close()) - require.Empty(t, content) - require.NoError(t, err) + content, err := io.ReadAll(response.Body) + require.NoError(t, response.Body.Close()) + require.NotEmpty(t, content) + require.Equal(t, byte('{'), content[0]) + require.NoError(t, err) + } + + t.Run("current", func(t *testing.T) { + testUI(t, baseURL) + }) + t.Run("back-office", func(t *testing.T) { + testUI(t, baseURL+"/back-office") + }) }) // Testing authorization behavior without Oauth from here on out. @@ -130,7 +141,12 @@ func TestWithOAuth(t *testing.T) { // Requests that require full access should not be accessible through Oauth. t.Run("UnauthorizedThroughOauth", func(t *testing.T) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/api/projects/%s/apikeys", baseURL, projectID.String()), nil) + req, err := http.NewRequestWithContext( + ctx, + http.MethodGet, + fmt.Sprintf("%s/api/projects/%s/apikeys", baseURL, projectID.String()), + nil, + ) require.NoError(t, err) req.Header.Set("Authorization", planet.Satellites[0].Config.Console.AuthToken) @@ -162,7 +178,10 @@ func TestWithOAuth(t *testing.T) { body, err := io.ReadAll(response.Body) require.NoError(t, response.Body.Close()) require.NoError(t, err) - errDetail := fmt.Sprintf(admin.UnauthorizedNotInGroup, []string{planet.Satellites[0].Config.Admin.Groups.LimitUpdate}) + errDetail := fmt.Sprintf( + admin.UnauthorizedNotInGroup, + []string{planet.Satellites[0].Config.Admin.Groups.LimitUpdate}, + ) require.Contains(t, string(body), errDetail) req, err = http.NewRequestWithContext(ctx, http.MethodGet, targetURL, nil) @@ -200,7 +219,12 @@ func TestWithAuthNoToken(t *testing.T) { address := sat.Admin.Admin.Listener.Addr() baseURL := "http://" + address.String() - req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/api/projects/%s/apikeys", baseURL, projectID.String()), nil) + req, err := http.NewRequestWithContext( + ctx, + http.MethodGet, + fmt.Sprintf("%s/api/projects/%s/apikeys", baseURL, projectID.String()), + nil, + ) require.NoError(t, err) // Authorization disabled, so this should fail. diff --git a/satellite/admin/ui/.gitignore b/satellite/admin/ui/.gitignore index ce3dd7bf0..483f84307 100644 --- a/satellite/admin/ui/.gitignore +++ b/satellite/admin/ui/.gitignore @@ -5,4 +5,4 @@ node_modules /.svelte-kit /package .env -.env.* \ No newline at end of file +.env.* diff --git a/scripts/testdata/satellite-config.yaml.lock b/scripts/testdata/satellite-config.yaml.lock index d2d1b0e2f..4a1558740 100755 --- a/scripts/testdata/satellite-config.yaml.lock +++ b/scripts/testdata/satellite-config.yaml.lock @@ -25,6 +25,9 @@ # an alternate directory path which contains the static assets to serve. When empty, it uses the embedded assets # admin.static-dir: "" +# an alternate directory path which contains the static assets for the currently in development back-office. When empty, it uses the embedded assets +# admin.static-dir-back-office: "" + # enable analytics reporting # analytics.enabled: false