feat(devices): P1 设备注册打通 —— 登录/注册即写 devices 表
ci-pangolin / Lint — shellcheck (push) Successful in 8s
ci-pangolin / OpenAPI Sync Check (push) Successful in 16s
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Successful in 7s
ci-pangolin / Flutter — analyze + test (push) Successful in 26s
ci-pangolin / Portable SQL — 可移植性 (mysql/sqlite) (push) Successful in 5s
ci-pangolin / Codegen Drift — token 生成物未漂移 (push) Successful in 5s
ci-pangolin / Go — build + test (push) Failing after 10s
ci-pangolin / E2E Smoke — L4 进程级端到端 (push) Successful in 15s
ci-pangolin / Go — integration (mysql/redis testcontainers) (push) Failing after 4m10s
ci-pangolin / Golden — 视觉回归 (components + auth) (push) Successful in 14s

后端:auth.Service 加 DeviceMeta + DeviceRegistrar 接口(consumer-side 解耦),
Login/Register 成功签发后 best-effort 注册设备(不强制设备上限,避免免费档重装
churn 锁死用户);handler 加 device 请求体;main 用 authDeviceRegistrar 适配
devices.Service 注入;normalizePlatform 加 linux。
客户端:新 device_identity.dart(SecureKV 接缝 + 稳定 UUIDv4 device_id 持久化 +
名称/平台/版本);弃用硬编码 'mac-001';auth_api login/register + connect 携带
device 元数据。加 uuid + device_info_plus 依赖。
测试:auth 设备注册(触发/best-effort/空 meta) + device_identity(生成/持久/
读失败不重生成/UUIDv4 形态);normalizePlatform linux=true。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-29 00:28:13 +08:00
parent 889cff4556
commit c0c4b94e29
18 changed files with 498 additions and 96 deletions
+17 -17
View File
@@ -40,7 +40,7 @@ func TestService_RegisterFullFlow(t *testing.T) {
}
code := codeInRedis(t, svc, email)
pair, apiErr := svc.Register(ctx, email, code, "supersecret")
pair, apiErr := svc.Register(ctx, email, code, "supersecret", DeviceMeta{})
if apiErr != nil {
t.Fatalf("Register: %v", apiErr)
}
@@ -84,7 +84,7 @@ func TestService_DuplicateEmailConflict(t *testing.T) {
// First registration.
_, _ = svc.SendCode(ctx, email, "")
if _, e := svc.Register(ctx, email, codeInRedis(t, svc, email), "password1"); e != nil {
if _, e := svc.Register(ctx, email, codeInRedis(t, svc, email), "password1", DeviceMeta{}); e != nil {
t.Fatalf("first register: %v", e)
}
@@ -103,7 +103,7 @@ func TestService_DuplicateEmailConflict(t *testing.T) {
if err := svc.rdb.Set(ctx, codeKey(email), "654321", 10*time.Minute).Err(); err != nil {
t.Fatalf("force code: %v", err)
}
_, apiErr := svc.Register(ctx, email, "654321", "password2")
_, apiErr := svc.Register(ctx, email, "654321", "password2", DeviceMeta{})
if apiErr == nil || apiErr.Code != ErrCodeInvalid.Code {
t.Fatalf("want code_invalid (anti-enumeration), got %v", apiErr)
}
@@ -115,7 +115,7 @@ func TestService_CodeWrong(t *testing.T) {
const email = "wrong@example.com"
_, _ = svc.SendCode(ctx, email, "")
_, apiErr := svc.Register(ctx, email, "000000", "password1")
_, apiErr := svc.Register(ctx, email, "000000", "password1", DeviceMeta{})
if apiErr == nil || apiErr.Code != ErrCodeInvalid.Code {
t.Fatalf("want code_invalid, got %v", apiErr)
}
@@ -131,7 +131,7 @@ func TestService_CodeExpired(t *testing.T) {
// Expire the code key.
svc.rdb.Del(ctx, codeKey(email))
_, apiErr := svc.Register(ctx, email, code, "password1")
_, apiErr := svc.Register(ctx, email, code, "password1", DeviceMeta{})
if apiErr == nil || apiErr.Code != ErrCodeInvalid.Code {
t.Fatalf("want code_invalid after expiry, got %v", apiErr)
}
@@ -144,11 +144,11 @@ func TestService_CodeReuseRejected(t *testing.T) {
_, _ = svc.SendCode(ctx, email, "")
code := codeInRedis(t, svc, email)
if _, e := svc.Register(ctx, email, code, "password1"); e != nil {
if _, e := svc.Register(ctx, email, code, "password1", DeviceMeta{}); e != nil {
t.Fatalf("first register: %v", e)
}
// Re-using the consumed code must fail.
_, apiErr := svc.Register(ctx, "other@example.com", code, "password1")
_, apiErr := svc.Register(ctx, "other@example.com", code, "password1", DeviceMeta{})
if apiErr == nil || apiErr.Code != ErrCodeInvalid.Code {
t.Fatalf("want code_invalid on reuse, got %v", apiErr)
}
@@ -163,12 +163,12 @@ func TestService_CodeBruteForceBurned(t *testing.T) {
// 3 wrong attempts burn the code.
for i := 0; i < 3; i++ {
if _, e := svc.Register(ctx, email, "999999", "password1"); e == nil {
if _, e := svc.Register(ctx, email, "999999", "password1", DeviceMeta{}); e == nil {
t.Fatal("wrong code should fail")
}
}
// Even the correct code no longer works.
if _, e := svc.Register(ctx, email, good, "password1"); e == nil || e.Code != ErrCodeInvalid.Code {
if _, e := svc.Register(ctx, email, good, "password1", DeviceMeta{}); e == nil || e.Code != ErrCodeInvalid.Code {
t.Fatalf("burned code should reject correct value, got %v", e)
}
}
@@ -205,25 +205,25 @@ func TestService_LoginAndLockout(t *testing.T) {
const pw = "rightpassword"
_, _ = svc.SendCode(ctx, email, "")
if _, e := svc.Register(ctx, email, codeInRedis(t, svc, email), pw); e != nil {
if _, e := svc.Register(ctx, email, codeInRedis(t, svc, email), pw, DeviceMeta{}); e != nil {
t.Fatalf("register: %v", e)
}
// Correct login works.
pair, _, apiErr := svc.Login(ctx, email, pw, "")
pair, _, apiErr := svc.Login(ctx, email, pw, "", DeviceMeta{})
if apiErr != nil || pair == nil {
t.Fatalf("login should succeed: %v", apiErr)
}
// 3 wrong attempts.
for i := 0; i < 3; i++ {
_, _, e := svc.Login(ctx, email, "wrong", "")
_, _, e := svc.Login(ctx, email, "wrong", "", DeviceMeta{})
if e == nil || e.Code != ErrInvalidCredentials.Code {
t.Fatalf("attempt %d want invalid_credentials, got %v", i, e)
}
}
// Now locked, even with the correct password.
_, ra, e := svc.Login(ctx, email, pw, "")
_, ra, e := svc.Login(ctx, email, pw, "", DeviceMeta{})
if e == nil || e.Code != ErrAccountLocked.Code {
t.Fatalf("want account_locked, got %v", e)
}
@@ -234,7 +234,7 @@ func TestService_LoginAndLockout(t *testing.T) {
func TestService_LoginUnknownUser(t *testing.T) {
svc, _, _ := newService(t, ServiceConfig{})
_, _, apiErr := svc.Login(context.Background(), "ghost@example.com", "whatever", "")
_, _, apiErr := svc.Login(context.Background(), "ghost@example.com", "whatever", "", DeviceMeta{})
if apiErr == nil || apiErr.Code != ErrInvalidCredentials.Code {
t.Fatalf("want invalid_credentials for unknown user, got %v", apiErr)
}
@@ -247,12 +247,12 @@ func TestService_BannedUserRejected(t *testing.T) {
const pw = "password1"
_, _ = svc.SendCode(ctx, email, "")
if _, e := svc.Register(ctx, email, codeInRedis(t, svc, email), pw); e != nil {
if _, e := svc.Register(ctx, email, codeInRedis(t, svc, email), pw, DeviceMeta{}); e != nil {
t.Fatalf("register: %v", e)
}
store.setStatus(email, "banned")
_, _, apiErr := svc.Login(ctx, email, pw, "")
_, _, apiErr := svc.Login(ctx, email, pw, "", DeviceMeta{})
if apiErr == nil || apiErr.Code != ErrAccountBanned.Code {
t.Fatalf("want account_banned, got %v", apiErr)
}
@@ -264,7 +264,7 @@ func TestService_RefreshRotation(t *testing.T) {
const email = "refresh@example.com"
_, _ = svc.SendCode(ctx, email, "")
pair, e := svc.Register(ctx, email, codeInRedis(t, svc, email), "password1")
pair, e := svc.Register(ctx, email, codeInRedis(t, svc, email), "password1", DeviceMeta{})
if e != nil {
t.Fatalf("register: %v", e)
}