diff --git a/backend/app/admin_handler.go b/backend/app/admin_handler.go index e0f99a8d..8b7edd8c 100644 --- a/backend/app/admin_handler.go +++ b/backend/app/admin_handler.go @@ -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 @@ -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) @@ -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 @@ -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 @@ -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. diff --git a/backend/app/admin_handler_test.go b/backend/app/admin_handler_test.go index c176f961..915043b7 100644 --- a/backend/app/admin_handler_test.go +++ b/backend/app/admin_handler_test.go @@ -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) diff --git a/backend/app/app.go b/backend/app/app.go index cb3c4508..362708ac 100644 --- a/backend/app/app.go +++ b/backend/app/app.go @@ -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) } @@ -306,7 +310,9 @@ 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) @@ -314,9 +320,12 @@ func (app *App) registerHandlers() { 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) } @@ -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) diff --git a/backend/app/deployment_handler.go b/backend/app/deployment_handler.go index 623926c4..ad57008c 100644 --- a/backend/app/deployment_handler.go +++ b/backend/app/deployment_handler.go @@ -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 diff --git a/backend/app/invoice_handler.go b/backend/app/invoice_handler.go index d3727b20..12838c11 100644 --- a/backend/app/invoice_handler.go +++ b/backend/app/invoice_handler.go @@ -10,8 +10,9 @@ import ( "time" - "github.com/gin-gonic/gin" "kubecloud/internal/logger" + + "github.com/gin-gonic/gin" ) // @Summary Get all invoices @@ -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 @@ -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 diff --git a/backend/app/node_handler.go b/backend/app/node_handler.go index c0081715..37bbfce6 100644 --- a/backend/app/node_handler.go +++ b/backend/app/node_handler.go @@ -338,9 +338,70 @@ func (h *Handler) ListRentableNodesHandler(c *gin.Context) { DiscountPrice: node.PriceUsd * 0.5, }) } - Success(c, http.StatusOK, "Nodes are retrieved successfully", ListNodesWithDiscountResponse{ - Total: count, - Nodes: nodesWithDiscount, + Success(c, http.StatusOK, "Nodes are retrieved successfully", PaginatedData[NodesWithDiscount]{ + Items: nodesWithDiscount, + Total: int64(count), + Limit: int(limit.Size), + Offset: (int(limit.Page) - 1) * int(limit.Size), + }) +} + +// @Summary List rentable nodes (paginated) +// @Description Retrieves a paginated list of rentable nodes from the grid proxy with discount price +// @Tags nodes +// @ID list-rentable-nodes-paginated +// @Accept json +// @Produce json +// @Param limit query int false "Limit the number of nodes returned (default: 20, max: 100)" +// @Param offset query int false "Offset for pagination (default: 0)" +// @Success 200 {object} APIResponse{data=ListNodesWithDiscountResponse} +// @Failure 400 {object} APIResponse "Invalid pagination parameters" +// @Failure 500 {object} APIResponse "Internal server error" +// @Router /user/nodes/rentable/paginated [get] +func (h *Handler) ListRentableNodesPaginatedHandler(c *gin.Context) { + healthy := true + rentable := true + filter := proxyTypes.NodeFilter{ + Healthy: &healthy, + Rentable: &rentable, + Features: zos3NodeFeatures, + } + + limitVal, offset, ok := bindPagination(c) + if !ok { + return + } + // Translate limit/offset -> size/page + page := 1 + if limitVal > 0 { + page = (offset / limitVal) + 1 + } + + limit := proxyTypes.DefaultLimit() + limit.Randomize = true + limit.RetCount = true + limit.Size = uint64(limitVal) + limit.Page = uint64(page) + + nodes, count, err := h.proxyClient.Nodes(c.Request.Context(), filter, limit) + if err != nil { + logger.GetLogger().Error().Err(err).Send() + InternalServerError(c) + return + } + + var nodesWithDiscount []NodesWithDiscount + for _, node := range nodes { + nodesWithDiscount = append(nodesWithDiscount, NodesWithDiscount{ + Node: node, + DiscountPrice: node.PriceUsd * 0.5, + }) + } + Success(c, http.StatusOK, "Nodes are retrieved successfully", PaginatedData[NodesWithDiscount]{ + Items: nodesWithDiscount, + Total: int64(count), + Limit: limitVal, + Offset: offset, }) } @@ -376,6 +437,68 @@ func (h *Handler) ListRentedNodesHandler(c *gin.Context) { }) } +// @Summary List reserved nodes (paginated) +// @Description Returns a paginated list of reserved nodes for a user +// @Tags nodes +// @ID list-reserved-nodes-paginated +// @Accept json +// @Produce json +// @Security UserMiddleware +// @Param limit query int false "Limit the number of nodes returned (default: 20, max: 100)" +// @Param offset query int false "Offset for pagination (default: 0)" +// @Success 200 {object} APIResponse{data=ListNodesWithDiscountResponse} +// @Failure 500 {object} APIResponse +// @Router /user/nodes/rented/paginated [get] +func (h *Handler) ListRentedNodesPaginatedHandler(c *gin.Context) { + userID := c.GetInt("user_id") + limitVal, offset, ok := bindPagination(c) + if !ok { + return + } + + twinID, err := h.getTwinIDFromUserID(userID) + if err != nil { + InternalServerError(c) + return + } + + healthy := true + filter := proxyTypes.NodeFilter{ + RentedBy: &twinID, + Healthy: &healthy, + Features: zos3NodeFeatures, + } + + page := 1 + if limitVal > 0 { + page = (offset / limitVal) + 1 + } + limit := proxyTypes.DefaultLimit() + limit.RetCount = true + limit.Size = uint64(limitVal) + limit.Page = uint64(page) + + nodes, count, err := h.proxyClient.Nodes(c.Request.Context(), filter, limit) + if err != nil { + InternalServerError(c) + return + } + + var nodesWithDiscount []NodesWithDiscount + for _, node := range nodes { + nodesWithDiscount = append(nodesWithDiscount, NodesWithDiscount{ + Node: node, + DiscountPrice: node.PriceUsd * 0.5, + }) + } + Success(c, http.StatusOK, "Nodes are retrieved successfully", PaginatedData[NodesWithDiscount]{ + Items: nodesWithDiscount, + Total: int64(count), + Limit: limitVal, + Offset: offset, + }) +} + // @Summary Unreserve node // @Description Unreserve a node for a user // @Tags nodes diff --git a/backend/app/notification_handler.go b/backend/app/notification_handler.go index e827733b..52e4461a 100644 --- a/backend/app/notification_handler.go +++ b/backend/app/notification_handler.go @@ -5,20 +5,12 @@ import ( "fmt" "kubecloud/models" "net/http" - "strconv" "github.com/gin-gonic/gin" "github.com/google/uuid" "gorm.io/gorm" ) -const ( - // Default pagination values - DefaultNotificationLimit = 20 - MaxNotificationLimit = 100 - DefaultOffset = 0 -) - // @Model NotificationResponse // @Description A notification response // @Property ID string @@ -89,24 +81,6 @@ func getUserIDFromContext(c *gin.Context) (int, error) { return userID, nil } -// validatePaginationParams validates and normalizes pagination parameters -func validatePaginationParams(limitStr, offsetStr string) (int, int, error) { - limit, err := strconv.Atoi(limitStr) - if err != nil || limit <= 0 { - limit = DefaultNotificationLimit - } - if limit > MaxNotificationLimit { - limit = MaxNotificationLimit - } - - offset, err := strconv.Atoi(offsetStr) - if err != nil || offset < 0 { - offset = DefaultOffset - } - - return limit, offset, nil -} - // @Summary Get all notifications // @Description Retrieves all user notifications with pagination // @Tags notifications @@ -126,13 +100,9 @@ func (h *Handler) GetAllNotificationsHandler(c *gin.Context) { return } - // Parse and validate pagination parameters - limitStr := c.DefaultQuery("limit", strconv.Itoa(DefaultNotificationLimit)) - offsetStr := c.DefaultQuery("offset", strconv.Itoa(DefaultOffset)) - - limit, offset, err := validatePaginationParams(limitStr, offsetStr) - if err != nil { - Error(c, http.StatusBadRequest, "Invalid pagination parameters", err.Error()) + // Parse and validate pagination parameters (common helper) + limit, offset, ok := bindPagination(c) + if !ok { return } @@ -280,13 +250,9 @@ func (h *Handler) GetUnreadNotificationsHandler(c *gin.Context) { return } - // Parse and validate pagination parameters - limitStr := c.DefaultQuery("limit", strconv.Itoa(DefaultNotificationLimit)) - offsetStr := c.DefaultQuery("offset", strconv.Itoa(DefaultOffset)) - - limit, offset, err := validatePaginationParams(limitStr, offsetStr) - if err != nil { - Error(c, http.StatusBadRequest, "Invalid pagination parameters", err.Error()) + // Parse and validate pagination parameters (common helper) + limit, offset, ok := bindPagination(c) + if !ok { return } diff --git a/backend/app/pagination.go b/backend/app/pagination.go new file mode 100644 index 00000000..21395bf5 --- /dev/null +++ b/backend/app/pagination.go @@ -0,0 +1,44 @@ +package app + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +const ( + defaultLimit = 20 + maxLimit = 100 + defaultOffset = 0 +) + +// PaginationQuery is a reusable binding model for limit/offset validation via Gin +type PaginationQuery struct { + Limit int `form:"limit" binding:"omitempty,min=1,max=100"` + Offset int `form:"offset" binding:"omitempty,min=0"` +} + +// bindPagination binds and validates pagination query params using Gin bindings +func bindPagination(c *gin.Context) (int, int, bool) { + var q PaginationQuery + if err := c.ShouldBindQuery(&q); err != nil { + Error(c, http.StatusBadRequest, "Invalid pagination parameters", err.Error()) + return 0, 0, false + } + + // Apply defaults and caps + limit := q.Limit + if limit == 0 { + limit = defaultLimit + } + if limit > maxLimit { + limit = maxLimit + } + + offset := q.Offset + if offset < 0 { + offset = defaultOffset + } + + return limit, offset, true +} diff --git a/backend/app/response.go b/backend/app/response.go index 6c8148a5..ca154204 100644 --- a/backend/app/response.go +++ b/backend/app/response.go @@ -14,6 +14,14 @@ type APIResponse struct { Error string `json:"error,omitempty"` } +// PaginatedData is a unified container for paginated responses +type PaginatedData[T any] struct { + Items []T `json:"items"` + Total int64 `json:"total"` + Limit int `json:"limit"` + Offset int `json:"offset"` +} + // Success returns data for successful requests func Success(c *gin.Context, status int, message string, data interface{}) { c.JSON(status, APIResponse{ diff --git a/backend/app/user_handler.go b/backend/app/user_handler.go index 61353c0c..a06176fe 100644 --- a/backend/app/user_handler.go +++ b/backend/app/user_handler.go @@ -914,6 +914,43 @@ func (h *Handler) ListSSHKeysHandler(c *gin.Context) { Success(c, http.StatusOK, "SSH keys retrieved successfully", sshKeys) } +// @Summary List user SSH keys (paginated) +// @Description Lists SSH keys for the authenticated user with pagination +// @Tags users +// @ID list-ssh-keys-paginated +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param limit query int false "Limit" +// @Param offset query int false "Offset" +// @Success 200 {object} APIResponse{data=object{ssh_keys=[]models.SSHKey,count=int,limit=int,offset=int}} +// @Failure 401 {object} APIResponse "Unauthorized" +// @Failure 500 {object} APIResponse +// @Router /user/ssh-keys/paginated [get] +func (h *Handler) ListSSHKeysPaginatedHandler(c *gin.Context) { + userID := c.GetInt("user_id") + if userID == 0 { + Error(c, http.StatusUnauthorized, "Unauthorized", "user not authenticated") + return + } + limit, offset, ok := bindPagination(c) + if !ok { + return + } + sshKeys, total, err := h.db.ListUserSSHPaginated(userID, limit, offset) + if err != nil { + logger.GetLogger().Error().Err(err).Msg("failed to list SSH keys") + InternalServerError(c) + return + } + Success(c, http.StatusOK, "SSH keys retrieved successfully", PaginatedData[models.SSHKey]{ + Items: sshKeys, + Total: total, + Limit: limit, + Offset: offset, + }) +} + // @Summary Add SSH key // @Description Adds a new SSH key for the authenticated user // @Tags users @@ -1126,6 +1163,61 @@ func (h *Handler) ListUserPendingRecordsHandler(c *gin.Context) { }) } +// @Summary List user pending records (paginated) +// @Description Returns user pending records with pagination +// @Tags users +// @ID list-user-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 BearerAuth +// @Router /user/pending-records/paginated [get] +func (h *Handler) ListUserPendingRecordsPaginatedHandler(c *gin.Context) { + userID := c.GetInt("user_id") + limit, offset, ok := bindPagination(c) + if !ok { + return + } + records, total, err := h.db.ListUserPendingRecordsPaginated(userID, limit, offset) + if err != nil { + logger.GetLogger().Error().Err(err).Msg("failed to list pending records") + InternalServerError(c) + return + } + + var pendingRecordsResponse []PendingRecordsResponse + for _, record := range records { + usdMillicentAmount, 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 + } + usdMillicentTransferredAmount, 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(usdMillicentAmount), + TransferredUSDAmount: internal.FromUSDMilliCentToUSD(usdMillicentTransferredAmount), + }) + } + + Success(c, http.StatusOK, "Pending records are retrieved successfully", PaginatedData[PendingRecordsResponse]{ + Items: pendingRecordsResponse, + Total: total, + Limit: limit, + Offset: offset, + }) +} + func isUserRegistered(user models.User) bool { return user.Sponsored && user.Verified && diff --git a/backend/docs/docs.go b/backend/docs/docs.go index f29e9a87..a996f1f7 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -142,6 +142,69 @@ const docTemplate = `{ } } }, + "/deployments/paginated": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieves a paginated list of deployments for the authenticated user", + "produces": [ + "application/json" + ], + "tags": [ + "deployments" + ], + "summary": "List deployments (paginated)", + "parameters": [ + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Deployments retrieved successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/app.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/app.PaginatedData-app_DeploymentResponse" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + } + } + } + }, "/deployments/{name}": { "get": { "security": [ @@ -480,6 +543,158 @@ const docTemplate = `{ } } }, + "/invoices/paginated": { + "get": { + "security": [ + { + "AdminMiddleware": [] + } + ], + "description": "Returns a paginated list of all invoices", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Get all invoices (paginated)", + "operationId": "get-all-invoices-paginated", + "parameters": [ + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/app.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "invoices": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Invoice" + } + }, + "limit": { + "type": "integer" + }, + "offset": { + "type": "integer" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + } + } + } + }, + "/nodes": { + "get": { + "description": "List all nodes from the grid proxy (no user-specific filtering)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "nodes" + ], + "summary": "List all grid nodes", + "operationId": "list-all-grid-nodes", + "parameters": [ + { + "type": "boolean", + "description": "Filter by healthy nodes (default: false)", + "name": "healthy", + "in": "query" + }, + { + "type": "integer", + "description": "Limit the number of nodes returned (default: 50)", + "name": "size", + "in": "query" + }, + { + "type": "integer", + "description": "page number (default: 1)", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "All grid nodes retrieved successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/app.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/app.ListNodesResponse" + } + } + } + ] + } + }, + "400": { + "description": "Invalid filter parameters", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + } + } + } + }, "/nodes/{node_id}/storage-pool": { "get": { "description": "Returns node storage pool", @@ -1001,9 +1216,14 @@ const docTemplate = `{ } } }, - "/nodes": { + "/pending-records": { "get": { - "description": "List all nodes from the grid proxy (no user-specific filtering)", + "security": [ + { + "AdminMiddleware": [] + } + ], + "description": "Returns all pending records in the system", "consumes": [ "application/json" ], @@ -1011,33 +1231,65 @@ const docTemplate = `{ "application/json" ], "tags": [ - "nodes" + "admin" ], - "summary": "List all grid nodes", - "operationId": "list-all-grid-nodes", - "parameters": [ - { - "type": "boolean", - "description": "Filter by healthy nodes (default: false)", - "name": "healthy", - "in": "query" - }, - { - "type": "integer", - "description": "Limit the number of nodes returned (default: 50)", - "name": "size", - "in": "query" + "summary": "List pending records", + "operationId": "list-pending-records", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/app.PendingRecordsResponse" + } + } }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + } + } + } + }, + "/pending-records/paginated": { + "get": { + "security": [ { - "type": "integer", - "description": "page number (default: 1)", - "name": "page", - "in": "query" + "AdminMiddleware": [] + } + ], + "description": "Returns pending records with pagination", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "List pending records (paginated)", + "operationId": "list-pending-records-paginated", + "parameters": [ + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" } ], "responses": { "200": { - "description": "All grid nodes retrieved successfully", + "description": "OK", "schema": { "allOf": [ { @@ -1047,7 +1299,24 @@ const docTemplate = `{ "type": "object", "properties": { "data": { - "$ref": "#/definitions/app.ListNodesResponse" + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "limit": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, + "pending_records": { + "type": "array", + "items": { + "$ref": "#/definitions/app.PendingRecordsResponse" + } + } + } } } } @@ -1055,48 +1324,10 @@ const docTemplate = `{ } }, "400": { - "description": "Invalid filter parameters", - "schema": { - "$ref": "#/definitions/app.APIResponse" - } - }, - "500": { - "description": "Internal server error", + "description": "Bad Request", "schema": { "$ref": "#/definitions/app.APIResponse" } - } - } - } - }, - "/pending-records": { - "get": { - "security": [ - { - "AdminMiddleware": [] - } - ], - "description": "Returns all pending records in the system", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "admin" - ], - "summary": "List pending records", - "operationId": "list-pending-records", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/app.PendingRecordsResponse" - } - } }, "500": { "description": "Internal Server Error", @@ -1599,6 +1830,90 @@ const docTemplate = `{ } } }, + "/user/invoice/paginated": { + "get": { + "security": [ + { + "UserMiddleware": [] + } + ], + "description": "Returns a paginated list of invoices for a user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "invoices" + ], + "summary": "Get invoices (paginated)", + "operationId": "get-invoices-paginated", + "parameters": [ + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/app.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "invoices": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Invoice" + } + }, + "limit": { + "type": "integer" + }, + "offset": { + "type": "integer" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + } + } + } + }, "/user/invoice/{invoice_id}": { "get": { "security": [ @@ -1811,6 +2126,68 @@ const docTemplate = `{ } } }, + "/user/nodes/rentable/paginated": { + "get": { + "description": "Retrieves a paginated list of rentable nodes from the grid proxy with discount price", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "nodes" + ], + "summary": "List rentable nodes (paginated)", + "operationId": "list-rentable-nodes-paginated", + "parameters": [ + { + "type": "integer", + "description": "Limit the number of nodes returned (default: 20, max: 100)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset for pagination (default: 0)", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/app.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/app.ListNodesWithDiscountResponse" + } + } + } + ] + } + }, + "400": { + "description": "Invalid pagination parameters", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + } + } + } + }, "/user/nodes/rented": { "get": { "security": [ @@ -1858,6 +2235,67 @@ const docTemplate = `{ } } }, + "/user/nodes/rented/paginated": { + "get": { + "security": [ + { + "UserMiddleware": [] + } + ], + "description": "Returns a paginated list of reserved nodes for a user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "nodes" + ], + "summary": "List reserved nodes (paginated)", + "operationId": "list-reserved-nodes-paginated", + "parameters": [ + { + "type": "integer", + "description": "Limit the number of nodes returned (default: 20, max: 100)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset for pagination (default: 0)", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/app.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/app.ListNodesWithDiscountResponse" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + } + } + } + }, "/user/nodes/unreserve/{contract_id}": { "delete": { "security": [ @@ -2008,6 +2446,90 @@ const docTemplate = `{ } } }, + "/user/pending-records/paginated": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Returns user pending records with pagination", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "List user pending records (paginated)", + "operationId": "list-user-pending-records-paginated", + "parameters": [ + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/app.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "limit": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, + "pending_records": { + "type": "array", + "items": { + "$ref": "#/definitions/app.PendingRecordsResponse" + } + } + } + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + } + } + } + }, "/user/redeem/{voucher_code}": { "put": { "description": "Redeems a voucher for the user", @@ -2264,7 +2786,65 @@ const docTemplate = `{ "BearerAuth": [] } ], - "description": "Adds a new SSH key for the authenticated user", + "description": "Adds a new SSH key for the authenticated user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Add SSH key", + "operationId": "add-ssh-key", + "parameters": [ + { + "description": "SSH Key Input", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app.SSHKeyInput" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.SSHKey" + } + }, + "400": { + "description": "Invalid request format", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + } + } + } + }, + "/user/ssh-keys/paginated": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Lists SSH keys for the authenticated user with pagination", "consumes": [ "application/json" ], @@ -2274,30 +2854,56 @@ const docTemplate = `{ "tags": [ "users" ], - "summary": "Add SSH key", - "operationId": "add-ssh-key", + "summary": "List user SSH keys (paginated)", + "operationId": "list-ssh-keys-paginated", "parameters": [ { - "description": "SSH Key Input", - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/app.SSHKeyInput" - } + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" } ], "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/models.SSHKey" - } - }, - "400": { - "description": "Invalid request format", + "200": { + "description": "OK", "schema": { - "$ref": "#/definitions/app.APIResponse" + "allOf": [ + { + "$ref": "#/definitions/app.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "limit": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, + "ssh_keys": { + "type": "array", + "items": { + "$ref": "#/definitions/models.SSHKey" + } + } + } + } + } + } + ] } }, "401": { @@ -2490,6 +3096,90 @@ const docTemplate = `{ } } }, + "/users/paginated": { + "get": { + "security": [ + { + "AdminMiddleware": [] + } + ], + "description": "Returns a paginated list of users with total count", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Get users (paginated)", + "operationId": "get-users-paginated", + "parameters": [ + { + "type": "integer", + "description": "Limit the number of users returned (default: 20, max: 100)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset for pagination (default: 0)", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/app.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "limit": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, + "users": { + "type": "array", + "items": { + "$ref": "#/definitions/app.UserResponse" + } + } + } + } + } + } + ] + } + }, + "400": { + "description": "Invalid pagination parameters", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + } + } + } + }, "/users/{user_id}": { "delete": { "security": [ @@ -2698,6 +3388,90 @@ const docTemplate = `{ } } }, + "/vouchers/paginated": { + "get": { + "security": [ + { + "AdminMiddleware": [] + } + ], + "description": "Returns vouchers with pagination", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "List vouchers (paginated)", + "operationId": "list-vouchers-paginated", + "parameters": [ + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/app.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "limit": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, + "vouchers": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Voucher" + } + } + } + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + } + } + } + }, "/workflow/{workflow_id}": { "get": { "description": "Returns the status of a workflow by its ID.", @@ -3251,6 +4025,59 @@ const docTemplate = `{ } } }, + "app.NotificationResponse": { + "description": "A notification response", + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "type": "string" + }, + "payload": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "read_at": { + "type": "string" + }, + "severity": { + "$ref": "#/definitions/models.NotificationSeverity" + }, + "status": { + "$ref": "#/definitions/models.NotificationStatus" + }, + "task_id": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/models.NotificationType" + } + } + }, + "app.PaginatedData-app_DeploymentResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/app.DeploymentResponse" + } + }, + "limit": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, "app.PendingRecordsResponse": { "type": "object", "properties": { @@ -3731,13 +4558,15 @@ const docTemplate = `{ "deployment", "billing", "user", - "connected" + "connected", + "node" ], "x-enum-varnames": [ "NotificationTypeDeployment", "NotificationTypeBilling", "NotificationTypeUser", - "NotificationTypeConnected" + "NotificationTypeConnected", + "NotificationTypeNode" ] }, "models.SSHKey": { @@ -4062,6 +4891,9 @@ const docTemplate = `{ }, "vendor": { "type": "string" + }, + "vram": { + "type": "integer" } } }, diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index 78e272f5..56e1f99c 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -135,6 +135,69 @@ } } }, + "/deployments/paginated": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieves a paginated list of deployments for the authenticated user", + "produces": [ + "application/json" + ], + "tags": [ + "deployments" + ], + "summary": "List deployments (paginated)", + "parameters": [ + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Deployments retrieved successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/app.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/app.PaginatedData-app_DeploymentResponse" + } + } + } + ] + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + } + } + } + }, "/deployments/{name}": { "get": { "security": [ @@ -473,6 +536,221 @@ } } }, + "/invoices/paginated": { + "get": { + "security": [ + { + "AdminMiddleware": [] + } + ], + "description": "Returns a paginated list of all invoices", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Get all invoices (paginated)", + "operationId": "get-all-invoices-paginated", + "parameters": [ + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/app.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "invoices": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Invoice" + } + }, + "limit": { + "type": "integer" + }, + "offset": { + "type": "integer" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + } + } + } + }, + "/nodes": { + "get": { + "description": "List all nodes from the grid proxy (no user-specific filtering)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "nodes" + ], + "summary": "List all grid nodes", + "operationId": "list-all-grid-nodes", + "parameters": [ + { + "type": "boolean", + "description": "Filter by healthy nodes (default: false)", + "name": "healthy", + "in": "query" + }, + { + "type": "integer", + "description": "Limit the number of nodes returned (default: 50)", + "name": "size", + "in": "query" + }, + { + "type": "integer", + "description": "page number (default: 1)", + "name": "page", + "in": "query" + } + ], + "responses": { + "200": { + "description": "All grid nodes retrieved successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/app.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/app.ListNodesResponse" + } + } + } + ] + } + }, + "400": { + "description": "Invalid filter parameters", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + } + } + } + }, + "/nodes/{node_id}/storage-pool": { + "get": { + "description": "Returns node storage pool", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "nodes" + ], + "summary": "Get node storage pool", + "operationId": "get-node-storage-pool", + "parameters": [ + { + "type": "string", + "description": "Node ID", + "name": "node_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Node storage pool is retrieved successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/app.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/app.NodeStoragePoolResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request or Invalid params", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + }, + "404": { + "description": "Node not found", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + } + } + } + }, "/notifications": { "get": { "description": "Retrieves all user notifications with pagination", @@ -931,9 +1209,14 @@ } } }, - "/nodes": { + "/pending-records": { "get": { - "description": "List all nodes from the grid proxy (no user-specific filtering)", + "security": [ + { + "AdminMiddleware": [] + } + ], + "description": "Returns all pending records in the system", "consumes": [ "application/json" ], @@ -941,57 +1224,22 @@ "application/json" ], "tags": [ - "nodes" - ], - "summary": "List all grid nodes", - "operationId": "list-all-grid-nodes", - "parameters": [ - { - "type": "boolean", - "description": "Filter by healthy nodes (default: false)", - "name": "healthy", - "in": "query" - }, - { - "type": "integer", - "description": "Limit the number of nodes returned (default: 50)", - "name": "size", - "in": "query" - }, - { - "type": "integer", - "description": "page number (default: 1)", - "name": "page", - "in": "query" - } + "admin" ], + "summary": "List pending records", + "operationId": "list-pending-records", "responses": { "200": { - "description": "All grid nodes retrieved successfully", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/app.APIResponse" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/app.ListNodesResponse" - } - } - } - ] - } - }, - "400": { - "description": "Invalid filter parameters", + "description": "OK", "schema": { - "$ref": "#/definitions/app.APIResponse" + "type": "array", + "items": { + "$ref": "#/definitions/app.PendingRecordsResponse" + } } }, "500": { - "description": "Internal server error", + "description": "Internal Server Error", "schema": { "$ref": "#/definitions/app.APIResponse" } @@ -999,9 +1247,14 @@ } } }, - "/nodes/{node_id}/storage-pool": { + "/pending-records/paginated": { "get": { - "description": "Returns node storage pool", + "security": [ + { + "AdminMiddleware": [] + } + ], + "description": "Returns pending records with pagination", "consumes": [ "application/json" ], @@ -1009,22 +1262,27 @@ "application/json" ], "tags": [ - "nodes" + "admin" ], - "summary": "Get node storage pool", - "operationId": "get-node-storage-pool", + "summary": "List pending records (paginated)", + "operationId": "list-pending-records-paginated", "parameters": [ { - "type": "string", - "description": "Node ID", - "name": "node_id", - "in": "path", - "required": true + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" } ], "responses": { "200": { - "description": "Node storage pool is retrieved successfully", + "description": "OK", "schema": { "allOf": [ { @@ -1034,7 +1292,24 @@ "type": "object", "properties": { "data": { - "$ref": "#/definitions/app.NodeStoragePoolResponse" + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "limit": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, + "pending_records": { + "type": "array", + "items": { + "$ref": "#/definitions/app.PendingRecordsResponse" + } + } + } } } } @@ -1042,13 +1317,7 @@ } }, "400": { - "description": "Bad Request or Invalid params", - "schema": { - "$ref": "#/definitions/app.APIResponse" - } - }, - "404": { - "description": "Node not found", + "description": "Bad Request", "schema": { "$ref": "#/definitions/app.APIResponse" } @@ -1062,44 +1331,6 @@ } } }, - "/pending-records": { - "get": { - "security": [ - { - "AdminMiddleware": [] - } - ], - "description": "Returns all pending records in the system", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "admin" - ], - "summary": "List pending records", - "operationId": "list-pending-records", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/app.PendingRecordsResponse" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/app.APIResponse" - } - } - } - } - }, "/stats": { "get": { "security": [ @@ -1577,10 +1808,94 @@ "200": { "description": "OK", "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Invoice" - } + "type": "array", + "items": { + "$ref": "#/definitions/models.Invoice" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + } + } + } + }, + "/user/invoice/paginated": { + "get": { + "security": [ + { + "UserMiddleware": [] + } + ], + "description": "Returns a paginated list of invoices for a user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "invoices" + ], + "summary": "Get invoices (paginated)", + "operationId": "get-invoices-paginated", + "parameters": [ + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/app.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "invoices": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Invoice" + } + }, + "limit": { + "type": "integer" + }, + "offset": { + "type": "integer" + } + } + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/app.APIResponse" } }, "500": { @@ -1804,6 +2119,68 @@ } } }, + "/user/nodes/rentable/paginated": { + "get": { + "description": "Retrieves a paginated list of rentable nodes from the grid proxy with discount price", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "nodes" + ], + "summary": "List rentable nodes (paginated)", + "operationId": "list-rentable-nodes-paginated", + "parameters": [ + { + "type": "integer", + "description": "Limit the number of nodes returned (default: 20, max: 100)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset for pagination (default: 0)", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/app.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/app.ListNodesWithDiscountResponse" + } + } + } + ] + } + }, + "400": { + "description": "Invalid pagination parameters", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + } + } + } + }, "/user/nodes/rented": { "get": { "security": [ @@ -1851,6 +2228,67 @@ } } }, + "/user/nodes/rented/paginated": { + "get": { + "security": [ + { + "UserMiddleware": [] + } + ], + "description": "Returns a paginated list of reserved nodes for a user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "nodes" + ], + "summary": "List reserved nodes (paginated)", + "operationId": "list-reserved-nodes-paginated", + "parameters": [ + { + "type": "integer", + "description": "Limit the number of nodes returned (default: 20, max: 100)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset for pagination (default: 0)", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/app.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/app.ListNodesWithDiscountResponse" + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + } + } + } + }, "/user/nodes/unreserve/{contract_id}": { "delete": { "security": [ @@ -2001,6 +2439,90 @@ } } }, + "/user/pending-records/paginated": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Returns user pending records with pagination", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "List user pending records (paginated)", + "operationId": "list-user-pending-records-paginated", + "parameters": [ + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/app.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "limit": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, + "pending_records": { + "type": "array", + "items": { + "$ref": "#/definitions/app.PendingRecordsResponse" + } + } + } + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + } + } + } + }, "/user/redeem/{voucher_code}": { "put": { "description": "Redeems a voucher for the user", @@ -2257,7 +2779,65 @@ "BearerAuth": [] } ], - "description": "Adds a new SSH key for the authenticated user", + "description": "Adds a new SSH key for the authenticated user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Add SSH key", + "operationId": "add-ssh-key", + "parameters": [ + { + "description": "SSH Key Input", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/app.SSHKeyInput" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.SSHKey" + } + }, + "400": { + "description": "Invalid request format", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + } + } + } + }, + "/user/ssh-keys/paginated": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Lists SSH keys for the authenticated user with pagination", "consumes": [ "application/json" ], @@ -2267,30 +2847,56 @@ "tags": [ "users" ], - "summary": "Add SSH key", - "operationId": "add-ssh-key", + "summary": "List user SSH keys (paginated)", + "operationId": "list-ssh-keys-paginated", "parameters": [ { - "description": "SSH Key Input", - "name": "body", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/app.SSHKeyInput" - } + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" } ], "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/models.SSHKey" - } - }, - "400": { - "description": "Invalid request format", + "200": { + "description": "OK", "schema": { - "$ref": "#/definitions/app.APIResponse" + "allOf": [ + { + "$ref": "#/definitions/app.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "limit": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, + "ssh_keys": { + "type": "array", + "items": { + "$ref": "#/definitions/models.SSHKey" + } + } + } + } + } + } + ] } }, "401": { @@ -2483,6 +3089,90 @@ } } }, + "/users/paginated": { + "get": { + "security": [ + { + "AdminMiddleware": [] + } + ], + "description": "Returns a paginated list of users with total count", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "Get users (paginated)", + "operationId": "get-users-paginated", + "parameters": [ + { + "type": "integer", + "description": "Limit the number of users returned (default: 20, max: 100)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset for pagination (default: 0)", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/app.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "limit": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, + "users": { + "type": "array", + "items": { + "$ref": "#/definitions/app.UserResponse" + } + } + } + } + } + } + ] + } + }, + "400": { + "description": "Invalid pagination parameters", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + } + } + } + }, "/users/{user_id}": { "delete": { "security": [ @@ -2691,6 +3381,90 @@ } } }, + "/vouchers/paginated": { + "get": { + "security": [ + { + "AdminMiddleware": [] + } + ], + "description": "Returns vouchers with pagination", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "List vouchers (paginated)", + "operationId": "list-vouchers-paginated", + "parameters": [ + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/app.APIResponse" + }, + { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "limit": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, + "vouchers": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Voucher" + } + } + } + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/app.APIResponse" + } + } + } + } + }, "/workflow/{workflow_id}": { "get": { "description": "Returns the status of a workflow by its ID.", @@ -3244,6 +4018,59 @@ } } }, + "app.NotificationResponse": { + "description": "A notification response", + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "id": { + "type": "string" + }, + "payload": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "read_at": { + "type": "string" + }, + "severity": { + "$ref": "#/definitions/models.NotificationSeverity" + }, + "status": { + "$ref": "#/definitions/models.NotificationStatus" + }, + "task_id": { + "type": "string" + }, + "type": { + "$ref": "#/definitions/models.NotificationType" + } + } + }, + "app.PaginatedData-app_DeploymentResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/app.DeploymentResponse" + } + }, + "limit": { + "type": "integer" + }, + "offset": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, "app.PendingRecordsResponse": { "type": "object", "properties": { diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index fc75020c..974ecbde 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -360,6 +360,19 @@ definitions: type: $ref: '#/definitions/models.NotificationType' type: object + app.PaginatedData-app_DeploymentResponse: + properties: + items: + items: + $ref: '#/definitions/app.DeploymentResponse' + type: array + limit: + type: integer + offset: + type: integer + total: + type: integer + type: object app.PendingRecordsResponse: properties: created_at: @@ -686,12 +699,14 @@ definitions: - billing - user - connected + - node type: string x-enum-varnames: - NotificationTypeDeployment - NotificationTypeBilling - NotificationTypeUser - NotificationTypeConnected + - NotificationTypeNode models.SSHKey: properties: created_at: @@ -907,6 +922,8 @@ definitions: type: integer vendor: type: string + vram: + type: integer type: object types.NodePower: properties: @@ -1246,6 +1263,44 @@ paths: summary: Remove node from deployment tags: - deployments + /deployments/paginated: + get: + description: Retrieves a paginated list of deployments for the authenticated + user + parameters: + - description: Limit + in: query + name: limit + type: integer + - description: Offset + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: Deployments retrieved successfully + schema: + allOf: + - $ref: '#/definitions/app.APIResponse' + - properties: + data: + $ref: '#/definitions/app.PaginatedData-app_DeploymentResponse' + type: object + "401": + description: Unauthorized + schema: + $ref: '#/definitions/app.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/app.APIResponse' + security: + - BearerAuth: [] + summary: List deployments (paginated) + tags: + - deployments /invoices: get: consumes: @@ -1270,6 +1325,57 @@ paths: summary: Get all invoices tags: - admin + /invoices/paginated: + get: + consumes: + - application/json + description: Returns a paginated list of all invoices + operationId: get-all-invoices-paginated + parameters: + - description: Limit + in: query + name: limit + type: integer + - description: Offset + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/app.APIResponse' + - properties: + data: + properties: + count: + type: integer + invoices: + items: + $ref: '#/definitions/models.Invoice' + type: array + limit: + type: integer + offset: + type: integer + type: object + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/app.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/app.APIResponse' + security: + - AdminMiddleware: [] + summary: Get all invoices (paginated) + tags: + - admin /nodes: get: consumes: @@ -1657,6 +1763,57 @@ paths: summary: List pending records tags: - admin + /pending-records/paginated: + get: + consumes: + - application/json + description: Returns pending records with pagination + operationId: list-pending-records-paginated + parameters: + - description: Limit + in: query + name: limit + type: integer + - description: Offset + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/app.APIResponse' + - properties: + data: + properties: + count: + type: integer + limit: + type: integer + offset: + type: integer + pending_records: + items: + $ref: '#/definitions/app.PendingRecordsResponse' + type: array + type: object + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/app.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/app.APIResponse' + security: + - AdminMiddleware: [] + summary: List pending records (paginated) + tags: + - admin /stats: get: consumes: @@ -2010,6 +2167,57 @@ paths: summary: Download invoice tags: - invoices + /user/invoice/paginated: + get: + consumes: + - application/json + description: Returns a paginated list of invoices for a user + operationId: get-invoices-paginated + parameters: + - description: Limit + in: query + name: limit + type: integer + - description: Offset + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/app.APIResponse' + - properties: + data: + properties: + count: + type: integer + invoices: + items: + $ref: '#/definitions/models.Invoice' + type: array + limit: + type: integer + offset: + type: integer + type: object + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/app.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/app.APIResponse' + security: + - UserMiddleware: [] + summary: Get invoices (paginated) + tags: + - invoices /user/login: post: consumes: @@ -2151,6 +2359,45 @@ paths: summary: List rentable nodes tags: - nodes + /user/nodes/rentable/paginated: + get: + consumes: + - application/json + description: Retrieves a paginated list of rentable nodes from the grid proxy + with discount price + operationId: list-rentable-nodes-paginated + parameters: + - description: 'Limit the number of nodes returned (default: 20, max: 100)' + in: query + name: limit + type: integer + - description: 'Offset for pagination (default: 0)' + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/app.APIResponse' + - properties: + data: + $ref: '#/definitions/app.ListNodesWithDiscountResponse' + type: object + "400": + description: Invalid pagination parameters + schema: + $ref: '#/definitions/app.APIResponse' + "500": + description: Internal server error + schema: + $ref: '#/definitions/app.APIResponse' + summary: List rentable nodes (paginated) + tags: + - nodes /user/nodes/rented: get: consumes: @@ -2178,6 +2425,42 @@ paths: summary: List reserved nodes tags: - nodes + /user/nodes/rented/paginated: + get: + consumes: + - application/json + description: Returns a paginated list of reserved nodes for a user + operationId: list-reserved-nodes-paginated + parameters: + - description: 'Limit the number of nodes returned (default: 20, max: 100)' + in: query + name: limit + type: integer + - description: 'Offset for pagination (default: 0)' + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/app.APIResponse' + - properties: + data: + $ref: '#/definitions/app.ListNodesWithDiscountResponse' + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/app.APIResponse' + security: + - UserMiddleware: [] + summary: List reserved nodes (paginated) + tags: + - nodes /user/nodes/unreserve/{contract_id}: delete: consumes: @@ -2238,6 +2521,57 @@ paths: summary: List user pending records tags: - users + /user/pending-records/paginated: + get: + consumes: + - application/json + description: Returns user pending records with pagination + operationId: list-user-pending-records-paginated + parameters: + - description: Limit + in: query + name: limit + type: integer + - description: Offset + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/app.APIResponse' + - properties: + data: + properties: + count: + type: integer + limit: + type: integer + offset: + type: integer + pending_records: + items: + $ref: '#/definitions/app.PendingRecordsResponse' + type: array + type: object + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/app.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/app.APIResponse' + security: + - BearerAuth: [] + summary: List user pending records (paginated) + tags: + - users /user/redeem/{voucher_code}: put: description: Redeems a voucher for the user @@ -2481,6 +2815,57 @@ paths: summary: Delete SSH key tags: - users + /user/ssh-keys/paginated: + get: + consumes: + - application/json + description: Lists SSH keys for the authenticated user with pagination + operationId: list-ssh-keys-paginated + parameters: + - description: Limit + in: query + name: limit + type: integer + - description: Offset + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/app.APIResponse' + - properties: + data: + properties: + count: + type: integer + limit: + type: integer + offset: + type: integer + ssh_keys: + items: + $ref: '#/definitions/models.SSHKey' + type: array + type: object + type: object + "401": + description: Unauthorized + schema: + $ref: '#/definitions/app.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/app.APIResponse' + security: + - BearerAuth: [] + summary: List user SSH keys (paginated) + tags: + - users /users: get: consumes: @@ -2627,6 +3012,57 @@ paths: summary: Send mail to all users tags: - admin + /users/paginated: + get: + consumes: + - application/json + description: Returns a paginated list of users with total count + operationId: get-users-paginated + parameters: + - description: 'Limit the number of users returned (default: 20, max: 100)' + in: query + name: limit + type: integer + - description: 'Offset for pagination (default: 0)' + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/app.APIResponse' + - properties: + data: + properties: + count: + type: integer + limit: + type: integer + offset: + type: integer + users: + items: + $ref: '#/definitions/app.UserResponse' + type: array + type: object + type: object + "400": + description: Invalid pagination parameters + schema: + $ref: '#/definitions/app.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/app.APIResponse' + security: + - AdminMiddleware: [] + summary: Get users (paginated) + tags: + - admin /vouchers: get: consumes: @@ -2686,6 +3122,57 @@ paths: summary: Generate vouchers tags: - admin + /vouchers/paginated: + get: + consumes: + - application/json + description: Returns vouchers with pagination + operationId: list-vouchers-paginated + parameters: + - description: Limit + in: query + name: limit + type: integer + - description: Offset + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/app.APIResponse' + - properties: + data: + properties: + count: + type: integer + limit: + type: integer + offset: + type: integer + vouchers: + items: + $ref: '#/definitions/models.Voucher' + type: array + type: object + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/app.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/app.APIResponse' + security: + - AdminMiddleware: [] + summary: List vouchers (paginated) + tags: + - admin /workflow/{workflow_id}: get: consumes: diff --git a/backend/models/db.go b/backend/models/db.go index 1721f404..d3cfd402 100644 --- a/backend/models/db.go +++ b/backend/models/db.go @@ -17,10 +17,12 @@ type DB interface { UpdateUserByID(user *User) error UpdatePassword(email string, hashedPassword []byte) error ListAllUsers() ([]User, error) + ListUsersPaginated(limit, offset int) ([]User, int64, error) ListAdmins() ([]User, error) DeleteUserByID(userID int) error CreateVoucher(voucher *Voucher) error ListAllVouchers() ([]Voucher, error) + ListVouchersPaginated(limit, offset int) ([]Voucher, int64, error) GetVoucherByCode(code string) (Voucher, error) RedeemVoucher(code string) error CreateTransaction(transaction *Transaction) error @@ -29,6 +31,8 @@ type DB interface { GetInvoice(id int) (Invoice, error) ListUserInvoices(userID int) ([]Invoice, error) ListInvoices() ([]Invoice, error) + ListUserInvoicesPaginated(userID, limit, offset int) ([]Invoice, int64, error) + ListInvoicesPaginated(limit, offset int) ([]Invoice, int64, error) UpdateInvoicePDF(id int, data []byte) error CreateUserNode(userNode *UserNodes) error DeleteUserNode(contractID uint64) error @@ -39,6 +43,7 @@ type DB interface { // SSH Key methods CreateSSHKey(sshKey *SSHKey) error ListUserSSHKeys(userID int) ([]SSHKey, error) + ListUserSSHPaginated(userID, limit, offset int) ([]SSHKey, int64, error) DeleteSSHKey(sshKeyID int, userID int) error GetSSHKeyByID(sshKeyID int, userID int) (SSHKey, error) // Notification methods @@ -53,6 +58,7 @@ type DB interface { // Cluster methods CreateCluster(userID int, cluster *Cluster) error ListUserClusters(userID int) ([]Cluster, error) + ListUserClustersPaginated(userID, limit, offset int) ([]Cluster, int64, error) GetClusterByName(userID int, projectName string) (Cluster, error) UpdateCluster(cluster *Cluster) error DeleteCluster(userID int, projectName string) error @@ -62,6 +68,8 @@ type DB interface { ListAllPendingRecords() ([]PendingRecord, error) ListOnlyPendingRecords() ([]PendingRecord, error) ListUserPendingRecords(userID int) ([]PendingRecord, error) + ListAllPendingRecordsPaginated(limit, offset int) ([]PendingRecord, int64, error) + ListUserPendingRecordsPaginated(userID, limit, offset int) ([]PendingRecord, int64, error) UpdatePendingRecordTransferredAmount(id int, amount uint64) error // stats methods CountAllUsers() (int64, error) diff --git a/backend/models/gorm.go b/backend/models/gorm.go index 678aaf42..d666ef18 100644 --- a/backend/models/gorm.go +++ b/backend/models/gorm.go @@ -159,6 +159,22 @@ func (s *GormDB) ListAllUsers() ([]User, error) { return users, nil } +// ListUsersPaginated lists users with limit/offset and returns total count +func (s *GormDB) ListUsersPaginated(limit, offset int) ([]User, int64, error) { + var users []User + var total int64 + + if err := s.db.Model(&User{}).Count(&total).Error; err != nil { + return nil, 0, err + } + + q := s.db.Order("id DESC").Limit(limit).Offset(offset).Find(&users) + if q.Error != nil { + return nil, 0, q.Error + } + return users, total, nil +} + // ListAdmins gets all admins func (s *GormDB) ListAdmins() ([]User, error) { var admins []User @@ -193,6 +209,18 @@ func (s *GormDB) ListAllVouchers() ([]Voucher, error) { return vouchers, nil } +func (s *GormDB) ListVouchersPaginated(limit, offset int) ([]Voucher, int64, error) { + var vouchers []Voucher + var total int64 + if err := s.db.Model(&Voucher{}).Count(&total).Error; err != nil { + return nil, 0, err + } + if err := s.db.Order("id DESC").Limit(limit).Offset(offset).Find(&vouchers).Error; err != nil { + return nil, 0, err + } + return vouchers, total, nil +} + // GetVoucherByCode returns voucher by its code func (s *GormDB) GetVoucherByCode(code string) (Voucher, error) { var voucher Voucher @@ -287,6 +315,44 @@ func (s *GormDB) ListInvoices() ([]Invoice, error) { return invoices, nil } +func (s *GormDB) ListUserInvoicesPaginated(userID, limit, offset int) ([]Invoice, int64, error) { + var invoices []Invoice + var total int64 + if err := s.db.Model(&Invoice{}).Where("user_id = ?", userID).Count(&total).Error; err != nil { + return nil, 0, err + } + if err := s.db.Where("user_id = ?", userID).Order("id DESC").Limit(limit).Offset(offset).Find(&invoices).Error; err != nil { + return nil, 0, err + } + for idx := range invoices { + inv, err := s.GetInvoice(invoices[idx].ID) + if err != nil { + return nil, 0, err + } + invoices[idx] = inv + } + return invoices, total, nil +} + +func (s *GormDB) ListInvoicesPaginated(limit, offset int) ([]Invoice, int64, error) { + var invoices []Invoice + var total int64 + if err := s.db.Model(&Invoice{}).Count(&total).Error; err != nil { + return nil, 0, err + } + if err := s.db.Order("id DESC").Limit(limit).Offset(offset).Find(&invoices).Error; err != nil { + return nil, 0, err + } + for idx := range invoices { + inv, err := s.GetInvoice(invoices[idx].ID) + if err != nil { + return nil, 0, err + } + invoices[idx] = inv + } + return invoices, total, nil +} + func (s *GormDB) UpdateInvoicePDF(id int, data []byte) error { return s.db.Model(&Invoice{}).Where("id = ?", id).Updates(map[string]interface{}{"file_data": data}).Error } @@ -336,6 +402,18 @@ func (s *GormDB) ListUserSSHKeys(userID int) ([]SSHKey, error) { return sshKeys, nil } +func (s *GormDB) ListUserSSHPaginated(userID, limit, offset int) ([]SSHKey, int64, error) { + var sshKeys []SSHKey + var total int64 + if err := s.db.Model(&SSHKey{}).Where("user_id = ?", userID).Count(&total).Error; err != nil { + return nil, 0, err + } + if err := s.db.Where("user_id = ?", userID).Order("id DESC").Limit(limit).Offset(offset).Find(&sshKeys).Error; err != nil { + return nil, 0, err + } + return sshKeys, total, nil +} + // DeleteSSHKey deletes an SSH key by ID for a specific user func (s *GormDB) DeleteSSHKey(sshKeyID int, userID int) error { result := s.db.Where("id = ? AND user_id = ?", sshKeyID, userID).Delete(&SSHKey{}) @@ -370,6 +448,19 @@ func (s *GormDB) ListUserClusters(userID int) ([]Cluster, error) { return clusters, query.Error } +func (s *GormDB) ListUserClustersPaginated(userID, limit, offset int) ([]Cluster, int64, error) { + var clusters []Cluster + var total int64 + if err := s.db.Model(&Cluster{}).Where("user_id = ?", userID).Count(&total).Error; err != nil { + return nil, 0, err + } + q := s.db.Where("user_id = ?", userID).Order("id DESC").Limit(limit).Offset(offset).Find(&clusters) + if q.Error != nil { + return nil, 0, q.Error + } + return clusters, total, nil +} + // GetClusterByName returns a cluster by name for a specific user func (s *GormDB) GetClusterByName(userID int, projectName string) (Cluster, error) { var cluster Cluster @@ -405,6 +496,30 @@ func (s *GormDB) ListAllPendingRecords() ([]PendingRecord, error) { return pendingRecords, s.db.Find(&pendingRecords).Error } +func (s *GormDB) ListAllPendingRecordsPaginated(limit, offset int) ([]PendingRecord, int64, error) { + var records []PendingRecord + var total int64 + if err := s.db.Model(&PendingRecord{}).Count(&total).Error; err != nil { + return nil, 0, err + } + if err := s.db.Order("id DESC").Limit(limit).Offset(offset).Find(&records).Error; err != nil { + return nil, 0, err + } + return records, total, nil +} + +func (s *GormDB) ListUserPendingRecordsPaginated(userID, limit, offset int) ([]PendingRecord, int64, error) { + var records []PendingRecord + var total int64 + if err := s.db.Model(&PendingRecord{}).Where("user_id = ?", userID).Count(&total).Error; err != nil { + return nil, 0, err + } + if err := s.db.Where("user_id = ?", userID).Order("id DESC").Limit(limit).Offset(offset).Find(&records).Error; err != nil { + return nil, 0, err + } + return records, total, nil +} + func (s *GormDB) ListOnlyPendingRecords() ([]PendingRecord, error) { var pendingRecords []PendingRecord return pendingRecords, s.db.Where("tft_amount > transferred_tft_amount").Find(&pendingRecords).Error