satellite/admin/back-office: add endpoint to get users by email

This change adds an endpoint to the back office API that returns user
info based on email address.

References #6503

Change-Id: Ib48d30b0b6c6862887b3f8114f50538b3deca57b
This commit is contained in:
Jeremy Wharton 2023-11-13 20:38:12 -06:00 committed by Storj Robot
parent 220920edb9
commit d2819522c6
15 changed files with 627 additions and 19 deletions

View File

@ -11,6 +11,8 @@ import (
"storj.io/private/process"
"storj.io/private/version"
"storj.io/storj/satellite"
"storj.io/storj/satellite/accounting"
"storj.io/storj/satellite/accounting/live"
"storj.io/storj/satellite/metabase"
"storj.io/storj/satellite/satellitedb"
)
@ -47,7 +49,21 @@ func cmdAdminRun(cmd *cobra.Command, args []string) (err error) {
err = errs.Combine(err, metabaseDB.Close())
}()
peer, err := satellite.NewAdmin(log, identity, db, metabaseDB, version.Build, &runCfg.Config, process.AtomicLevel(cmd))
accountingCache, err := live.OpenCache(ctx, log.Named("live-accounting"), runCfg.LiveAccounting)
if err != nil {
if !accounting.ErrSystemOrNetError.Has(err) || accountingCache == nil {
return errs.New("Error instantiating live accounting cache: %w", err)
}
log.Warn("Unable to connect to live accounting cache. Verify connection",
zap.Error(err),
)
}
defer func() {
err = errs.Combine(err, accountingCache.Close())
}()
peer, err := satellite.NewAdmin(log, identity, db, metabaseDB, accountingCache, version.Build, &runCfg.Config, process.AtomicLevel(cmd))
if err != nil {
return err
}

View File

