storj/satellite/admin/server.go
Ivan Fraixedes 5573ece848 satellite/admin/ui: Migrate to SvelteKit
Migrate the satellite admin UI web app from the Svelte template used to
generate a Svelte App scaffolding to SvelteKit.

There aren't any functional changes in the  application, however, the
commit has a lot because:

1. SvelteKit uses a different directory layout and constraints to it, so
   the files have been moved.
2. The files have changed its formatting due to the new default linter
   configurations that SvelteKit uses.
3. The linter detected some issues with using `object` and `any` types
   in Typescript, so they have been replaced by better general types
   (e.g. Record).

The  migration allows to use the new tooling rather than Rollup
directly, besides that will empower the future of it when it needs more
features (e.g. different routes, etc.).

Change-Id: Ifa6736c13585708337f6c5a59388077b784eaddd
2021-12-17 10:27:13 +00:00

183 lines
5.6 KiB
Go

// Copyright (C) 2020 Storj Labs, Inc.
// See LICENSE for copying information.
// Package admin implements administrative endpoints for satellite.
package admin
import (
"context"
"crypto/subtle"
"embed"
"errors"
"io/fs"
"net"
"net/http"
"time"
"github.com/gorilla/mux"
"go.uber.org/zap"
"golang.org/x/sync/errgroup"
"storj.io/common/errs2"
"storj.io/storj/satellite/accounting"
"storj.io/storj/satellite/buckets"
"storj.io/storj/satellite/console"
"storj.io/storj/satellite/payments"
"storj.io/storj/satellite/payments/stripecoinpayments"
)
//go:embed ui/assets/*
var ui embed.FS
// 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:""`
AuthorizationToken string `internal:"true"`
ConsoleConfig console.Config
}
// DB is databases needed for the admin server.
type DB interface {
// ProjectAccounting returns database for storing information about project data use
ProjectAccounting() accounting.ProjectAccounting
// Console returns database for satellite console
Console() console.DB
// StripeCoinPayments returns database for satellite stripe coin payments
StripeCoinPayments() stripecoinpayments.DB
}
// Server provides endpoints for administrative tasks.
type Server struct {
log *zap.Logger
listener net.Listener
server http.Server
db DB
payments payments.Accounts
buckets *buckets.Service
nowFn func() time.Time
config Config
}
// NewServer returns a new administration Server.
func NewServer(log *zap.Logger, listener net.Listener, db DB, buckets *buckets.Service, accounts payments.Accounts, config Config) *Server {
server := &Server{
log: log,
listener: listener,
db: db,
payments: accounts,
buckets: buckets,
nowFn: time.Now,
config: config,
}
root := mux.NewRouter()
api := root.PathPrefix("/api/").Subrouter()
api.Use(allowedAuthorization(config.AuthorizationToken))
// When adding new options, also update README.md
api.HandleFunc("/users", server.addUser).Methods("POST")
api.HandleFunc("/users/{useremail}", server.updateUser).Methods("PUT")
api.HandleFunc("/users/{useremail}", server.userInfo).Methods("GET")
api.HandleFunc("/users/{useremail}", server.deleteUser).Methods("DELETE")
api.HandleFunc("/projects", server.addProject).Methods("POST")
api.HandleFunc("/projects/{project}/usage", server.checkProjectUsage).Methods("GET")
api.HandleFunc("/projects/{project}/limit", server.getProjectLimit).Methods("GET")
api.HandleFunc("/projects/{project}/limit", server.putProjectLimit).Methods("PUT", "POST")
api.HandleFunc("/projects/{project}", server.getProject).Methods("GET")
api.HandleFunc("/projects/{project}", server.renameProject).Methods("PUT")
api.HandleFunc("/projects/{project}", server.deleteProject).Methods("DELETE")
api.HandleFunc("/projects/{project}/apikeys", server.listAPIKeys).Methods("GET")
api.HandleFunc("/projects/{project}/apikeys", server.addAPIKey).Methods("POST")
api.HandleFunc("/projects/{project}/apikeys/{name}", server.deleteAPIKeyByName).Methods("DELETE")
api.HandleFunc("/projects/{project}/buckets/{bucket}", server.getBucketInfo).Methods("GET")
api.HandleFunc("/projects/{project}/buckets/{bucket}/geofence", server.createGeofenceForBucket).Methods("POST")
api.HandleFunc("/projects/{project}/buckets/{bucket}/geofence", server.deleteGeofenceForBucket).Methods("DELETE")
api.HandleFunc("/apikeys/{apikey}", server.deleteAPIKey).Methods("DELETE")
// 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 == "" {
uiAssets, err := fs.Sub(ui, "ui/assets")
if err != nil {
log.Error("invalid embbeded static assets directory, the Admin UI is not enabled")
} else {
root.PathPrefix("/").Handler(http.FileServer(http.FS(uiAssets))).Methods("GET")
}
} else {
root.PathPrefix("/").Handler(http.FileServer(http.Dir(config.StaticDir))).Methods("GET")
}
server.server.Handler = root
return server
}
// Run starts the admin endpoint.
func (server *Server) Run(ctx context.Context) error {
if server.listener == nil {
return nil
}
ctx, cancel := context.WithCancel(ctx)
var group errgroup.Group
group.Go(func() error {
<-ctx.Done()
return Error.Wrap(server.server.Shutdown(context.Background()))
})
group.Go(func() error {
defer cancel()
err := server.server.Serve(server.listener)
if errs2.IsCanceled(err) || errors.Is(err, http.ErrServerClosed) {
err = nil
}
return Error.Wrap(err)
})
return group.Wait()
}
// SetNow allows tests to have the server act as if the current time is whatever they want.
func (server *Server) SetNow(nowFn func() time.Time) {
server.nowFn = nowFn
}
// Close closes server and underlying listener.
func (server *Server) Close() error {
return Error.Wrap(server.server.Close())
}
func allowedAuthorization(token 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 token == "" {
sendJSONError(w, "Authorization not enabled.",
"", http.StatusForbidden)
return
}
equality := subtle.ConstantTimeCompare(
[]byte(r.Header.Get("Authorization")),
[]byte(token),
)
if equality != 1 {
sendJSONError(w, "Forbidden",
"", http.StatusForbidden)
return
}
r.Header.Set("Cache-Control", "must-revalidate")
next.ServeHTTP(w, r)
})
}
}