feat(desktop): isolate platform accounts per desktop client
Frontend CI / Frontend (push) Successful in 3m4s
Backend CI / Backend (push) Successful in 14m24s

Scope desktop media accounts by the authenticated desktop client_id so
the same user logging in from Mac and Windows no longer sees a shared
account list. The list, identity lookup, upsert, patch, tombstone and
async health sink paths now all require matching client_id, and the
active uniqueness index moves from (workspace, platform, uid) to
(workspace, client, platform, uid).

Also strengthen the desktop client_id seed: when the OS machine id is
unavailable, fall back to a hashed fingerprint of stable hardware MAC
addresses before the random installation id, so the same hardware
keeps the same client across reinstalls.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-06 13:13:11 +08:00
parent f6aed87e44
commit 1ba29b9a09
14 changed files with 378 additions and 24 deletions
@@ -155,6 +155,9 @@ func validateBufferedDesktopAccountHealthReport(report bufferedDesktopAccountHea
if _, err := uuid.Parse(strings.TrimSpace(report.AccountID)); err != nil {
return fmt.Errorf("invalid account_id: %w", err)
}
if _, err := uuid.Parse(strings.TrimSpace(report.ClientID)); err != nil {
return fmt.Errorf("invalid client_id: %w", err)
}
if report.WorkspaceID == 0 {
return fmt.Errorf("workspace_id is required")
}
@@ -188,9 +191,10 @@ SET health = $1,
updated_at = now()
WHERE desktop_id = $5::uuid
AND workspace_id = $6
AND client_id = $7::uuid
AND deleted_at IS NULL
AND (last_check_at IS NULL OR last_check_at <= $4::timestamptz)
`, report.Health, desktopAccountLegacyStatusFromHealth(report.Health), verifiedAt, checkedAt, report.AccountID, report.WorkspaceID)
`, report.Health, desktopAccountLegacyStatusFromHealth(report.Health), verifiedAt, checkedAt, report.AccountID, report.WorkspaceID, report.ClientID)
if err != nil {
return fmt.Errorf("sink desktop account health snapshot: %w", err)
}
@@ -133,12 +133,12 @@ type bufferedDesktopAccountHealthReport struct {
ReceivedAt time.Time `json:"received_at"`
}
func (s *DesktopAccountService) ListByUser(ctx context.Context, client *repository.DesktopClient) ([]DesktopAccountView, error) {
func (s *DesktopAccountService) ListByClient(ctx context.Context, client *repository.DesktopClient) ([]DesktopAccountView, error) {
if client == nil {
return nil, response.ErrUnauthorized(40108, "desktop_client_missing", "desktop client context is required")
}
rows, err := s.repo.ListByUser(ctx, client.WorkspaceID, client.UserID)
rows, err := s.repo.ListByClient(ctx, client.WorkspaceID, client.ID)
if err != nil {
return nil, response.ErrInternal(50081, "desktop_accounts_query_failed", "failed to list desktop accounts")
}
@@ -177,7 +177,7 @@ func (s *DesktopAccountService) Upsert(ctx context.Context, client *repository.D
}
if req.IfSyncVersion != nil {
existing, err := s.repo.GetByIdentity(ctx, client.WorkspaceID, req.Platform, req.PlatformUID)
existing, err := s.repo.GetByIdentity(ctx, client.WorkspaceID, client.ID, req.Platform, req.PlatformUID)
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return nil, response.ErrInternal(50082, "desktop_account_lookup_failed", "failed to inspect desktop account before upsert")
}
@@ -346,6 +346,7 @@ func (s *DesktopAccountService) Patch(ctx context.Context, client *repository.De
VerifiedAt: req.VerifiedAt,
Tags: req.Tags,
IfSyncVersion: req.IfSyncVersion,
ClientID: client.ID,
})
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
@@ -363,7 +364,7 @@ func (s *DesktopAccountService) Tombstone(ctx context.Context, client *repositor
return nil, response.ErrUnauthorized(40108, "desktop_client_missing", "desktop client context is required")
}
account, err := s.repo.Tombstone(ctx, desktopID, client.WorkspaceID, ifSyncVersion)
account, err := s.repo.Tombstone(ctx, desktopID, client.WorkspaceID, client.ID, ifSyncVersion)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, response.ErrConflict(40982, "desktop_account_sync_conflict", "desktop account sync version conflict")