Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 137 additions & 8 deletions backend/app/admin_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,47 @@ type MaintenanceModeStatus struct {
Enabled bool `json:"enabled"`
}

// @Summary Get users (paginated)
// @Description Returns a paginated list of users with total count
// @Tags admin
// @ID get-users-paginated
// @Accept json
// @Produce json
// @Param limit query int false "Limit the number of users returned (default: 20, max: 100)"
// @Param offset query int false "Offset for pagination (default: 0)"
// @Success 200 {object} APIResponse{data=object{users=[]UserResponse,count=int,limit=int,offset=int}}
// @Failure 400 {object} APIResponse "Invalid pagination parameters"
// @Failure 500 {object} APIResponse
// @Security AdminMiddleware
// @Router /users/paginated [get]
// ListUsersPaginatedHandler lists users with pagination
func (h *Handler) ListUsersPaginatedHandler(c *gin.Context) {
limit, offset, ok := bindPagination(c)
if !ok {
return
}

users, total, err := h.db.ListUsersPaginated(limit, offset)
if err != nil {
logger.GetLogger().Error().Err(err).Msg("failed to list users (paginated)")
InternalServerError(c)
return
}

usersWithBalance, err := h.buildUsersWithBalance(users)
if err != nil {
InternalServerError(c)
return
}

Success(c, http.StatusOK, "Users are retrieved successfully", PaginatedData[UserResponse]{
Items: usersWithBalance,
Total: total,
Limit: limit,
Offset: offset,
})
}

// @Summary Get all users
// @Description Returns a list of all users
// @Tags admin
Expand All @@ -91,6 +132,16 @@ func (h *Handler) ListUsersHandler(c *gin.Context) {
InternalServerError(c)
return
}
usersWithBalance, err := h.buildUsersWithBalance(users)
if err != nil {
InternalServerError(c)
return
}
Success(c, http.StatusOK, "Users are retrieved successfully", usersWithBalance)
}

// buildUsersWithBalance enriches users with USD balance using controlled concurrency
func (h *Handler) buildUsersWithBalance(users []models.User) ([]UserResponse, error) {
var usersWithBalance []UserResponse
const maxConcurrentBalanceFetches = 20
balanceConcurrencyLimiter := make(chan struct{}, maxConcurrentBalanceFetches)
Expand Down Expand Up @@ -129,17 +180,11 @@ func (h *Handler) ListUsersHandler(c *gin.Context) {
}

wg.Wait()

// Check if there were any errors during balance fetching
if multiErr != nil {
logger.GetLogger().Error().Err(multiErr).Msg("errors occurred while fetching user balances")
InternalServerError(c)
return
return nil, multiErr
}

Success(c, http.StatusOK, "Users are retrieved successfully", map[string]interface{}{
"users": usersWithBalance,
})
return usersWithBalance, nil
}

// @Summary Delete a user
Expand Down Expand Up @@ -285,6 +330,38 @@ func (h *Handler) ListVouchersHandler(c *gin.Context) {
})
}

// @Summary List vouchers (paginated)
// @Description Returns vouchers with pagination
// @Tags admin
// @ID list-vouchers-paginated
// @Accept json
// @Produce json
// @Param limit query int false "Limit"
// @Param offset query int false "Offset"
// @Success 200 {object} APIResponse{data=object{vouchers=[]models.Voucher,count=int,limit=int,offset=int}}
// @Failure 400 {object} APIResponse
// @Failure 500 {object} APIResponse
// @Security AdminMiddleware
// @Router /vouchers/paginated [get]
func (h *Handler) ListVouchersPaginatedHandler(c *gin.Context) {
limit, offset, ok := bindPagination(c)
if !ok {
return
}
vouchers, total, err := h.db.ListVouchersPaginated(limit, offset)
if err != nil {
logger.GetLogger().Error().Err(err).Msg("failed to list all vouchers")
InternalServerError(c)
return
}
Success(c, http.StatusOK, "Vouchers are Retrieved successfully", PaginatedData[models.Voucher]{
Items: vouchers,
Total: total,
Limit: limit,
Offset: offset,
})
}

