Skip to content

Commit

Permalink
satellite/admin: list users pending deletion
Browse files Browse the repository at this point in the history
This change adds an endpoint to the admin API and UI to get a list of
users pending deletion and have no unpaid invoice.

Issue: #6410

Change-Id: I906dbf9eee9e7469e45f0c622a891867bf0cc201
  • Loading branch information
wilfred-asomanii authored and Storj Robot committed Oct 30, 2023
1 parent 405491e commit 513c3cc
Show file tree
Hide file tree
Showing 9 changed files with 409 additions and 1 deletion.
9 changes: 8 additions & 1 deletion satellite/admin/README.md
Expand Up @@ -24,6 +24,7 @@ Requires setting `Authorization` header for requests.
* [PUT /api/users/{user-email}/violation-freeze](#put-apiusersuser-emailviolation-freeze)
* [DELETE /api/users/{user-email}/violation-freeze](#delete-apiusersuser-emailviolation-freeze)
* [DELETE /api/users/{user-email}/billing-warning](#delete-apiusersuser-emailbilling-warning)
* [GET /api/users/pending-deletion](#get-apiuserspending-deletion)
* [PATCH /api/users/{user-email}/geofence](#patch-apiusersuser-emailgeofence)
* [DELETE /api/users/{user-email}/geofence](#delete-apiusersuser-emailgeofence)
* [OAuth Client Management](#oauth-client-management)
Expand Down Expand Up @@ -199,6 +200,12 @@ User status is set back to Active. This is the only way to exit the violation fr

Removes the billing warning status from a user's account.

#### GET /api/users/pending-deletion

Returns a limited list of users pending deletion and have no unpaid invoices.
Required parameters: `limit` and `page`.
Example: `/api/users/pending-deletion?limit=10&page=1`

#### PATCH /api/users/{user-email}/geofence

Sets the account level geofence for the user.
Expand Down Expand Up @@ -454,7 +461,7 @@ A successful response body:
},
"project": {
"id": "12345678-1234-1234-1234-123456789abc",
"name": "My Project",
"name": "My Project"
},
"owner": {
"id": "12345678-1234-1234-1234-123456789abc",
Expand Down
1 change: 1 addition & 0 deletions satellite/admin/server.go
Expand Up @@ -174,6 +174,7 @@ func NewServer(
limitUpdateAPI.HandleFunc("/users/{useremail}/billing-warning", server.billingUnWarnUser).Methods("DELETE")
limitUpdateAPI.HandleFunc("/users/{useremail}/violation-freeze", server.violationFreezeUser).Methods("PUT")
limitUpdateAPI.HandleFunc("/users/{useremail}/violation-freeze", server.violationUnfreezeUser).Methods("DELETE")
limitUpdateAPI.HandleFunc("/users/pending-deletion", server.usersPendingDeletion).Methods("GET")
limitUpdateAPI.HandleFunc("/projects/{project}/limit", server.getProjectLimit).Methods("GET")
limitUpdateAPI.HandleFunc("/projects/{project}/limit", server.putProjectLimit).Methods("PUT", "POST")

Expand Down
11 changes: 11 additions & 0 deletions satellite/admin/ui/src/lib/api.ts
Expand Up @@ -343,6 +343,17 @@ export class Admin {
return this.fetch('GET', `users/${email}`);
}
},
{
name: 'get users pending deletion',
desc: 'Get the information of a users pending deletion and have no unpaid invoices',
params: [
['Limit', new InputText('number', true)],
['Page', new InputText('number', true)]
],
func: async (limit: number, page: number): Promise<Record<string, unknown>> => {
return this.fetch('GET', `users-pending-deletion?limit=${limit}&page=${page}`);
}
},
{
name: 'get user limits',
desc: 'Get the current limits for a user',
Expand Down
90 changes: 90 additions & 0 deletions satellite/admin/user.go
Expand Up @@ -206,6 +206,96 @@ func (server *Server) userInfo(w http.ResponseWriter, r *http.Request) {
sendJSONData(w, http.StatusOK, data)
}

func (server *Server) usersPendingDeletion(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

type User struct {
ID uuid.UUID `json:"id"`
FullName string `json:"fullName"`
Email string `json:"email"`
}

query := r.URL.Query()

limitParam := query.Get("limit")
if limitParam == "" {
sendJSONError(w, "Bad request", "parameter 'limit' can't be empty", http.StatusBadRequest)
return
}

limit, err := strconv.ParseUint(limitParam, 10, 32)
if err != nil {
sendJSONError(w, "Bad request", err.Error(), http.StatusBadRequest)
return
}

pageParam := query.Get("page")
if pageParam == "" {
sendJSONError(w, "Bad request", "parameter 'page' can't be empty", http.StatusBadRequest)
return
}

page, err := strconv.ParseUint(pageParam, 10, 32)
if err != nil {
sendJSONError(w, "Bad request", err.Error(), http.StatusBadRequest)
return
}

var sendingPage struct {
Users []User `json:"users"`
PageCount uint `json:"pageCount"`
CurrentPage uint `json:"currentPage"`
TotalCount uint64 `json:"totalCount"`
HasMore bool `json:"hasMore"`
}
usersPage, err := server.db.Console().Users().GetByStatus(
ctx, console.PendingDeletion, console.UserCursor{
Limit: uint(limit),
Page: uint(page),
},
)
if err != nil {
sendJSONError(w, "failed retrieving a usersPage of users", err.Error(), http.StatusInternalServerError)
return
}

sendingPage.PageCount = usersPage.PageCount
sendingPage.CurrentPage = usersPage.CurrentPage
sendingPage.TotalCount = usersPage.TotalCount
sendingPage.Users = make([]User, 0, len(usersPage.Users))

if sendingPage.PageCount > sendingPage.CurrentPage {
sendingPage.HasMore = true
}

for _, user := range usersPage.Users {
invoices, err := server.payments.Invoices().ListFailed(ctx, &user.ID)
if err != nil {
sendJSONError(w, "getting invoices failed",
err.Error(), http.StatusInternalServerError)
return
}
if len(invoices) != 0 {
sendingPage.TotalCount--
continue
}
sendingPage.Users = append(sendingPage.Users, User{
ID: user.ID,
FullName: user.FullName,
Email: user.Email,
})
}

data, err := json.Marshal(sendingPage)
if err != nil {
sendJSONError(w, "json encoding failed",
err.Error(), http.StatusInternalServerError)
return
}

sendJSONData(w, http.StatusOK, data)
}

func (server *Server) userLimits(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

Expand Down
20 changes: 20 additions & 0 deletions satellite/console/users.go
Expand Up @@ -30,6 +30,8 @@ type Users interface {
UpdateFailedLoginCountAndExpiration(ctx context.Context, failedLoginPenalty *float64, id uuid.UUID) error
// GetByEmailWithUnverified is a method for querying users by email from the database.
GetByEmailWithUnverified(ctx context.Context, email string) (*User, []User, error)
// GetByStatus is a method for querying user by status from the database.
GetByStatus(ctx context.Context, status UserStatus, cursor UserCursor) (*UsersPage, error)
// GetByEmail is a method for querying user by verified email from the database.
GetByEmail(ctx context.Context, email string) (*User, error)
// Insert is a method for inserting user into the database.
Expand Down Expand Up @@ -66,6 +68,24 @@ type UserInfo struct {
ShortName string `json:"shortName"`
}

// UserCursor holds info for user info cursor pagination.
type UserCursor struct {
Limit uint `json:"limit"`
Page uint `json:"page"`
}

// UsersPage represent user info page result.
type UsersPage struct {
Users []User `json:"users"`

Limit uint `json:"limit"`
Offset uint64 `json:"offset"`

PageCount uint `json:"pageCount"`
CurrentPage uint `json:"currentPage"`
TotalCount uint64 `json:"totalCount"`
}

// IsValid checks UserInfo validity and returns error describing whats wrong.
// The returned error has the class ErrValiation.
func (user *UserInfo) IsValid() error {
Expand Down
47 changes: 47 additions & 0 deletions satellite/console/users_test.go
Expand Up @@ -336,6 +336,53 @@ func TestGetUserByEmail(t *testing.T) {
})
}

func TestGetUsersByStatus(t *testing.T) {
satellitedbtest.Run(t, func(ctx *testcontext.Context, t *testing.T, db satellite.DB) {
usersRepo := db.Console().Users()

inactiveUser := console.User{
ID: testrand.UUID(),
FullName: "Inactive User",
Email: email,
PasswordHash: []byte("123a123"),
}

_, err := usersRepo.Insert(ctx, &inactiveUser)
require.NoError(t, err)

activeUser := console.User{
ID: testrand.UUID(),
FullName: "Active User",
Email: email,
Status: console.Active,
PasswordHash: []byte("123a123"),
}

_, err = usersRepo.Insert(ctx, &activeUser)
require.NoError(t, err)

// Required to set the active status.
err = usersRepo.Update(ctx, activeUser.ID, console.UpdateUserRequest{
Status: &activeUser.Status,
})
require.NoError(t, err)

cursor := console.UserCursor{
Limit: 50,
Page: 1,
}
usersPage, err := usersRepo.GetByStatus(ctx, console.Inactive, cursor)
require.NoError(t, err)
require.Lenf(t, usersPage.Users, 1, "expected 1 inactive user")
require.Equal(t, inactiveUser.ID, usersPage.Users[0].ID)

usersPage, err = usersRepo.GetByStatus(ctx, console.Active, cursor)
require.NoError(t, err)
require.Lenf(t, usersPage.Users, 1, "expected 1 active user")
require.Equal(t, activeUser.ID, usersPage.Users[0].ID)
})
}

func TestGetUnverifiedNeedingReminder(t *testing.T) {
testplanet.Run(t, testplanet.Config{
Reconfigure: testplanet.Reconfigure{
Expand Down

0 comments on commit 513c3cc

Please sign in to comment.