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
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:
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user