// @Summary Credit user balance
// @Description Credits a specific user's balance
// @Tags admin
Expand Down Expand Up @@ -415,6 +492,58 @@ func (h *Handler) ListPendingRecordsHandler(c *gin.Context) {
})
}

// @Summary List pending records (paginated)
// @Description Returns pending records with pagination
// @Tags admin
// @ID list-pending-records-paginated
// @Accept json
// @Produce json
// @Param limit query int false "Limit"
// @Param offset query int false "Offset"
// @Success 200 {object} APIResponse{data=object{pending_records=[]PendingRecordsResponse,count=int,limit=int,offset=int}}
// @Failure 400 {object} APIResponse
// @Failure 500 {object} APIResponse
// @Security AdminMiddleware
// @Router /pending-records/paginated [get]
func (h *Handler) ListPendingRecordsPaginatedHandler(c *gin.Context) {
limit, offset, ok := bindPagination(c)
if !ok {
return
}
records, total, err := h.db.ListAllPendingRecordsPaginated(limit, offset)
if err != nil {
logger.GetLogger().Error().Err(err).Msg("failed to list all pending records")
InternalServerError(c)
return
}
var pendingRecordsResponse []PendingRecordsResponse
for _, record := range records {
usdAmount, err := internal.FromTFTtoUSDMillicent(h.substrateClient, record.TFTAmount)
if err != nil {
logger.GetLogger().Error().Err(err).Msg("failed to convert tft to usd amount")
InternalServerError(c)
return
}
usdTransferredAmount, err := internal.FromTFTtoUSDMillicent(h.substrateClient, record.TransferredTFTAmount)
if err != nil {
logger.GetLogger().Error().Err(err).Msg("failed to convert tft to usd transferred amount")
InternalServerError(c)
return
}
pendingRecordsResponse = append(pendingRecordsResponse, PendingRecordsResponse{
PendingRecord: record,
USDAmount: internal.FromUSDMilliCentToUSD(usdAmount),
TransferredUSDAmount: internal.FromUSDMilliCentToUSD(usdTransferredAmount),
})
}
Success(c, http.StatusOK, "Pending records are retrieved successfully", PaginatedData[PendingRecordsResponse]{
Items: pendingRecordsResponse,
Total: total,
Limit: limit,
Offset: offset,
})
}

// Only accessible by admins
// @Summary Send mail to all users
// @Description Allows admin to send a custom email to all users with optional file attachments. Returns detailed statistics about successful and failed email deliveries.
Expand Down
5 changes: 1 addition & 4 deletions backend/app/admin_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,7 @@ func TestListUsersHandler(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, "Users are retrieved successfully", usersResp["message"])

data, ok := usersResp["data"].(map[string]interface{})
assert.True(t, ok)
usersRaw, ok := data["users"]
assert.True(t, ok)
usersRaw := usersResp["data"]
usersBytes, err := json.Marshal(usersRaw)
assert.NoError(t, err)

