Mexa / api /v1 /mx /handler.go
Mrkiti's picture
HF snapshot of main ac37255 (excludes fly.toml and binaries)
1e69593
Raw History Blame Contribute Delete
9.94 kB
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)
}