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:
@@ -77,6 +77,22 @@ func (c *ServiceConfig) withDefaults() {
|
||||
}
|
||||
}
|
||||
|
||||
// DeviceMeta is the client-reported device identity carried on login/register so
|
||||
// the device can be registered (devices table) and, later, bound to a session.
|
||||
type DeviceMeta struct {
|
||||
DeviceID string // client-generated stable UUID (secure storage)
|
||||
Name string // host/model name
|
||||
Platform string // ios|android|windows|macos|linux
|
||||
ClientVersion string // app version (stored from P2 onward)
|
||||
}
|
||||
|
||||
// DeviceRegistrar registers the logging-in device. Defined consumer-side to
|
||||
// avoid an import cycle; devices.Service is adapted to it in main wiring.
|
||||
// Registration is best-effort and must never block login (see registerDevice).
|
||||
type DeviceRegistrar interface {
|
||||
RegisterDevice(ctx context.Context, userID int64, meta DeviceMeta) error
|
||||
}
|
||||
|
||||
// Service is the auth business layer: code issuance, registration, login, and
|
||||
// token refresh. It is safe for concurrent use.
|
||||
type Service struct {
|
||||
@@ -87,6 +103,24 @@ type Service struct {
|
||||
mailer Mailer
|
||||
cfg ServiceConfig
|
||||
now func() time.Time
|
||||
devReg DeviceRegistrar // nil until wired; registration is best-effort
|
||||
}
|
||||
|
||||
// SetDeviceRegistrar wires the device registrar after construction (main keeps
|
||||
// auth and devices decoupled). Safe to call once during startup.
|
||||
func (s *Service) SetDeviceRegistrar(r DeviceRegistrar) { s.devReg = r }
|
||||
|
||||
// registerDevice records the logging-in device. Best-effort: a registrar error
|
||||
// (device cap, transient DB) is logged but never fails the login/registration —
|
||||
// the user must always be able to get in (notably: free-plan reinstall churns
|
||||
// the device UUID, so a hard cap here would lock users out).
|
||||
func (s *Service) registerDevice(ctx context.Context, userID int64, meta DeviceMeta) {
|
||||
if s.devReg == nil || meta.DeviceID == "" {
|
||||
return
|
||||
}
|
||||
if err := s.devReg.RegisterDevice(ctx, userID, meta); err != nil {
|
||||
slog.Warn("auth: device register failed (login proceeds)", "uid", userID, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// NewService wires the auth service. now may be nil (defaults to time.Now).
|
||||
@@ -184,7 +218,7 @@ func (s *Service) SendCode(ctx context.Context, rawEmail, ip string) (retryAfter
|
||||
|
||||
// Register verifies the code (one-time), creates the account plus a 7-day PRO
|
||||
// trial in a single transaction, and returns a fresh token pair.
|
||||
func (s *Service) Register(ctx context.Context, rawEmail, code, password string) (*TokenPair, *apierr.Error) {
|
||||
func (s *Service) Register(ctx context.Context, rawEmail, code, password string, device DeviceMeta) (*TokenPair, *apierr.Error) {
|
||||
email := NormalizeEmail(rawEmail)
|
||||
if !ValidEmail(email) || len(password) < 8 || len(code) != 6 {
|
||||
return nil, ErrInvalidRequest
|
||||
@@ -217,6 +251,7 @@ func (s *Service) Register(ctx context.Context, rawEmail, code, password string)
|
||||
if err != nil {
|
||||
return nil, ErrInternal
|
||||
}
|
||||
s.registerDevice(ctx, user.ID, device)
|
||||
return pair, nil
|
||||
}
|
||||
|
||||
@@ -272,7 +307,7 @@ const (
|
||||
totpPendingTTL = 5 * time.Minute
|
||||
)
|
||||
|
||||
func (s *Service) Login(ctx context.Context, rawEmail, password, ip string) (*LoginOutcome, time.Duration, *apierr.Error) {
|
||||
func (s *Service) Login(ctx context.Context, rawEmail, password, ip string, device DeviceMeta) (*LoginOutcome, time.Duration, *apierr.Error) {
|
||||
_ = ip // IP reserved for future per-IP login throttling; not logged.
|
||||
email := NormalizeEmail(rawEmail)
|
||||
if email == "" || password == "" {
|
||||
@@ -329,6 +364,7 @@ func (s *Service) Login(ctx context.Context, rawEmail, password, ip string) (*Lo
|
||||
if err != nil {
|
||||
return nil, 0, ErrInternal
|
||||
}
|
||||
s.registerDevice(ctx, user.ID, device)
|
||||
return &LoginOutcome{Tokens: pair}, 0, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user