159 lines
5.0 KiB
Go
159 lines
5.0 KiB
Go
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
|
|
}
|