package handlers import ( "crypto/rand" "encoding/hex" "net/http" "regexp" "strconv" "strings" "github.com/flatrender/payment-svc/internal/db" "github.com/flatrender/payment-svc/internal/models" "github.com/gin-gonic/gin" "github.com/google/uuid" ) type AdminHandler struct { store *db.Store } func NewAdminHandler(store *db.Store) *AdminHandler { return &AdminHandler{store: store} } var slugRe = regexp.MustCompile(`[^a-z0-9]+`) func slugify(s string) string { s = strings.ToLower(strings.TrimSpace(s)) s = slugRe.ReplaceAllString(s, "-") return strings.Trim(s, "-") } func randToken(prefix string) string { b := make([]byte, 24) _, _ = rand.Read(b) return prefix + hex.EncodeToString(b) } type clientInput struct { Name string `json:"name"` Slug string `json:"slug"` ZarinPalMerchantID *string `json:"zarinpal_merchant_id"` ZarinPalSandbox *bool `json:"zarinpal_sandbox"` AllowedReturnOrigins []string `json:"allowed_return_origins"` WebhookURL *string `json:"webhook_url"` IsActive *bool `json:"is_active"` } func (h *AdminHandler) List(c *gin.Context) { clients, err := h.store.ListClientApps(c.Request.Context()) if err != nil { c.JSON(http.StatusInternalServerError, models.APIError{Code: "db_error", Message: err.Error()}) return } if clients == nil { clients = []*models.ClientApp{} } c.JSON(http.StatusOK, gin.H{"data": clients}) } func (h *AdminHandler) Create(c *gin.Context) { var in clientInput if err := c.ShouldBindJSON(&in); err != nil || strings.TrimSpace(in.Name) == "" { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "name is required"}) return } slug := slugify(in.Slug) if slug == "" { slug = slugify(in.Name) } active := true if in.IsActive != nil { active = *in.IsActive } if in.AllowedReturnOrigins == nil { in.AllowedReturnOrigins = []string{} } client := &models.ClientApp{ Name: in.Name, Slug: slug, APIKey: randToken("pk_"), Secret: randToken("sk_"), ZarinPalMerchantID: in.ZarinPalMerchantID, ZarinPalSandbox: in.ZarinPalSandbox, AllowedReturnOrigins: in.AllowedReturnOrigins, WebhookURL: in.WebhookURL, IsActive: active, } created, err := h.store.CreateClientApp(c.Request.Context(), client) if err != nil { if strings.Contains(err.Error(), "duplicate") || strings.Contains(err.Error(), "unique") { c.JSON(http.StatusConflict, models.APIError{Code: "conflict", Message: "slug already exists"}) return } c.JSON(http.StatusInternalServerError, models.APIError{Code: "db_error", Message: err.Error()}) return } // created includes the plaintext secret exactly once. c.JSON(http.StatusCreated, created) } func (h *AdminHandler) Get(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid id"}) return } client, err := h.store.GetClientApp(c.Request.Context(), id) if err != nil { c.JSON(http.StatusNotFound, models.APIError{Code: "not_found", Message: "client not found"}) return } c.JSON(http.StatusOK, client) } func (h *AdminHandler) Update(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid id"}) return } existing, err := h.store.GetClientApp(c.Request.Context(), id) if err != nil { c.JSON(http.StatusNotFound, models.APIError{Code: "not_found", Message: "client not found"}) return } var in clientInput if err := c.ShouldBindJSON(&in); err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid body"}) return } if in.Name != "" { existing.Name = in.Name } existing.ZarinPalMerchantID = in.ZarinPalMerchantID existing.ZarinPalSandbox = in.ZarinPalSandbox if in.AllowedReturnOrigins != nil { existing.AllowedReturnOrigins = in.AllowedReturnOrigins } existing.WebhookURL = in.WebhookURL if in.IsActive != nil { existing.IsActive = *in.IsActive } updated, err := h.store.UpdateClientApp(c.Request.Context(), id, existing) if err != nil { c.JSON(http.StatusInternalServerError, models.APIError{Code: "db_error", Message: err.Error()}) return } c.JSON(http.StatusOK, updated) } func (h *AdminHandler) RotateSecret(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid id"}) return } updated, err := h.store.RotateSecret(c.Request.Context(), id, randToken("sk_")) if err != nil { c.JSON(http.StatusNotFound, models.APIError{Code: "not_found", Message: "client not found"}) return } c.JSON(http.StatusOK, updated) // includes the new secret once } func (h *AdminHandler) Delete(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, models.APIError{Code: "bad_request", Message: "invalid id"}) return } if err := h.store.DeleteClientApp(c.Request.Context(), id); err != nil { c.JSON(http.StatusNotFound, models.APIError{Code: "not_found", Message: "client not found"}) return } c.Status(http.StatusNoContent) } func (h *AdminHandler) ListTransactions(c *gin.Context) { page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) if page < 1 { page = 1 } if pageSize < 1 || pageSize > 200 { pageSize = 20 } var clientID *uuid.UUID if cidStr := c.Query("client_id"); cidStr != "" { if cid, err := uuid.Parse(cidStr); err == nil { clientID = &cid } } txns, total, err := h.store.ListTransactions(c.Request.Context(), clientID, c.Query("status"), page, pageSize) if err != nil { c.JSON(http.StatusInternalServerError, models.APIError{Code: "db_error", Message: err.Error()}) return } if txns == nil { txns = []*models.Transaction{} } c.JSON(http.StatusOK, gin.H{ "data": txns, "meta": gin.H{"page": page, "page_size": pageSize, "total": total, "has_more": int64(page*pageSize) < total}, }) }