124 lines
6.2 KiB
Go
124 lines
6.2 KiB
Go
package onboarding
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"errors"
|
||
|
|
|
||
|
|
"data-terminal-cloud/internal/identity"
|
||
|
|
"github.com/jackc/pgx/v5"
|
||
|
|
)
|
||
|
|
|
||
|
|
type BLEDeviceInput struct {
|
||
|
|
DeviceID string `json:"device_id"`
|
||
|
|
}
|
||
|
|
|
||
|
|
// BLEDeviceState does not authenticate the radio. It authorizes the signed-in
|
||
|
|
// account and returns the authoritative cloud binding state, never owner data.
|
||
|
|
func (s *Service) BLEDeviceState(ctx context.Context, p identity.Principal, in BLEDeviceInput) (map[string]any, error) {
|
||
|
|
a, err := readBLEDeviceAccess(ctx, s.pool, p, in.DeviceID, false)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
return map[string]any{"device_id": in.DeviceID, "id": a.Device.PublicID, "model": a.Device.Model, "bound": a.Bound}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
type bleDeviceAccess struct {
|
||
|
|
Device Device
|
||
|
|
UserID string
|
||
|
|
Bound bool
|
||
|
|
}
|
||
|
|
|
||
|
|
// Keep state checks and token issuance consistent. Errors describe the failed
|
||
|
|
// condition, never the identity or profile of another device owner.
|
||
|
|
func readBLEDeviceAccess(ctx context.Context, q queryer, p identity.Principal, factoryID string, lock bool) (bleDeviceAccess, error) {
|
||
|
|
var a bleDeviceAccess
|
||
|
|
if !factoryPattern.MatchString(factoryID) {
|
||
|
|
return a, fail(400, "DEVICE_ID_INVALID", "设备编号格式不正确,请重新搜索并选择设备")
|
||
|
|
}
|
||
|
|
d := &a.Device
|
||
|
|
var tenantStatus, userStatus, accountKind string
|
||
|
|
var knownAccount, familyBound, accountBound, accountCurrent, ownAccount, manageFamily bool
|
||
|
|
suffix := ""
|
||
|
|
if lock {
|
||
|
|
suffix = " FOR UPDATE OF d"
|
||
|
|
}
|
||
|
|
err := q.QueryRow(ctx, `SELECT d.id::text,d.public_id::text,d.tenant_id::text,d.factory_device_id,d.model,
|
||
|
|
d.status,d.activation_state,d.enrollment_mode,d.binding_generation,d.onboarding_released_at IS NOT NULL,t.status,
|
||
|
|
COALESCE(ua.id::text,''),COALESCE(ua.status,''),COALESCE(ua.account_kind,''),
|
||
|
|
EXISTS(SELECT 1 FROM user_account WHERE oidc_issuer=$2 AND oidc_subject=$3),
|
||
|
|
EXISTS(SELECT 1 FROM device_binding WHERE device_id=d.id AND unbound_at IS NULL),
|
||
|
|
EXISTS(SELECT 1 FROM device_account_binding WHERE device_id=d.id AND released_at IS NULL),
|
||
|
|
EXISTS(SELECT 1 FROM device_account_binding WHERE device_id=d.id AND released_at IS NULL AND generation=d.binding_generation),
|
||
|
|
EXISTS(SELECT 1 FROM device_account_binding WHERE device_id=d.id AND released_at IS NULL AND generation=d.binding_generation AND user_id=ua.id),
|
||
|
|
EXISTS(SELECT 1 FROM device_binding db JOIN household_member hm ON hm.household_id=db.household_id AND hm.tenant_id=db.tenant_id
|
||
|
|
JOIN household h ON h.id=hm.household_id AND h.status='active'
|
||
|
|
WHERE db.device_id=d.id AND db.unbound_at IS NULL AND hm.user_id=ua.id AND hm.status='active' AND hm.role IN ('owner','admin','editor'))
|
||
|
|
FROM device d JOIN tenant t ON t.id=d.tenant_id
|
||
|
|
LEFT JOIN user_account ua ON ua.tenant_id=d.tenant_id AND ua.oidc_issuer=$2 AND ua.oidc_subject=$3
|
||
|
|
WHERE d.factory_device_id=$1`+suffix, factoryID, p.Issuer, p.Subject).Scan(
|
||
|
|
&d.ID, &d.PublicID, &d.TenantID, &d.FactoryID, &d.Model, &d.Status, &d.Lifecycle, &d.Mode, &d.Generation, &d.Released,
|
||
|
|
&tenantStatus, &a.UserID, &userStatus, &accountKind, &knownAccount, &familyBound, &accountBound, &accountCurrent, &ownAccount, &manageFamily)
|
||
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
||
|
|
return a, fail(404, "DEVICE_NOT_REGISTERED", "平台未找到该设备的注册记录,请联系管理员登记设备")
|
||
|
|
}
|
||
|
|
if err != nil {
|
||
|
|
return a, err
|
||
|
|
}
|
||
|
|
if a.UserID == "" {
|
||
|
|
if knownAccount {
|
||
|
|
return a, fail(403, "DEVICE_TENANT_MISMATCH", "设备不属于当前账号所在组织,请切换到设备所属组织的账号")
|
||
|
|
}
|
||
|
|
return a, fail(403, "ACCOUNT_NOT_FOUND", "当前登录身份没有对应的平台账号,请重新登录或联系管理员")
|
||
|
|
}
|
||
|
|
if userStatus != "active" {
|
||
|
|
if userStatus == "deleted" {
|
||
|
|
return a, fail(403, "ACCOUNT_DELETED", "当前账号已删除,请使用其他有效账号登录")
|
||
|
|
}
|
||
|
|
return a, fail(403, "ACCOUNT_DISABLED", "当前账号已停用,请联系管理员恢复账号")
|
||
|
|
}
|
||
|
|
if accountKind != "customer" {
|
||
|
|
return a, fail(403, "CUSTOMER_REQUIRED", "当前登录的是管理账号,请切换到家庭账号添加设备")
|
||
|
|
}
|
||
|
|
if tenantStatus != "active" {
|
||
|
|
return a, fail(403, "TENANT_INACTIVE", "设备所属组织已停用,请联系平台管理员")
|
||
|
|
}
|
||
|
|
if d.Status == "revoked" || d.Lifecycle == "REVOKED" {
|
||
|
|
return a, fail(403, "DEVICE_REVOKED", "设备已被停用,无法添加或配网,请联系管理员")
|
||
|
|
}
|
||
|
|
if d.Lifecycle == "LOST" {
|
||
|
|
return a, fail(403, "DEVICE_LOST", "设备已被标记为丢失,请先联系管理员解除丢失状态")
|
||
|
|
}
|
||
|
|
if d.Status == "quarantined" {
|
||
|
|
return a, fail(403, "DEVICE_QUARANTINED", "设备已被隔离,暂时禁止添加或配网,请联系管理员检查设备")
|
||
|
|
}
|
||
|
|
if d.Mode != "physical_confirm" {
|
||
|
|
return a, fail(409, "DEVICE_ENROLLMENT_UNSUPPORTED", "该设备尚未启用蓝牙实体确认绑定,请联系管理员完成设备接入配置")
|
||
|
|
}
|
||
|
|
if !d.Released {
|
||
|
|
return a, fail(409, "DEVICE_NOT_RELEASED", "设备已登记但尚未放行,请联系管理员完成出厂放行")
|
||
|
|
}
|
||
|
|
if accountBound && !accountCurrent && !manageFamily {
|
||
|
|
return a, fail(409, "DEVICE_BINDING_STALE", "设备归属记录与当前设备状态不一致,请联系管理员处理")
|
||
|
|
}
|
||
|
|
if accountBound && !ownAccount && !manageFamily {
|
||
|
|
return a, fail(403, "DEVICE_OWNED_BY_OTHER_ACCOUNT", "设备已添加到其他账号,请使用原账号操作;如需更换账号,请先由原账号移除设备")
|
||
|
|
}
|
||
|
|
if familyBound && !manageFamily && !ownAccount {
|
||
|
|
return a, fail(403, "DEVICE_MANAGE_PERMISSION_REQUIRED", "设备已绑定家庭,当前账号没有该家庭的设备管理权限,请联系家庭管理员授权")
|
||
|
|
}
|
||
|
|
a.Bound = familyBound || accountCurrent
|
||
|
|
return a, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// DeviceBLEState is read-only: an empty BLE bind_token must never create a
|
||
|
|
// binding or bypass a cloud reset. Device authentication remains HMAC + HTTPS.
|
||
|
|
func (s *Service) DeviceBLEState(ctx context.Context, d Device) (map[string]any, error) {
|
||
|
|
if !deviceAllowed(d) {
|
||
|
|
return nil, errAccess
|
||
|
|
}
|
||
|
|
var bound bool
|
||
|
|
err := s.pool.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM device_binding WHERE device_id=$1::uuid AND unbound_at IS NULL)
|
||
|
|
OR EXISTS(SELECT 1 FROM device_account_binding ab JOIN device d ON d.id=ab.device_id WHERE ab.device_id=$1::uuid AND ab.released_at IS NULL AND ab.generation=d.binding_generation)`, d.ID).Scan(&bound)
|
||
|
|
return map[string]any{"bound": bound}, err
|
||
|
|
}
|