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
@@ -52,6 +52,7 @@ type UpsertDesktopAccountParams struct {
type PatchDesktopAccountParams struct {
DesktopID uuid.UUID
WorkspaceID int64
ClientID uuid.UUID
DisplayName *string
Health *string
VerifiedAt *time.Time
@@ -60,12 +61,13 @@ type PatchDesktopAccountParams struct {
}
type DesktopAccountRepository interface {
ListByClient(ctx context.Context, workspaceID int64, clientID uuid.UUID) ([]DesktopAccount, error)
ListByUser(ctx context.Context, workspaceID, userID int64) ([]DesktopAccount, error)
GetByDesktopID(ctx context.Context, desktopID uuid.UUID, workspaceID int64) (*DesktopAccount, error)
GetByIdentity(ctx context.Context, workspaceID int64, platform, platformUID string) (*DesktopAccount, error)
GetByIdentity(ctx context.Context, workspaceID int64, clientID uuid.UUID, platform, platformUID string) (*DesktopAccount, error)
Upsert(ctx context.Context, params UpsertDesktopAccountParams) (*DesktopAccount, error)
Patch(ctx context.Context, params PatchDesktopAccountParams) (*DesktopAccount, error)
Tombstone(ctx context.Context, desktopID uuid.UUID, workspaceID, ifSyncVersion int64) (*DesktopAccount, error)
Tombstone(ctx context.Context, desktopID uuid.UUID, workspaceID int64, clientID uuid.UUID, ifSyncVersion int64) (*DesktopAccount, error)
MarkDeleteRequested(ctx context.Context, desktopID uuid.UUID, workspaceID int64) (*DesktopAccount, error)
ClearDeleteRequested(ctx context.Context, desktopID uuid.UUID, workspaceID int64) (*DesktopAccount, error)
}
@@ -78,6 +80,26 @@ func NewDesktopAccountRepository(db generated.DBTX) DesktopAccountRepository {
return &desktopAccountRepository{q: newQuerier(db)}
}
func (r *desktopAccountRepository) ListByClient(ctx context.Context, workspaceID int64, clientID uuid.UUID) ([]DesktopAccount, error) {
rows, err := r.q.ListDesktopAccountsByClient(ctx, generated.ListDesktopAccountsByClientParams{
WorkspaceID: workspaceID,
ClientID: pgUUID(clientID),
})
if err != nil {
return nil, err
}
items := make([]DesktopAccount, 0, len(rows))
for _, row := range rows {
item, err := desktopAccountFromClientListRow(row)
if err != nil {
return nil, err
}
items = append(items, *item)
}
return items, nil
}
func (r *desktopAccountRepository) ListByUser(ctx context.Context, workspaceID, userID int64) ([]DesktopAccount, error) {
rows, err := r.q.ListDesktopAccountsByUser(ctx, generated.ListDesktopAccountsByUserParams{
WorkspaceID: workspaceID,
@@ -109,9 +131,10 @@ func (r *desktopAccountRepository) GetByDesktopID(ctx context.Context, desktopID
return desktopAccountFromGetRow(row)
}
func (r *desktopAccountRepository) GetByIdentity(ctx context.Context, workspaceID int64, platform, platformUID string) (*DesktopAccount, error) {
func (r *desktopAccountRepository) GetByIdentity(ctx context.Context, workspaceID int64, clientID uuid.UUID, platform, platformUID string) (*DesktopAccount, error) {
row, err := r.q.GetDesktopAccountByIdentity(ctx, generated.GetDesktopAccountByIdentityParams{
WorkspaceID: workspaceID,
ClientID: pgUUID(clientID),
PlatformID: platform,
PlatformUid: platformUID,
})
@@ -177,6 +200,7 @@ func (r *desktopAccountRepository) Patch(ctx context.Context, params PatchDeskto
Tags: tagsJSON,
DesktopID: pgUUID(params.DesktopID),
WorkspaceID: params.WorkspaceID,
ClientID: pgUUID(params.ClientID),
IfSyncVersion: params.IfSyncVersion,
})
if err != nil {
@@ -185,10 +209,11 @@ func (r *desktopAccountRepository) Patch(ctx context.Context, params PatchDeskto
return desktopAccountFromPatchRow(row)
}
func (r *desktopAccountRepository) Tombstone(ctx context.Context, desktopID uuid.UUID, workspaceID, ifSyncVersion int64) (*DesktopAccount, error) {
func (r *desktopAccountRepository) Tombstone(ctx context.Context, desktopID uuid.UUID, workspaceID int64, clientID uuid.UUID, ifSyncVersion int64) (*DesktopAccount, error) {
row, err := r.q.TombstoneDesktopAccount(ctx, generated.TombstoneDesktopAccountParams{
DesktopID: pgUUID(desktopID),
WorkspaceID: workspaceID,
ClientID: pgUUID(clientID),
IfSyncVersion: ifSyncVersion,
})
if err != nil {
@@ -219,6 +244,29 @@ func (r *desktopAccountRepository) ClearDeleteRequested(ctx context.Context, des
return desktopAccountFromClearDeleteRow(row)
}
func desktopAccountFromClientListRow(row generated.ListDesktopAccountsByClientRow) (*DesktopAccount, error) {
return buildDesktopAccount(
row.DesktopID,
row.TenantID,
row.WorkspaceID,
row.UserID,
row.ClientID,
row.PlatformID,
row.PlatformUid,
row.AccountFingerprint,
row.DisplayName,
row.AvatarUrl,
row.Health,
row.VerifiedAt,
row.Tags,
row.SyncVersion,
row.DeletedAt,
row.DeleteRequestedAt,
row.CreatedAt,
row.UpdatedAt,
)
}
func desktopAccountFromListRow(row generated.ListDesktopAccountsByUserRow) (*DesktopAccount, error) {
return buildDesktopAccount(
row.DesktopID,