Expand Down
10 changes: 10 additions & 0 deletions backend/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,18 +268,22 @@ func (app *App) registerHandlers() {
usersGroup := adminGroup.Group("/users")
{
usersGroup.GET("", app.handlers.ListUsersHandler)
usersGroup.GET("/paginated", app.handlers.ListUsersPaginatedHandler)
usersGroup.DELETE("/:user_id", app.handlers.DeleteUsersHandler)
usersGroup.POST("/:user_id/credit", app.handlers.CreditUserHandler)
}
usersGroup.POST("/mail", app.handlers.SendMailToAllUsersHandler)

adminGroup.GET("/invoices", app.handlers.ListAllInvoicesHandler)
adminGroup.GET("/invoices/paginated", app.handlers.ListAllInvoicesPaginatedHandler)
adminGroup.GET("/pending-records", app.handlers.ListPendingRecordsHandler)
adminGroup.GET("/pending-records/paginated", app.handlers.ListPendingRecordsPaginatedHandler)

vouchersGroup := adminGroup.Group("/vouchers")
{
vouchersGroup.POST("/generate", app.handlers.GenerateVouchersHandler)
vouchersGroup.GET("", app.handlers.ListVouchersHandler)
vouchersGroup.GET("/paginated", app.handlers.ListVouchersPaginatedHandler)

}

Expand All @@ -306,17 +310,22 @@ func (app *App) registerHandlers() {
authGroup.PUT("/change_password", app.handlers.ChangePasswordHandler)
authGroup.GET("/nodes", app.handlers.ListNodesHandler)
authGroup.GET("/nodes/rentable", app.handlers.ListRentableNodesHandler)
authGroup.GET("/nodes/rentable/paginated", app.handlers.ListRentableNodesPaginatedHandler)
authGroup.GET("/nodes/rented", app.handlers.ListRentedNodesHandler)
authGroup.GET("/nodes/rented/paginated", app.handlers.ListRentedNodesPaginatedHandler)
authGroup.POST("/nodes/:node_id", app.handlers.ReserveNodeHandler)
authGroup.DELETE("/nodes/unreserve/:contract_id", app.handlers.UnreserveNodeHandler)
authGroup.POST("/balance/charge", app.handlers.ChargeBalance)
authGroup.GET("/balance", app.handlers.GetUserBalance)
authGroup.PUT("/redeem/:voucher_code", app.handlers.RedeemVoucherHandler)
authGroup.GET("/invoice/:invoice_id", app.handlers.DownloadInvoiceHandler)
authGroup.GET("/invoice", app.handlers.ListUserInvoicesHandler)
authGroup.GET("/invoice/paginated", app.handlers.ListUserInvoicesPaginatedHandler)
authGroup.GET("/pending-records", app.handlers.ListUserPendingRecordsHandler)
authGroup.GET("/pending-records/paginated", app.handlers.ListUserPendingRecordsPaginatedHandler)
// SSH Key management
authGroup.GET("/ssh-keys", app.handlers.ListSSHKeysHandler)
authGroup.GET("/ssh-keys/paginated", app.handlers.ListSSHKeysPaginatedHandler)
authGroup.POST("/ssh-keys", app.handlers.AddSSHKeyHandler)
authGroup.DELETE("/ssh-keys/:ssh_key_id", app.handlers.DeleteSSHKeyHandler)
}
Expand All @@ -331,6 +340,7 @@ func (app *App) registerHandlers() {
{
deploymentGroup.POST("", app.handlers.HandleDeployCluster)
deploymentGroup.GET("", app.handlers.HandleListDeployments)
deploymentGroup.GET("/paginated", app.handlers.HandleListDeploymentsPaginated)
deploymentGroup.DELETE("", app.handlers.HandleDeleteAllDeployments)
deploymentGroup.GET("/:name", app.handlers.HandleGetDeployment)
deploymentGroup.GET("/:name/kubeconfig", app.handlers.HandleGetKubeconfig)
Expand Down
53 changes: 53 additions & 0 deletions backend/app/deployment_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,59 @@ func (h *Handler) HandleListDeployments(c *gin.Context) {
})
}

// @Summary List deployments (paginated)
// @Description Retrieves a paginated list of deployments for the authenticated user
// @Tags deployments
// @Security BearerAuth
// @Produce json
// @Param limit query int false "Limit"
// @Param offset query int false "Offset"
// @Success 200 {object} APIResponse{data=PaginatedData[DeploymentResponse]} "Deployments retrieved successfully"
// @Failure 401 {object} APIResponse "Unauthorized"
// @Failure 500 {object} APIResponse "Internal server error"
// @Router /deployments/paginated [get]
func (h *Handler) HandleListDeploymentsPaginated(c *gin.Context) {
userID := c.GetInt("user_id")
if userID == 0 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}

limit, offset, ok := bindPagination(c)
if !ok {
return
}
clusters, total, err := h.db.ListUserClustersPaginated(userID, limit, offset)
if err != nil {
logger.GetLogger().Error().Err(err).Int("user_id", userID).Msg("Failed to list user clusters")
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve deployments"})
return
}

deployments := make([]gin.H, 0, len(clusters))
for _, cluster := range clusters {
clusterResult, err := cluster.GetClusterResult()
if err != nil {
logger.GetLogger().Error().Err(err).Int("cluster_id", cluster.ID).Msg("Failed to deserialize cluster result")
continue
}
deployments = append(deployments, gin.H{
"id": cluster.ID,
"project_name": cluster.ProjectName,
"cluster": clusterResult,
"created_at": cluster.CreatedAt,
"updated_at": cluster.UpdatedAt,
})
}

Success(c, http.StatusOK, "Deployments retrieved successfully", PaginatedData[gin.H]{
Items: deployments,
Total: total,
Limit: limit,
Offset: offset,
})
}

