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:
parent
220920edb9
commit
d2819522c6
@ -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
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
@ -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"))
|
||||
|
@ -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)))
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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.
|
||||
|
38
satellite/admin/back-office/service.go
Normal file
38
satellite/admin/back-office/service.go
Normal 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,
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
7
satellite/admin/back-office/ui/src/types/common.ts
Normal file
7
satellite/admin/back-office/ui/src/types/common.ts
Normal 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;
|
159
satellite/admin/back-office/users.go
Normal file
159
satellite/admin/back-office/users.go
Normal 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{}
|
||||
}
|
168
satellite/admin/back-office/users_test.go
Normal file
168
satellite/admin/back-office/users_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
@ -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.
|
||||
|
@ -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"`
|
||||
|
Loading…
Reference in New Issue
Block a user