@ -695,7 +695,13 @@ func (planet *Planet) newAdmin(ctx context.Context, index int, identity *identit
prefix := "satellite-admin" + strconv.Itoa(index)
log := planet.log.Named(prefix)
return satellite.NewAdmin(log, identity, db, metabaseDB, versionInfo, &config, nil)
liveAccounting, err := live.OpenCache(ctx, log.Named("live-accounting"), config.LiveAccounting)
if err != nil {
return nil, errs.Wrap(err)
}
planet.databases = append(planet.databases, liveAccounting)
return satellite.NewAdmin(log, identity, db, metabaseDB, liveAccounting, versionInfo, &config, nil)
}
func (planet *Planet) newRepairer(ctx context.Context, index int, identity *identity.FullIdentity, db satellite.DB, metabaseDB *metabase.DB, config satellite.Config, versionInfo version.Info) (_ *satellite.Repairer, err error) {

View File

@ -20,7 +20,9 @@ import (
"storj.io/private/version"
"storj.io/storj/private/lifecycle"
"storj.io/storj/private/version/checker"
"storj.io/storj/satellite/accounting"
"storj.io/storj/satellite/admin"
backoffice "storj.io/storj/satellite/admin/back-office"
"storj.io/storj/satellite/analytics"
"storj.io/storj/satellite/buckets"
"storj.io/storj/satellite/console"
@ -66,6 +68,7 @@ type Admin struct {
Admin struct {
Listener net.Listener
Server *admin.Server
Service *backoffice.Service
}
Buckets struct {
@ -79,11 +82,23 @@ type Admin struct {
FreezeAccounts struct {
Service *console.AccountFreezeService
}
LiveAccounting struct {
Cache accounting.Cache
}
ProjectLimits struct {
Cache *accounting.ProjectLimitCache
}
Accounting struct {
Service *accounting.Service
}
}
// NewAdmin creates a new satellite admin peer.
func NewAdmin(log *zap.Logger, full *identity.FullIdentity, db DB, metabaseDB *metabase.DB,
versionInfo version.Info, config *Config, atomicLogLevel *zap.AtomicLevel) (*Admin, error) {
liveAccounting accounting.Cache, versionInfo version.Info, config *Config, atomicLogLevel *zap.AtomicLevel) (*Admin, error) {
peer := &Admin{
Log: log,
Identity: full,
@ -208,7 +223,31 @@ func NewAdmin(log *zap.Logger, full *identity.FullIdentity, db DB, metabaseDB *m
peer.Payments.Accounts = peer.Payments.Service.Accounts()
}
{ // setup admin endpoint
{ // setup live accounting
peer.LiveAccounting.Cache = liveAccounting
}
{ // setup project limits
peer.ProjectLimits.Cache = accounting.NewProjectLimitCache(peer.DB.ProjectAccounting(),
config.Console.Config.UsageLimits.Storage.Free,
config.Console.Config.UsageLimits.Bandwidth.Free,
config.Console.Config.UsageLimits.Segment.Free,
config.ProjectLimit,
)
}
{ // setup accounting project usage
peer.Accounting.Service = accounting.NewService(
peer.DB.ProjectAccounting(),
peer.LiveAccounting.Cache,
peer.ProjectLimits.Cache,
*metabaseDB,
config.LiveAccounting.BandwidthCacheTTL,
config.LiveAccounting.AsOfSystemInterval,
)
}
{ // setup admin
var err error
peer.Admin.Listener, err = net.Listen("tcp", config.Admin.Address)
if err != nil {
@ -220,6 +259,14 @@ func NewAdmin(log *zap.Logger, full *identity.FullIdentity, db DB, metabaseDB *m
return nil, err
}
peer.Admin.Service = backoffice.NewService(
log.Named("back-office:service"),
peer.DB.Console(),
peer.DB.ProjectAccounting(),
peer.Accounting.Service,
placement,
)
adminConfig := config.Admin
adminConfig.AuthorizationToken = config.Console.AuthToken
@ -232,7 +279,7 @@ func NewAdmin(log *zap.Logger, full *identity.FullIdentity, db DB, metabaseDB *m
peer.FreezeAccounts.Service,
peer.Analytics.Service,
peer.Payments.Accounts,
placement,
peer.Admin.Service,
config.Console,
adminConfig,
)

View File

@ -6,6 +6,8 @@
* PlacementManagement
* [Get placements](#placementmanagement-get-placements)
* UserManagement
* [Get user](#usermanagement-get-user)
<h3 id='placementmanagement-get-placements'>Get placements (<a href='#list-of-endpoints'>go to full list</a>)</h3>
@ -26,3 +28,45 @@ Gets placement rule IDs and their locations
```
<h3 id='usermanagement-get-user'>Get user (<a href='#list-of-endpoints'>go to full list</a>)</h3>
Gets user by email address
`GET /back-office/api/v1/users/{email}`
**Path Params:**
| name | type | elaboration |
|---|---|---|
| `email` | `string` | |
**Response body:**
```typescript
{
id: string // UUID formatted as `00000000-0000-0000-0000-000000000000`
fullName: string
email: string
paidTier: boolean
createdAt: string // Date timestamp formatted as `2006-01-02T15:00:00Z`
status: string
userAgent: string
defaultPlacement: number
projectUsageLimits: [
{
id: string // UUID formatted as `00000000-0000-0000-0000-000000000000`
name: string
storageLimit: number
storageUsed: number
bandwidthLimit: number
bandwidthUsed: number
segmentLimit: number
segmentUsed: number
}
]
}
```

View File

@ -34,6 +34,19 @@ func main() {
Response: []backoffice.PlacementInfo{},
})
group = api.Group("UserManagement", "users")
group.Get("/{email}", &apigen.Endpoint{
Name: "Get user",
Description: "Gets user by email address",
GoName: "GetUserByEmail",
TypeScriptName: "getUserByEmail",
PathParams: []apigen.Param{
apigen.NewParam("email", ""),
},
Response: backoffice.User{},
})
modroot := findModuleRootDir()
api.MustWriteGo(filepath.Join(modroot, "satellite", "admin", "back-office", "handlers.gen.go"))
api.MustWriteTS(filepath.Join(modroot, "satellite", "admin", "back-office", "ui", "src", "api", "client.gen.ts"))

View File

@ -17,11 +17,16 @@ import (
)
var ErrPlacementsAPI = errs.Class("admin placements api")
var ErrUsersAPI = errs.Class("admin users api")
type PlacementManagementService interface {
GetPlacements(ctx context.Context) ([]PlacementInfo, api.HTTPError)
}
type UserManagementService interface {
GetUserByEmail(ctx context.Context, email string) (*User, api.HTTPError)
}
// PlacementManagementHandler is an api handler that implements all PlacementManagement API endpoints functionality.
type PlacementManagementHandler struct {
log *zap.Logger
@ -29,6 +34,13 @@ type PlacementManagementHandler struct {
service PlacementManagementService
}
// UserManagementHandler is an api handler that implements all UserManagement API endpoints functionality.
type UserManagementHandler struct {
log *zap.Logger
mon *monkit.Scope
service UserManagementService
}
func NewPlacementManagement(log *zap.Logger, mon *monkit.Scope, service PlacementManagementService, router *mux.Router) *PlacementManagementHandler {
handler := &PlacementManagementHandler{
log: log,
@ -42,6 +54,19 @@ func NewPlacementManagement(log *zap.Logger, mon *monkit.Scope, service Placemen
return handler
}
func NewUserManagement(log *zap.Logger, mon *monkit.Scope, service UserManagementService, router *mux.Router) *UserManagementHandler {
handler := &UserManagementHandler{
log: log,
mon: mon,
service: service,
}
usersRouter := router.PathPrefix("/back-office/api/v1/users").Subrouter()
usersRouter.HandleFunc("/{email}", handler.handleGetUserByEmail).Methods("GET")
return handler
}
func (h *PlacementManagementHandler) handleGetPlacements(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
@ -60,3 +85,28 @@ func (h *PlacementManagementHandler) handleGetPlacements(w http.ResponseWriter,
h.log.Debug("failed to write json GetPlacements response", zap.Error(ErrPlacementsAPI.Wrap(err)))
}
}
func (h *UserManagementHandler) handleGetUserByEmail(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var err error
defer h.mon.Task()(&ctx)(&err)
w.Header().Set("Content-Type", "application/json")
email, ok := mux.Vars(r)["email"]
if !ok {
api.ServeError(h.log, w, http.StatusBadRequest, errs.New("missing email route param"))
return
}
retVal, httpErr := h.service.GetUserByEmail(ctx, email)
if httpErr.Err != nil {
api.ServeError(h.log, w, httpErr.Status, httpErr.Err)
return
}
err = json.NewEncoder(w).Encode(retVal)
if err != nil {
h.log.Debug("failed to write json GetUserByEmail response", zap.Error(ErrUsersAPI.Wrap(err)))
}
}

View File

@ -18,7 +18,7 @@ type PlacementInfo struct {
}
// GetPlacements returns IDs and locations of placement rules.
func (s *Server) GetPlacements(ctx context.Context) ([]PlacementInfo, api.HTTPError) {
func (s *Service) GetPlacements(ctx context.Context) ([]PlacementInfo, api.HTTPError) {
var err error
defer mon.Task()(&ctx)(&err)

View File

@ -17,7 +17,6 @@ import (
"storj.io/common/errs2"
ui "storj.io/storj/satellite/admin/back-office/ui"
"storj.io/storj/satellite/overlay"
)
// PathPrefix is the path that will be prefixed to the router passed to the NewServer constructor.
@ -44,9 +43,8 @@ type Config struct {
// Server serves the API endpoints and the web application to allow preforming satellite
// administration tasks.
type Server struct {
log *zap.Logger
listener net.Listener
placement *overlay.PlacementDefinitions
log *zap.Logger
listener net.Listener
config Config
@ -60,15 +58,14 @@ type Server struct {
func NewServer(
log *zap.Logger,
listener net.Listener,
placement *overlay.PlacementDefinitions,
service *Service,
root *mux.Router,
config Config,
) *Server {
server := &Server{
log: log,
listener: listener,
placement: placement,
config: config,
log: log,
listener: listener,
config: config,
}
if root == nil {
@ -77,7 +74,8 @@ func NewServer(
// API endpoints.
// API generator already add the PathPrefix.
NewPlacementManagement(log, mon, server, root)
NewPlacementManagement(log, mon, service, root)
NewUserManagement(log, mon, service, root)
root = root.PathPrefix(PathPrefix).Subrouter()
// Static assets for the web interface.

View File

@ -0,0 +1,38 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
package admin
import (
"go.uber.org/zap"
"storj.io/storj/satellite/accounting"
"storj.io/storj/satellite/console"
"storj.io/storj/satellite/overlay"
)
// Service provides functionality for administrating satellites.
type Service struct {
log *zap.Logger
consoleDB console.DB
accountingDB accounting.ProjectAccounting
accounting *accounting.Service
placement *overlay.PlacementDefinitions
}
// NewService creates a new satellite administration service.
func NewService(
log *zap.Logger,
consoleDB console.DB,
accountingDB accounting.ProjectAccounting,
accounting *accounting.Service,
placement *overlay.PlacementDefinitions,
) *Service {
return &Service{
log: log,
consoleDB: consoleDB,
accountingDB: accountingDB,
accounting: accounting,
placement: placement,
}
}

View File

@ -2,12 +2,36 @@
// DO NOT EDIT.
import { HttpClient } from '@/utils/httpClient';
import { Time, UUID } from '@/types/common';
export class PlacementInfo {
id: number;
location: string;
}
export class ProjectUsageLimits {
id: UUID;
name: string;
storageLimit: number;
storageUsed: number | null;
bandwidthLimit: number;
bandwidthUsed: number;
segmentLimit: number;
segmentUsed: number | null;
}
export class User {
id: UUID;
fullName: string;
email: string;
paidTier: boolean;
createdAt: Time;
status: string;
userAgent: string;
defaultPlacement: number;
projectUsageLimits: ProjectUsageLimits[] | null;
}
class APIError extends Error {
constructor(
public readonly msg: string,
@ -31,3 +55,18 @@ export class PlacementManagementHttpApiV1 {
throw new APIError(err.error, response.status);
}
}
export class UserManagementHttpApiV1 {
private readonly http: HttpClient = new HttpClient();
private readonly ROOT_PATH: string = '/back-office/api/v1/users';
public async getUserByEmail(email: string): Promise<User> {
const fullPath = `${this.ROOT_PATH}/${email}`;
const response = await this.http.get(fullPath);
if (response.ok) {
return response.json().then((body) => body as User);
}
const err = await response.json();
throw new APIError(err.error, response.status);
}
}

View File

@ -0,0 +1,7 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
// TODO: fully implement these types and their methods according to their Go counterparts
export type UUID = string;
export type MemorySize = string;
export type Time = string;

View File

@ -0,0 +1,159 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
package admin
import (
"context"
"database/sql"
"errors"
"net/http"
"time"
"go.uber.org/zap"
"storj.io/common/storj"
"storj.io/common/uuid"
"storj.io/storj/private/api"
"storj.io/storj/satellite/accounting"
)
// User holds information about a user account.
type User struct {
ID uuid.UUID `json:"id"`
FullName string `json:"fullName"`
Email string `json:"email"`
PaidTier bool `json:"paidTier"`
CreatedAt time.Time `json:"createdAt"`
Status string `json:"status"`
UserAgent string `json:"userAgent"`
DefaultPlacement storj.PlacementConstraint `json:"defaultPlacement"`
ProjectUsageLimits []ProjectUsageLimits `json:"projectUsageLimits"`
}
// ProjectUsageLimits holds project usage limits and current usage.
// StorageUsed, BandwidthUsed, and SegmentUsed are nil if there was
// an error connecting to the Redis live accounting cache.
type ProjectUsageLimits struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
StorageLimit int64 `json:"storageLimit"`
StorageUsed *int64 `json:"storageUsed"`
BandwidthLimit int64 `json:"bandwidthLimit"`
BandwidthUsed int64 `json:"bandwidthUsed"`
SegmentLimit int64 `json:"segmentLimit"`
SegmentUsed *int64 `json:"segmentUsed"`
}
// GetUserByEmail returns a verified user by its email address.
func (s *Service) GetUserByEmail(ctx context.Context, email string) (*User, api.HTTPError) {
var err error
defer mon.Task()(&ctx)(&err)
user, err := s.consoleDB.Users().GetByEmail(ctx, email)
if err != nil {
status := http.StatusInternalServerError
if errors.Is(err, sql.ErrNoRows) {
status = http.StatusNotFound
}
return nil, api.HTTPError{
Status: status,
Err: Error.Wrap(err),
}
}
projects, err := s.consoleDB.Projects().GetOwn(ctx, user.ID)
if err != nil {
return nil, api.HTTPError{
Status: http.StatusInternalServerError,
Err: Error.Wrap(err),
}
}
// We return status 409 in the rare case that a project is deleted
// before its limits can be obtained.
makeDBErr := func(err error) api.HTTPError {
status := http.StatusInternalServerError
if errors.Is(err, sql.ErrNoRows) {
status = http.StatusConflict
}
return api.HTTPError{
Status: status,
Err: Error.Wrap(err),
}
}
var cacheErrs []error
usageLimits := make([]ProjectUsageLimits, 0, len(projects))
for _, project := range projects {
usage := ProjectUsageLimits{
ID: project.ID,
Name: project.Name,
}
storageLimit, err := s.accounting.GetProjectStorageLimit(ctx, project.ID)
if err != nil {
return nil, makeDBErr(err)
}
usage.StorageLimit = storageLimit.Int64()
bandwidthLimit, err := s.accounting.GetProjectBandwidthLimit(ctx, project.ID)
if err != nil {
return nil, makeDBErr(err)
}
usage.BandwidthLimit = bandwidthLimit.Int64()
segmentLimit, err := s.accounting.GetProjectSegmentLimit(ctx, project.ID)
if err != nil {
return nil, makeDBErr(err)
}
usage.SegmentLimit = segmentLimit.Int64()
storageUsed, err := s.accounting.GetProjectStorageTotals(ctx, project.ID)
if err == nil {
usage.StorageUsed = &storageUsed
} else if accounting.ErrSystemOrNetError.Has(err) {
cacheErrs = append(cacheErrs, err)
} else {
return nil, api.HTTPError{
Status: http.StatusInternalServerError,
Err: Error.Wrap(err),
}
}
usage.BandwidthUsed, err = s.accounting.GetProjectBandwidthTotals(ctx, project.ID)
if err != nil {
return nil, makeDBErr(err)
}
segmentUsed, err := s.accounting.GetProjectSegmentTotals(ctx, project.ID)
if err == nil {
usage.SegmentUsed = &segmentUsed
} else if accounting.ErrSystemOrNetError.Has(err) {
cacheErrs = append(cacheErrs, err)
} else {
return nil, api.HTTPError{
Status: http.StatusInternalServerError,
Err: Error.Wrap(err),
}
}
usageLimits = append(usageLimits, usage)
}
if len(cacheErrs) != 0 {
s.log.Warn("Error getting project usage data from live accounting cache", zap.Errors("errors", cacheErrs))
}
return &User{
ID: user.ID,
FullName: user.FullName,
Email: user.Email,
PaidTier: user.PaidTier,
CreatedAt: user.CreatedAt,
Status: user.Status.String(),
UserAgent: string(user.UserAgent),
DefaultPlacement: user.DefaultPlacement,
ProjectUsageLimits: usageLimits,
}, api.HTTPError{}
}

View File

@ -0,0 +1,168 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
package admin_test
import (
"fmt"
"net/http"
"sort"
"testing"
"time"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"storj.io/common/memory"
"storj.io/common/pb"
"storj.io/common/testcontext"
"storj.io/common/testrand"
"storj.io/storj/private/testplanet"
"storj.io/storj/satellite"
"storj.io/storj/satellite/buckets"
"storj.io/storj/satellite/console"
"storj.io/storj/satellite/metabase"
"storj.io/storj/satellite/metabase/metabasetest"
)
func TestGetUser(t *testing.T) {
testplanet.Run(t, testplanet.Config{
SatelliteCount: 1,
Reconfigure: testplanet.Reconfigure{
Satellite: func(log *zap.Logger, index int, config *satellite.Config) {
config.LiveAccounting.AsOfSystemInterval = 0
},
},
}, func(t *testing.T, ctx *testcontext.Context, planet *testplanet.Planet) {
sat := planet.Satellites[0]
service := sat.Admin.Admin.Service
consoleDB := sat.DB.Console()
consoleUser := &console.User{
ID: testrand.UUID(),
FullName: "Test User",
Email: "test@storj.io",
PasswordHash: testrand.Bytes(8),
Status: console.Inactive,
UserAgent: []byte("agent"),
DefaultPlacement: 5,
}
_, apiErr := service.GetUserByEmail(ctx, consoleUser.Email)
require.Equal(t, http.StatusNotFound, apiErr.Status)
require.Error(t, apiErr.Err)
_, err := consoleDB.Users().Insert(ctx, consoleUser)
require.NoError(t, err)
consoleUser.PaidTier = true
require.NoError(t, sat.DB.Console().Users().Update(ctx, consoleUser.ID, console.UpdateUserRequest{PaidTier: &consoleUser.PaidTier}))
_, apiErr = service.GetUserByEmail(ctx, consoleUser.Email)
require.Equal(t, http.StatusNotFound, apiErr.Status)
require.Error(t, apiErr.Err)
consoleUser.Status = console.Active
require.NoError(t, consoleDB.Users().Update(ctx, consoleUser.ID, console.UpdateUserRequest{Status: &consoleUser.Status}))
user, apiErr := service.GetUserByEmail(ctx, consoleUser.Email)
require.NoError(t, apiErr.Err)
require.NotNil(t, user)
require.Equal(t, consoleUser.ID, user.ID)
require.Equal(t, consoleUser.FullName, user.FullName)
require.Equal(t, consoleUser.Email, user.Email)
require.Equal(t, consoleUser.PaidTier, user.PaidTier)
require.Equal(t, consoleUser.Status.String(), user.Status)
require.Equal(t, string(consoleUser.UserAgent), user.UserAgent)
require.Equal(t, consoleUser.DefaultPlacement, user.DefaultPlacement)
require.Empty(t, user.ProjectUsageLimits)
type expectedTotal struct {
storage int64
segments int64
bandwidth int64
objects int64
}
var projects []*console.Project
var expectedTotals []expectedTotal
for projNum := 1; projNum <= 2; projNum++ {
storageLimit := memory.GB * memory.Size(projNum)
bandwidthLimit := memory.GB * memory.Size(projNum*2)
segmentLimit := int64(1000 * projNum)
proj := &console.Project{
ID: testrand.UUID(),
Name: fmt.Sprintf("Project %d", projNum),
OwnerID: consoleUser.ID,
StorageLimit: &storageLimit,
BandwidthLimit: &bandwidthLimit,
SegmentLimit: &segmentLimit,
}
projects = append(projects, proj)
_, err := consoleDB.Projects().Insert(ctx, proj)
require.NoError(t, err)
_, err = consoleDB.ProjectMembers().Insert(ctx, user.ID, proj.ID)
require.NoError(t, err)
bucket, err := sat.DB.Buckets().CreateBucket(ctx, buckets.Bucket{
ID: testrand.UUID(),
Name: testrand.BucketName(),
ProjectID: proj.ID,
})
require.NoError(t, err)
total := expectedTotal{}
for objNum := 0; objNum < projNum*10; objNum++ {
obj := metabasetest.CreateObject(ctx, t, sat.Metabase.DB, metabase.ObjectStream{
ProjectID: proj.ID,
BucketName: bucket.Name,
ObjectKey: metabasetest.RandObjectKey(),
Version: 12345,
StreamID: testrand.UUID(),
}, byte(16*projNum))
total.storage += obj.TotalEncryptedSize
total.segments += int64(obj.SegmentCount)
total.objects++
}
testBandwidth := int64(2000 * projNum)
err = sat.DB.Orders().UpdateBucketBandwidthAllocation(ctx, proj.ID, []byte(bucket.Name), pb.PieceAction_GET, testBandwidth, time.Now())
require.NoError(t, err)
total.bandwidth += testBandwidth
expectedTotals = append(expectedTotals, total)
}
sat.Accounting.Tally.Loop.TriggerWait()
user, apiErr = service.GetUserByEmail(ctx, consoleUser.Email)
require.NoError(t, apiErr.Err)
require.NotNil(t, user)
require.Len(t, user.ProjectUsageLimits, len(projects))
sort.Slice(user.ProjectUsageLimits, func(i, j int) bool {
return user.ProjectUsageLimits[i].Name < user.ProjectUsageLimits[j].Name
})
for i, info := range user.ProjectUsageLimits {
proj := projects[i]
name := proj.Name
require.Equal(t, proj.ID, info.ID, name)
require.Equal(t, name, info.Name, name)
require.EqualValues(t, *proj.StorageLimit, info.StorageLimit, name)
require.EqualValues(t, *proj.BandwidthLimit, info.BandwidthLimit, name)
require.Equal(t, *proj.SegmentLimit, info.SegmentLimit, name)
total := expectedTotals[i]
require.Equal(t, total.storage, *info.StorageUsed, name)
require.Equal(t, total.bandwidth, info.BandwidthUsed, name)
require.Equal(t, total.segments, *info.SegmentUsed, name)
}
})
}

View File

@ -29,7 +29,6 @@ import (
"storj.io/storj/satellite/console/consoleweb"
"storj.io/storj/satellite/console/restkeys"
"storj.io/storj/satellite/oidc"
"storj.io/storj/satellite/overlay"
"storj.io/storj/satellite/payments"
"storj.io/storj/satellite/payments/stripe"
)
@ -106,7 +105,7 @@ func NewServer(
freezeAccounts *console.AccountFreezeService,
analyticsService *analytics.Service,
accounts payments.Accounts,
placement *overlay.PlacementDefinitions,
backOfficeService *backoffice.Service,
console consoleweb.Config,
config Config,
) *Server {
@ -185,7 +184,13 @@ func NewServer(
// NewServer adds the backoffice.PahtPrefix for the static assets, but not for the API because the
// generator already add the PathPrefix to router when the API handlers are hooked.
_ = backoffice.NewServer(log.Named("back-office"), nil, placement, root, config.BackOffice)
_ = backoffice.NewServer(
log.Named("back-office"),
nil,
backOfficeService,
root,
config.BackOffice,
)
// 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.

View File

@ -174,6 +174,24 @@ const (
LegalHold UserStatus = 4
)
// String returns a string representation of the user status.
func (s UserStatus) String() string {
switch s {
case Inactive:
return "Inactive"
case Active:
return "Active"
case Deleted:
return "Deleted"
case PendingDeletion:
return "Pending Deletion"
case LegalHold:
return "Legal Hold"
default:
return ""
}
}
// User is a database object that describes User entity.
type User struct {
ID uuid.UUID `json:"id"`