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:
Ivan Fraixedes 2023-08-31 15:22:08 +02:00 committed by Ivan Fraixedes
parent 48d7be7eab
commit 6555a68fa9
12 changed files with 130 additions and 24 deletions

View File

@ -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:

View File

@ -2,4 +2,5 @@ node_modules
.DS_Store
.idea
package-lock.json
dist
/build/*
!/build/.keep

View 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
}()

View 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 }

View 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,
})

View File

@ -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"
}
})

View File

@ -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"
@ -44,6 +45,7 @@ const (
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:""`
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
@ -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 == "" {

View File

@ -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,7 +37,8 @@ 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)
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)
@ -46,8 +48,17 @@ func TestBasic(t *testing.T) {
content, err := io.ReadAll(response.Body)
require.NoError(t, response.Body.Close())
require.Empty(t, content)
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.

View File

@ -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