// @Summary Get deployment
// @Description Retrieves details of a specific deployment by name
// @Tags deployments
Expand Down
69 changes: 68 additions & 1 deletion backend/app/invoice_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import (

"time"

"github.com/gin-gonic/gin"
"kubecloud/internal/logger"

"github.com/gin-gonic/gin"
)

// @Summary Get all invoices
Expand All @@ -38,6 +39,39 @@ func (h *Handler) ListAllInvoicesHandler(c *gin.Context) {
})
}

// @Summary Get all invoices (paginated)
// @Description Returns a paginated list of all invoices
// @Tags admin
// @ID get-all-invoices-paginated
// @Accept json
// @Produce json
// @Param limit query int false "Limit"
// @Param offset query int false "Offset"
// @Success 200 {object} APIResponse{data=object{invoices=[]models.Invoice,count=int,limit=int,offset=int}}
// @Failure 400 {object} APIResponse
// @Failure 500 {object} APIResponse
// @Security AdminMiddleware
// @Router /invoices/paginated [get]
func (h *Handler) ListAllInvoicesPaginatedHandler(c *gin.Context) {
limit, offset, ok := bindPagination(c)
if !ok {
return
}
invoices, total, err := h.db.ListInvoicesPaginated(limit, offset)
if err != nil {
logger.GetLogger().Error().Err(err).Send()
InternalServerError(c)
return
}

Success(c, http.StatusOK, "Invoices are retrieved successfully", PaginatedData[models.Invoice]{
Items: invoices,
Total: total,
Limit: limit,
Offset: offset,
})
}

// @Summary Get invoices
// @Description Returns a list of invoices for a user
// @Tags invoices
Expand All @@ -64,6 +98,39 @@ func (h *Handler) ListUserInvoicesHandler(c *gin.Context) {
})
}

// @Summary Get invoices (paginated)
// @Description Returns a paginated list of invoices for a user
// @Tags invoices
// @ID get-invoices-paginated
// @Accept json
// @Produce json
// @Param limit query int false "Limit"
// @Param offset query int false "Offset"
// @Success 200 {object} APIResponse{data=object{invoices=[]models.Invoice,count=int,limit=int,offset=int}}
// @Failure 400 {object} APIResponse
// @Failure 500 {object} APIResponse
// @Security UserMiddleware
// @Router /user/invoice/paginated [get]
func (h *Handler) ListUserInvoicesPaginatedHandler(c *gin.Context) {
userID := c.GetInt("user_id")
limit, offset, ok := bindPagination(c)
if !ok {
return
}
invoices, total, err := h.db.ListUserInvoicesPaginated(userID, limit, offset)
if err != nil {
logger.GetLogger().Error().Err(err).Send()
InternalServerError(c)
return
}
Success(c, http.StatusOK, "Invoices are retrieved successfully", PaginatedData[models.Invoice]{
Items: invoices,
Total: total,
Limit: limit,
Offset: offset,
})
}

func (h *Handler) MonthlyInvoicesHandler() {
var lastProcessedMonth time.Month
var lastProcessedYear int
Expand Down
Loading
Loading