package mx import ( "database/sql" "net/http" "time" "github.com/gin-gonic/gin" "mexa/api/v1/actiongate" "mexa/api/v1/constants" "mexa/api/v1/db" "mexa/middleware" ) func RegisterRoutes(rg *gin.RouterGroup) { m := rg.Group("/mx", middleware.RequireAuth()) { m.POST("/flag", middleware.RequireRole("MX"), Flag) m.GET("/flags", middleware.RequireRole("MX", "DIR", "XA"), Flags) m.GET("/reports", middleware.RequireRole("MX"), Reports) m.GET("/on-behalf", middleware.RequireRole("MX"), OnBehalfGet) m.POST("/on-behalf", middleware.RequireRole("MX"), OnBehalfPost) m.GET("/profile", middleware.RequireRole("MX"), Profile) m.PATCH("/profile", middleware.RequireRole("MX"), UpdateProfile) m.GET("/history", middleware.RequireRole("MX"), History) } } func Flag(c *gin.Context) { claims, ok := middleware.GetClaims(c) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorised"}) return } var body struct { FlagType string `json:"flag_type" binding:"required"` ListingID string `json:"listing_id"` SellerID string `json:"seller_id"` Description string `json:"description" binding:"required"` EvidenceNote string `json:"evidence_note"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } ctx := c.Request.Context() var flagID int err := db.DB.QueryRowContext(ctx, `INSERT INTO mx_flags (mx_id, flag_type, listing_id, seller_id, description, evidence_note, status, created_at) VALUES ($1, $2, NULLIF($3,''), NULLIF($4,''), $5, NULLIF($6,''), 'OPEN', NOW()) RETURNING id`, claims.UserID, body.FlagType, body.ListingID, body.SellerID, body.Description, body.EvidenceNote, ).Scan(&flagID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to submit flag"}) return } c.JSON(http.StatusCreated, gin.H{"flag_id": flagID, "status": "OPEN"}) } func Flags(c *gin.Context) { ctx := c.Request.Context() rows, err := db.DB.QueryContext(ctx, `SELECT id, mx_id, flag_type, COALESCE(listing_id,''), COALESCE(seller_id,''), description, COALESCE(evidence_note,''), status, created_at FROM mx_flags ORDER BY created_at DESC LIMIT 200`) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load flags"}) return } defer rows.Close() type Row struct { ID int `json:"id"` MxID string `json:"mx_id"` FlagType string `json:"flag_type"` ListingID string `json:"listing_id"` SellerID string `json:"seller_id"` Description string `json:"description"` EvidenceNote string `json:"evidence_note"` Status string `json:"status"` CreatedAt string `json:"created_at"` } results := []Row{} for rows.Next() { var r Row if err := rows.Scan(&r.ID, &r.MxID, &r.FlagType, &r.ListingID, &r.SellerID, &r.Description, &r.EvidenceNote, &r.Status, &r.CreatedAt); err != nil { continue } results = append(results, r) } c.JSON(http.StatusOK, results) } func Reports(c *gin.Context) { claims, ok := middleware.GetClaims(c) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorised"}) return } ctx := c.Request.Context() rows, err := db.DB.QueryContext(ctx, `SELECT id, flag_type, COALESCE(listing_id,''), COALESCE(seller_id,''), description, status, created_at FROM mx_flags WHERE mx_id = $1 ORDER BY created_at DESC`, claims.UserID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load reports"}) return } defer rows.Close() type Row struct { ID int `json:"id"` FlagType string `json:"flag_type"` ListingID string `json:"listing_id"` SellerID string `json:"seller_id"` Description string `json:"description"` Status string `json:"status"` CreatedAt string `json:"created_at"` } results := []Row{} for rows.Next() { var r Row if err := rows.Scan(&r.ID, &r.FlagType, &r.ListingID, &r.SellerID, &r.Description, &r.Status, &r.CreatedAt); err != nil { continue } results = append(results, r) } c.JSON(http.StatusOK, results) } func OnBehalfGet(c *gin.Context) { claims, ok := middleware.GetClaims(c) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorised"}) return } ctx := c.Request.Context() rows, err := db.DB.QueryContext(ctx, `SELECT id, event_id, initiator_id, on_behalf_of, action_type, status, created_at FROM custody_consents WHERE initiator_id = $1 ORDER BY created_at DESC LIMIT 100`, claims.UserID) if err != nil { c.JSON(http.StatusOK, []gin.H{}) return } defer rows.Close() type Row struct { ID string `json:"id"` EventID string `json:"event_id"` InitiatorID string `json:"initiator_id"` OnBehalfOf string `json:"on_behalf_of"` ActionType string `json:"action_type"` Status string `json:"status"` CreatedAt time.Time `json:"created_at"` } results := []Row{} for rows.Next() { var r Row if err := rows.Scan(&r.ID, &r.EventID, &r.InitiatorID, &r.OnBehalfOf, &r.ActionType, &r.Status, &r.CreatedAt); err != nil { continue } results = append(results, r) } c.JSON(http.StatusOK, results) } func OnBehalfPost(c *gin.Context) { claims, ok := middleware.GetClaims(c) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorised"}) return } var body struct { EventID string `json:"event_id" binding:"required"` OnBehalfOf string `json:"on_behalf_of" binding:"required"` ActionType string `json:"action_type" binding:"required"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Rule: No Self-Verification -- carried over from the retired on_behalf_of.go // design (custody_consents never got this check when it superseded it). if claims.UserID == body.OnBehalfOf { c.JSON(http.StatusForbidden, gin.H{"error": "cannot act on behalf of self"}) return } ctx := c.Request.Context() key, err := actiongate.IdempotencyKey("mx-obo", gin.H{"event_id": body.EventID, "initiator_id": claims.UserID, "on_behalf_of": body.OnBehalfOf, "action_type": body.ActionType}) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid action payload"}) return } gate, err := actiongate.CreateOrGet(ctx, actiongate.Request{ ActionType: body.ActionType, InitiatorID: claims.UserID, SubjectID: body.OnBehalfOf, ResourceType: "event", ResourceID: body.EventID, IdempotencyKey: key, Payload: gin.H{"event_id": body.EventID, "action_type": body.ActionType}, PolicyVersion: "v1", Purpose: "MX action on behalf of the subject", }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create action gate"}) return } var consentID string if !gate.Created { err = db.DB.QueryRowContext(ctx, `SELECT id::text FROM custody_consents WHERE action_gate_request_id = $1`, gate.ID).Scan(&consentID) } else { err = db.DB.QueryRowContext(ctx, `INSERT INTO custody_consents (event_id, initiator_id, on_behalf_of, action_type, status, payload, action_gate_request_id, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, '{}', $6, NOW(), NOW()) RETURNING id::text`, body.EventID, claims.UserID, body.OnBehalfOf, body.ActionType, constants.CustodyStatusPending, gate.ID).Scan(&consentID) } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to log consent"}) return } c.JSON(http.StatusCreated, gin.H{"message": "consent logged", "consent_id": consentID, "status": constants.CustodyStatusPending}) } func Profile(c *gin.Context) { claims, ok := middleware.GetClaims(c) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorised"}) return } ctx := c.Request.Context() var myMezaID, role, kycTier string var createdAt time.Time err := db.DB.QueryRowContext(ctx, `SELECT mymeza_id, role, kyc_tier, created_at FROM users WHERE mymeza_id = $1`, claims.UserID).Scan(&myMezaID, &role, &kycTier, &createdAt) if err == sql.ErrNoRows { c.JSON(http.StatusNotFound, gin.H{"error": "profile not found"}) return } if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "profile lookup failed"}) return } c.JSON(http.StatusOK, gin.H{ "mymeza_id": myMezaID, "role": role, "kyc_tier": kycTier, "created_at": createdAt, }) } func UpdateProfile(c *gin.Context) { claims, ok := middleware.GetClaims(c) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorised"}) return } ctx := c.Request.Context() _, err := db.DB.ExecContext(ctx, `UPDATE users SET updated_at = NOW() WHERE mymeza_id = $1`, claims.UserID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "profile update failed"}) return } middleware.WriteAuditLog(middleware.AuditEntry{ EventType: "MX_PROFILE_UPDATE", ActorMyMezaID: claims.UserID, ActorRole: claims.Role, Description: "MX profile updated", }) c.JSON(http.StatusOK, gin.H{"message": "profile updated"}) } func History(c *gin.Context) { claims, ok := middleware.GetClaims(c) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorised"}) return } ctx := c.Request.Context() rows, err := db.DB.QueryContext(ctx, `SELECT id, event_id, action, COALESCE(note,''), created_at FROM events_log WHERE actor_mymeza_id = $1 ORDER BY created_at DESC LIMIT 100`, claims.UserID) if err != nil { c.JSON(http.StatusOK, []gin.H{}) return } defer rows.Close() type Row struct { ID string `json:"id"` EventID string `json:"event_id"` Action string `json:"action"` Note string `json:"note"` CreatedAt time.Time `json:"created_at"` } results := []Row{} for rows.Next() { var r Row if err := rows.Scan(&r.ID, &r.EventID, &r.Action, &r.Note, &r.CreatedAt); err != nil { continue } results = append(results, r) } c.JSON(http.StatusOK, results) }