BEGIN; -- Physical possession can establish account ownership before a family exists. CREATE TABLE device_account_binding ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), device_id uuid NOT NULL, tenant_id uuid NOT NULL, user_id uuid NOT NULL, generation bigint NOT NULL, bound_at timestamptz NOT NULL DEFAULT now(), released_at timestamptz, assigned_binding_id uuid REFERENCES device_binding(id), FOREIGN KEY(device_id,tenant_id) REFERENCES device(id,tenant_id), FOREIGN KEY(user_id,tenant_id) REFERENCES user_account(id,tenant_id) ); CREATE UNIQUE INDEX device_account_binding_active ON device_account_binding(device_id) WHERE released_at IS NULL; CREATE INDEX device_account_binding_user ON device_account_binding(user_id) WHERE released_at IS NULL; ALTER TABLE device_ble_binding ALTER COLUMN household_id DROP NOT NULL, ALTER COLUMN subject_id DROP NOT NULL; ALTER TABLE device_ble_binding ADD COLUMN account_binding_id uuid REFERENCES device_account_binding(id); ALTER TABLE device_ble_binding ADD CONSTRAINT device_ble_binding_profile_pair CHECK ((household_id IS NULL) = (subject_id IS NULL)); -- Existing physical-confirm claims cannot steal an account-owned device. CREATE FUNCTION guard_device_account_assignment() RETURNS trigger LANGUAGE plpgsql AS $$ DECLARE owner device_account_binding%ROWTYPE; BEGIN PERFORM 1 FROM device WHERE id=NEW.device_id FOR UPDATE; SELECT * INTO owner FROM device_account_binding WHERE device_id=NEW.device_id AND released_at IS NULL; IF FOUND AND (owner.user_id IS DISTINCT FROM NEW.bound_by OR owner.id::text IS DISTINCT FROM current_setting('app.device_account_binding',true)) THEN RAISE EXCEPTION 'DEVICE_CONFIRMATION_REQUIRED' USING ERRCODE='P0001'; END IF; RETURN NEW; END $$; CREATE TRIGGER device_account_assignment_guard BEFORE INSERT ON device_binding FOR EACH ROW EXECUTE FUNCTION guard_device_account_assignment(); CREATE FUNCTION finish_device_account_assignment() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN UPDATE device_account_binding SET released_at=now(),assigned_binding_id=NEW.id WHERE device_id=NEW.device_id AND released_at IS NULL; RETURN NEW; END $$; CREATE TRIGGER device_account_assignment_finish AFTER INSERT ON device_binding FOR EACH ROW EXECUTE FUNCTION finish_device_account_assignment(); CREATE FUNCTION reset_device_account_binding() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF NEW.factory_reset_counter IS DISTINCT FROM OLD.factory_reset_counter THEN UPDATE device_account_binding SET released_at=now() WHERE device_id=NEW.id AND released_at IS NULL; END IF; RETURN NEW; END $$; CREATE TRIGGER device_account_binding_reset AFTER UPDATE OF factory_reset_counter ON device FOR EACH ROW EXECUTE FUNCTION reset_device_account_binding(); COMMIT;