feat(t217): verify authorized order dry runs

This commit is contained in:
QiuSW
2026-07-28 16:52:29 +08:00
parent 913107c28d
commit 1e87b6273a
45 changed files with 4358 additions and 128 deletions
@@ -21,11 +21,13 @@ type DeviceServices struct {
Assets *usecase.AssetService
Results *usecase.ExecutionResultService
Commands *usecase.DeviceOrderCommandService
DryRuns *usecase.OrderDryRunService
}
func (services DeviceServices) validate() error {
if services.Lifecycle == nil || services.Assets == nil ||
services.Results == nil || services.Commands == nil {
services.Results == nil || services.Commands == nil ||
services.DryRuns == nil {
return errors.New("device services are required")
}
return nil
@@ -83,12 +85,141 @@ func NewDeviceRouteRegistrar(
"/api/v1/tasks/:id/commands/:command_id/ack",
handler.acknowledgeOrderCommand,
)
routes.POST(
"/api/v1/tasks/:id/order-dry-runs/start",
handler.startOrderDryRun,
)
routes.POST(
"/api/v1/tasks/:id/order-dry-runs/:command_id/ready",
handler.readyOrderDryRun,
)
routes.POST("/api/v1/tasks/:id/complete", handler.completeTask)
routes.POST("/api/v1/tasks/:id/fail", handler.failTask)
return nil
}, nil
}
func (handler *deviceHandlers) startOrderDryRun(ctx *gin.Context) {
principal, ok := devicePrincipal(ctx)
if !ok {
return
}
var request struct {
DeviceID string `json:"device_id"`
ExecutionID string `json:"execution_id"`
ClaimGeneration int64 `json:"claim_generation"`
CommandID string `json:"command_id"`
CommandSHA256 string `json:"command_sha256"`
}
if !decodeDeviceJSON(ctx, &request) ||
!deviceIDMatches(ctx, request.DeviceID, principal.DeviceID) {
return
}
result, err := handler.services.DryRuns.Start(
ctx.Request.Context(),
usecase.StartOrderDryRunCommand{
UserID: principal.UserID,
DeviceID: principal.DeviceID,
TaskID: ctx.Param("id"),
ExecutionID: request.ExecutionID,
AuthorizationID: request.CommandID,
ClaimGeneration: request.ClaimGeneration,
ClaimToken: ctx.GetHeader(claimTokenHeader),
CommandSHA256: request.CommandSHA256,
IdempotencyKey: ctx.GetHeader("Idempotency-Key"),
},
)
if err != nil {
writeUsecaseError(ctx, err)
return
}
ctx.Header("Cache-Control", "no-store")
ctx.JSON(http.StatusOK, gin.H{
"dry_run": orderDryRunResponse(result.DryRun),
"replayed": result.Replayed,
})
}
func (handler *deviceHandlers) readyOrderDryRun(ctx *gin.Context) {
principal, ok := devicePrincipal(ctx)
if !ok {
return
}
var request struct {
DeviceID string `json:"device_id"`
ExecutionID string `json:"execution_id"`
ClaimGeneration int64 `json:"claim_generation"`
CommandSHA256 string `json:"command_sha256"`
CardSignature string `json:"card_signature"`
DetailSignature string `json:"detail_signature"`
ObservedTitle string `json:"observed_title"`
SelectedSKU string `json:"selected_sku"`
Quantity int `json:"quantity"`
UnitPriceCents int64 `json:"unit_price_cents"`
TotalPriceCents int64 `json:"total_price_cents"`
EvidenceAssetID string `json:"evidence_asset_id"`
EvidenceSHA256 string `json:"evidence_sha256"`
}
if !decodeDeviceJSON(ctx, &request) ||
!deviceIDMatches(ctx, request.DeviceID, principal.DeviceID) {
return
}
result, err := handler.services.DryRuns.Ready(
ctx.Request.Context(),
usecase.ReadyOrderDryRunCommand{
UserID: principal.UserID,
DeviceID: principal.DeviceID,
TaskID: ctx.Param("id"),
ExecutionID: request.ExecutionID,
AuthorizationID: ctx.Param("command_id"),
ClaimGeneration: request.ClaimGeneration,
ClaimToken: ctx.GetHeader(claimTokenHeader),
CommandSHA256: request.CommandSHA256,
CardSignature: request.CardSignature,
DetailSignature: request.DetailSignature,
ObservedTitle: request.ObservedTitle,
SelectedSKU: request.SelectedSKU,
Quantity: request.Quantity,
UnitPriceCents: request.UnitPriceCents,
TotalPriceCents: request.TotalPriceCents,
EvidenceAssetID: request.EvidenceAssetID,
EvidenceSHA256: request.EvidenceSHA256,
IdempotencyKey: ctx.GetHeader("Idempotency-Key"),
},
)
if err != nil {
writeUsecaseError(ctx, err)
return
}
ctx.Header("Cache-Control", "no-store")
ctx.JSON(http.StatusOK, gin.H{
"dry_run": orderDryRunResponse(result.DryRun),
"replayed": result.Replayed,
})
}
func orderDryRunResponse(dryRun domain.OrderDryRun) gin.H {
return gin.H{
"id": dryRun.ID,
"command_id": dryRun.AuthorizationID,
"task_id": dryRun.TaskID,
"execution_id": dryRun.ExecutionID,
"command_sha256": dryRun.CommandSHA256,
"status": dryRun.Status,
"card_signature": dryRun.CardSignature,
"detail_signature": dryRun.DetailSignature,
"observed_title": dryRun.ObservedTitle,
"selected_sku": dryRun.SelectedSKU,
"quantity": dryRun.Quantity,
"unit_price_cents": dryRun.UnitPriceCents,
"total_price_cents": dryRun.TotalPriceCents,
"evidence_asset_id": dryRun.EvidenceAssetID,
"evidence_sha256": dryRun.EvidenceSHA256,
"started_at": formatTime(dryRun.StartedAt),
"ready_at": formatOptionalTime(dryRun.ReadyAt),
}
}
func (handler *deviceHandlers) pullOrderCommand(ctx *gin.Context) {
principal, ok := devicePrincipal(ctx)
if !ok {