feat: complete device onboarding and mobile binding workflows
ci / verify (push) Canceled after 0s

This commit is contained in:
Codex Local
2026-09-12 08:57:29 +08:00
parent 7846f30d0b
commit 98fb2965d0
116 changed files with 15669 additions and 64 deletions
+69 -5
View File
@@ -16,6 +16,7 @@ import (
"data-terminal-cloud/internal/audit"
"data-terminal-cloud/internal/deviceactivation"
"data-terminal-cloud/internal/deviceauth"
"data-terminal-cloud/internal/devicebinding"
"data-terminal-cloud/internal/idempotency"
"github.com/google/uuid"
@@ -48,6 +49,7 @@ type Claim struct {
}
type Response struct {
BindingGeneration int64 `json:"binding_generation,omitempty"`
ResetID string `json:"reset_id"`
DeviceID string `json:"device_id"`
ActivationState string `json:"activation_state"`
@@ -59,7 +61,7 @@ type Response struct {
EndedConversationCount int `json:"ended_conversation_count"`
CancelledCommandCount int64 `json:"cancelled_command_count"`
ConfigRevision int64 `json:"config_revision"`
Claim Claim `json:"claim"`
Claim *Claim `json:"claim,omitempty"`
LocalActions []string `json:"local_actions"`
ResetAt time.Time `json:"reset_at"`
}
@@ -102,11 +104,40 @@ func (s *Service) Reset(ctx context.Context, identity deviceauth.Identity, idemp
return Response{}, false, ErrPendingTranscripts
}
tenantID := identity.TenantID
result, err := s.executor.Execute(ctx, idempotency.Request{
if err := devicebinding.RequireUnboundForReset(ctx, s.pool, identity.ID); err != nil {
return Response{}, false, err
}
guard := func(ctx context.Context, tx pgx.Tx, cached *idempotency.Result) error {
var mode string
var generation, counter int64
if err := tx.QueryRow(ctx, `SELECT enrollment_mode,binding_generation,factory_reset_counter FROM device WHERE id=$1::uuid FOR UPDATE`, identity.ID).Scan(&mode, &generation, &counter); err != nil {
return err
}
if mode != "physical_confirm" {
return nil
}
if generation != identity.BindingGeneration {
return ErrStateConflict
}
if err := devicebinding.RequireUnboundForReset(ctx, tx, identity.ID); err != nil {
return err
}
if cached != nil {
var previous Response
if err := json.Unmarshal(cached.Body, &previous); err != nil {
return err
}
if counter != input.ResetCounter || generation != previous.BindingGeneration {
return ErrResetCounterConflict
}
}
return nil
}
result, err := s.executor.ExecuteGuarded(ctx, idempotency.Request{
TenantID: &tenantID, ActorKey: "device:" + identity.PublicID,
Scope: "POST /device/v3/factory-reset", Key: idempotencyKey,
RequestHash: idempotency.Hash(rawRequest), ExpiresAfter: 7 * 24 * time.Hour,
}, func(operationContext context.Context, tx pgx.Tx) (idempotency.Result, error) {
}, guard, func(operationContext context.Context, tx pgx.Tx) (idempotency.Result, error) {
response, resetErr := s.reset(operationContext, tx, identity, requestID, input)
if resetErr != nil {
return idempotency.Result{}, resetErr
@@ -145,6 +176,9 @@ func (s *Service) reset(ctx context.Context, tx pgx.Tx, identity deviceauth.Iden
if input.ResetCounter <= currentCounter {
return Response{}, ErrResetCounterConflict
}
if err := devicebinding.RequireUnboundForReset(ctx, tx, identity.ID); err != nil {
return Response{}, err
}
var activeFirmware int
if err := tx.QueryRow(ctx, `
@@ -233,10 +267,35 @@ func (s *Service) reset(ctx context.Context, tx pgx.Tx, identity deviceauth.Iden
return Response{}, fmt.Errorf("record factory reset counter: %w", err)
}
claim, err := issueClaim(ctx, tx, identity, now)
if err != nil {
var claim *Claim
var mode string
if err = tx.QueryRow(ctx, `SELECT enrollment_mode FROM device WHERE id=$1::uuid`, identity.ID).Scan(&mode); err != nil {
return Response{}, err
}
if mode == "legacy" {
value, e := issueClaim(ctx, tx, identity, now)
if e != nil {
return Response{}, e
}
claim = &value
} else {
_, err = tx.Exec(ctx, `UPDATE device SET binding_generation=binding_generation+1,onboarding_revision=onboarding_revision+1,onboarding_boot_id=NULL WHERE id=$1::uuid`, identity.ID)
if err != nil {
return Response{}, err
}
_, err = tx.Exec(ctx, `UPDATE device_setup_session SET closed_at=COALESCE(closed_at,now()),close_reason='FACTORY_RESET' WHERE device_id=$1::uuid`, identity.ID)
if err != nil {
return Response{}, err
}
_, err = tx.Exec(ctx, `UPDATE device_binding_claim SET status='CANCELLED',completed_at=now() WHERE device_id=$1::uuid AND status='PENDING'`, identity.ID)
if err != nil {
return Response{}, err
}
_, err = tx.Exec(ctx, `UPDATE onboarding_web_session SET revoked_at=COALESCE(revoked_at,now()) WHERE setup_id IN (SELECT id FROM device_setup_session WHERE device_id=$1::uuid)`, identity.ID)
if err != nil {
return Response{}, err
}
}
response := Response{
ResetID: input.ResetID, DeviceID: identity.PublicID, ActivationState: string(deviceactivation.Claimable),
ResetCounter: input.ResetCounter, BindingRemoved: bindingRemoved, PreviousBindingID: previousBindingID,
@@ -245,6 +304,11 @@ func (s *Service) reset(ctx context.Context, tx pgx.Tx, identity deviceauth.Iden
ConfigRevision: configRevision, Claim: claim, ResetAt: now,
LocalActions: []string{"clear_volc_credential", "clear_user_context", "clear_session_state", "clear_network_credentials", "reboot_to_claimable"},
}
if mode == "physical_confirm" {
if err = tx.QueryRow(ctx, `SELECT binding_generation FROM device WHERE id=$1::uuid`, identity.ID).Scan(&response.BindingGeneration); err != nil {
return Response{}, err
}
}
actorID := identity.PublicID
if err := s.audit.Record(ctx, tx, audit.Event{
TenantID: &identity.TenantID, ActorType: "device", ActorPublicID: &actorID,