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
This commit is contained in:
parent
48d7be7eab
commit
6555a68fa9
10
Makefile
10
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:
|
||||
|
3
satellite/admin/back-office/ui/.gitignore
vendored
3
satellite/admin/back-office/ui/.gitignore
vendored
@ -2,4 +2,5 @@ node_modules
|
||||
.DS_Store
|
||||
.idea
|
||||
package-lock.json
|
||||
dist
|
||||
/build/*
|
||||
!/build/.keep
|
||||
|
25
satellite/admin/back-office/ui/assets.go
Normal file
25
satellite/admin/back-office/ui/assets.go
Normal file
@ -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
|
||||
}()
|
18
satellite/admin/back-office/ui/assets_noembed.go
Normal file
18
satellite/admin/back-office/ui/assets_noembed.go
Normal file
@ -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 }
|
0
satellite/admin/back-office/ui/build/.keep
Normal file
0
satellite/admin/back-office/ui/build/.keep
Normal file
0
satellite/admin/back-office/ui/public/.keep
Normal file
0
satellite/admin/back-office/ui/public/.keep
Normal file
@ -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,
|
||||
})
|
||||
|
||||
|
@ -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"
|
||||
}
|
||||
})
|
||||
|
@ -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 == "" {
|
||||
|
@ -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.
|
||||
|
2
satellite/admin/ui/.gitignore
vendored
2
satellite/admin/ui/.gitignore
vendored
@ -5,4 +5,4 @@ node_modules
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
.env.*
|
||||
|
3
scripts/testdata/satellite-config.yaml.lock
vendored
3
scripts/testdata/satellite-config.yaml.lock
vendored
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user