This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
package onboarding
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (s *Service) replayGuard(ctx context.Context, tx pgx.Tx, d Device, actor, scope string, raw []byte) error {
|
||||
if strings.HasPrefix(actor, "web:") {
|
||||
if err := s.checkWebActor(ctx, tx, strings.TrimPrefix(actor, "web:"), d); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
switch scope {
|
||||
case "register":
|
||||
var in RegisterInput
|
||||
_ = json.Unmarshal(raw, &in)
|
||||
if d.BootID != in.BootID {
|
||||
return errState
|
||||
}
|
||||
case "setup":
|
||||
var in SetupInput
|
||||
_ = json.Unmarshal(raw, &in)
|
||||
var valid bool
|
||||
err := tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM device_setup_session WHERE device_id=$1::uuid AND boot_id=$2::uuid AND local_setup_id=$3::uuid AND binding_generation=$4 AND closed_at IS NULL AND expires_at>now())`, d.ID, in.BootID, in.LocalSetupID, d.Generation).Scan(&valid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !valid {
|
||||
return fail(410, "SETUP_EXPIRED", "请创建新的恢复会话")
|
||||
}
|
||||
case "handoff":
|
||||
var in HandoffInput
|
||||
_ = json.Unmarshal(raw, &in)
|
||||
if _, err := readSetup(ctx, tx, in.SetupID, d, true); err != nil {
|
||||
return err
|
||||
}
|
||||
var valid bool
|
||||
err := tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM device_handoff WHERE setup_id=$1::uuid AND code_challenge=$2 AND consumed_at IS NULL AND expires_at>now())`, in.SetupID, in.Challenge).Scan(&valid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !valid {
|
||||
return fail(410, "HANDOFF_INVALID", "入口已消费或失效,请重新获取")
|
||||
}
|
||||
case "confirm":
|
||||
var in ConfirmationInput
|
||||
_ = json.Unmarshal(raw, &in)
|
||||
c, err := readClaim(ctx, tx, in.ClaimID, d.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = claimPermission(ctx, tx, c); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.Status != "CONFIRMED" || c.PressID != in.PressEventID || d.Generation != c.Generation+1 {
|
||||
return errState
|
||||
}
|
||||
var valid bool
|
||||
err = tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM device_binding WHERE id=$1::uuid AND generation=$2 AND unbound_at IS NULL)`, c.BindingID, d.Generation).Scan(&valid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !valid {
|
||||
return errState
|
||||
}
|
||||
case "claim":
|
||||
var in ClaimInput
|
||||
_ = json.Unmarshal(raw, &in)
|
||||
w, err := s.webSession(ctx, tx, strings.TrimPrefix(actor, "web:"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var c claimRecord
|
||||
c.UserID = w.UserID
|
||||
c.TenantID = d.TenantID
|
||||
err = tx.QueryRow(ctx, `SELECT id::text,household_id::text FROM subject_profile WHERE public_id=$1::uuid AND tenant_id=$2::uuid`, in.ElderID, d.TenantID).Scan(&c.SubjectID, &c.HouseholdID)
|
||||
if err != nil {
|
||||
return errAccess
|
||||
}
|
||||
if err = claimPermission(ctx, tx, c); err != nil {
|
||||
return err
|
||||
}
|
||||
var valid bool
|
||||
err = tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM device_binding_claim WHERE setup_id=$1::uuid AND device_id=$2::uuid AND user_id=$3::uuid AND subject_id=$4::uuid AND status='PENDING' AND expires_at>now())`, w.SetupID, d.ID, w.UserID, c.SubjectID).Scan(&valid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !valid {
|
||||
return fail(410, "CLAIM_EXPIRED", "请查询当前绑定进度")
|
||||
}
|
||||
case "attempt":
|
||||
var in AttemptInput
|
||||
_ = json.Unmarshal(raw, &in)
|
||||
j, err := readJob(ctx, tx, d.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = validConfig(s, d, j, in.ActivationID, in.ConfigVersion); err != nil {
|
||||
return err
|
||||
}
|
||||
var status string
|
||||
var expiry time.Time
|
||||
var conversation *string
|
||||
err = tx.QueryRow(ctx, `SELECT status,permit_expires_at,conversation_id::text FROM device_activation_attempt WHERE id=$1::uuid AND activation_id=$2::uuid`, in.AttemptID, j.ID).Scan(&status, &expiry, &conversation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status != "CONTACT_POSSIBLE" || conversation != nil || !expiry.After(s.now()) {
|
||||
return fail(409, "ATTEMPT_ALREADY_ISSUED", "请恢复原尝试的结果")
|
||||
}
|
||||
case "activation-ack":
|
||||
var in ActivationAckInput
|
||||
_ = json.Unmarshal(raw, &in)
|
||||
j, err := readJob(ctx, tx, d.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return validConfig(s, d, j, in.ActivationID, in.ConfigVersion)
|
||||
case "activation-result":
|
||||
var in ResultInput
|
||||
_ = json.Unmarshal(raw, &in)
|
||||
j, err := readJob(ctx, tx, d.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var version int64
|
||||
err = tx.QueryRow(ctx, `SELECT config_version FROM device_activation_attempt WHERE id=$1::uuid AND activation_id=$2::uuid`, in.AttemptID, j.ID).Scan(&version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if j.ID != in.ActivationID || j.Generation != d.Generation || j.ConfigVersion != version || j.Status == "BLOCKED" {
|
||||
return errState
|
||||
}
|
||||
return nil
|
||||
case "profile":
|
||||
var in ProfileInput
|
||||
_ = json.Unmarshal(raw, &in)
|
||||
if in.HouseholdID != "" {
|
||||
w, err := s.webSession(ctx, tx, strings.TrimPrefix(actor, "web:"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var allowed bool
|
||||
err = tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM household h JOIN household_member hm ON hm.household_id=h.id WHERE h.public_id=$1::uuid AND h.status='active' AND hm.user_id=$2::uuid AND hm.status='active' AND hm.role IN ('owner','admin','editor'))`, in.HouseholdID, w.UserID).Scan(&allowed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !allowed {
|
||||
return errAccess
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user