feat(desktop): implement workspace foundation + desktop-client skeleton
Plan 0 (workspaces): add workspaces + workspace_memberships schema, extend JWT/Actor/claims with primary_workspace_id, seed default workspace per tenant, thread workspace_id through tenant monitoring quota. Plan A (desktop skeleton): new Electron app (apps/desktop-client) with main/ preload/renderer, shared Vue component package (packages/ui-shared), and server surface — desktop client registration + token rotation + heartbeat, SSE task event stream, desktop accounts/tasks/content handlers, publish job endpoint, and supporting repositories, services, sqlc queries, and migrations. Hard cutover per plan: remove browser-extension monitoring callback endpoints, stub legacy media API in admin-web, and delete monitoring_callback_handler.go.
This commit is contained in:
@@ -0,0 +1,459 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/tenant/repository/generated"
|
||||
)
|
||||
|
||||
type DesktopAccount struct {
|
||||
DesktopID uuid.UUID
|
||||
TenantID int64
|
||||
WorkspaceID int64
|
||||
UserID int64
|
||||
ClientID *uuid.UUID
|
||||
Platform string
|
||||
PlatformUID string
|
||||
AccountFingerprint *string
|
||||
DisplayName string
|
||||
Health string
|
||||
VerifiedAt *time.Time
|
||||
Tags []string
|
||||
SyncVersion int64
|
||||
DeletedAt *time.Time
|
||||
DeleteRequestedAt *time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type UpsertDesktopAccountParams struct {
|
||||
DesktopID uuid.UUID
|
||||
TenantID int64
|
||||
WorkspaceID int64
|
||||
UserID int64
|
||||
ClientID uuid.UUID
|
||||
Platform string
|
||||
PlatformUID string
|
||||
AccountFingerprint *string
|
||||
DisplayName string
|
||||
Health string
|
||||
VerifiedAt *time.Time
|
||||
Tags []string
|
||||
IfSyncVersion *int64
|
||||
}
|
||||
|
||||
type PatchDesktopAccountParams struct {
|
||||
DesktopID uuid.UUID
|
||||
WorkspaceID int64
|
||||
DisplayName *string
|
||||
Health *string
|
||||
VerifiedAt *time.Time
|
||||
Tags *[]string
|
||||
IfSyncVersion int64
|
||||
}
|
||||
|
||||
type DesktopAccountRepository interface {
|
||||
ListByClient(ctx context.Context, workspaceID int64, clientID uuid.UUID) ([]DesktopAccount, error)
|
||||
GetByDesktopID(ctx context.Context, desktopID uuid.UUID, workspaceID int64) (*DesktopAccount, error)
|
||||
GetByIdentity(ctx context.Context, workspaceID int64, 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)
|
||||
MarkDeleteRequested(ctx context.Context, desktopID uuid.UUID, workspaceID int64) (*DesktopAccount, error)
|
||||
ClearDeleteRequested(ctx context.Context, desktopID uuid.UUID, workspaceID int64) (*DesktopAccount, error)
|
||||
}
|
||||
|
||||
type desktopAccountRepository struct {
|
||||
q generated.Querier
|
||||
}
|
||||
|
||||
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 := desktopAccountFromListRow(row)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, *item)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *desktopAccountRepository) GetByDesktopID(ctx context.Context, desktopID uuid.UUID, workspaceID int64) (*DesktopAccount, error) {
|
||||
row, err := r.q.GetDesktopAccountByDesktopID(ctx, generated.GetDesktopAccountByDesktopIDParams{
|
||||
DesktopID: pgUUID(desktopID),
|
||||
WorkspaceID: workspaceID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return desktopAccountFromGetRow(row)
|
||||
}
|
||||
|
||||
func (r *desktopAccountRepository) GetByIdentity(ctx context.Context, workspaceID int64, platform, platformUID string) (*DesktopAccount, error) {
|
||||
row, err := r.q.GetDesktopAccountByIdentity(ctx, generated.GetDesktopAccountByIdentityParams{
|
||||
WorkspaceID: workspaceID,
|
||||
PlatformID: platform,
|
||||
PlatformUid: platformUID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return desktopAccountFromIdentityRow(row)
|
||||
}
|
||||
|
||||
func (r *desktopAccountRepository) Upsert(ctx context.Context, params UpsertDesktopAccountParams) (*DesktopAccount, error) {
|
||||
tagsJSON, err := marshalJSON(params.Tags)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal tags: %w", err)
|
||||
}
|
||||
|
||||
var metadataJSON []byte
|
||||
if len(tagsJSON) > 0 {
|
||||
metadataJSON, err = marshalJSON(map[string]any{"tags": params.Tags})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal metadata: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
row, err := r.q.UpsertDesktopAccount(ctx, generated.UpsertDesktopAccountParams{
|
||||
DesktopID: pgUUID(params.DesktopID),
|
||||
TenantID: params.TenantID,
|
||||
WorkspaceID: params.WorkspaceID,
|
||||
UserID: params.UserID,
|
||||
ClientID: pgUUID(params.ClientID),
|
||||
PlatformID: params.Platform,
|
||||
PlatformUid: params.PlatformUID,
|
||||
DisplayName: params.DisplayName,
|
||||
LegacyStatus: legacyStatusFromHealth(params.Health),
|
||||
MetadataJson: metadataJSON,
|
||||
VerifiedAt: pgTimestamp(params.VerifiedAt),
|
||||
AccountFingerprint: pgText(params.AccountFingerprint),
|
||||
Health: params.Health,
|
||||
Tags: tagsJSON,
|
||||
IfSyncVersion: pgInt8(params.IfSyncVersion),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return desktopAccountFromUpsertRow(row)
|
||||
}
|
||||
|
||||
func (r *desktopAccountRepository) Patch(ctx context.Context, params PatchDesktopAccountParams) (*DesktopAccount, error) {
|
||||
var tagsJSON []byte
|
||||
var err error
|
||||
if params.Tags != nil {
|
||||
tagsJSON, err = marshalJSON(*params.Tags)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal tags: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
row, err := r.q.PatchDesktopAccount(ctx, generated.PatchDesktopAccountParams{
|
||||
DisplayName: pgText(params.DisplayName),
|
||||
Health: pgText(params.Health),
|
||||
LegacyStatus: pgText(legacyStatusPtrFromHealth(params.Health)),
|
||||
VerifiedAt: pgTimestamp(params.VerifiedAt),
|
||||
Tags: tagsJSON,
|
||||
DesktopID: pgUUID(params.DesktopID),
|
||||
WorkspaceID: params.WorkspaceID,
|
||||
IfSyncVersion: params.IfSyncVersion,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return desktopAccountFromPatchRow(row)
|
||||
}
|
||||
|
||||
func (r *desktopAccountRepository) Tombstone(ctx context.Context, desktopID uuid.UUID, workspaceID, ifSyncVersion int64) (*DesktopAccount, error) {
|
||||
row, err := r.q.TombstoneDesktopAccount(ctx, generated.TombstoneDesktopAccountParams{
|
||||
DesktopID: pgUUID(desktopID),
|
||||
WorkspaceID: workspaceID,
|
||||
IfSyncVersion: ifSyncVersion,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return desktopAccountFromTombstoneRow(row)
|
||||
}
|
||||
|
||||
func (r *desktopAccountRepository) MarkDeleteRequested(ctx context.Context, desktopID uuid.UUID, workspaceID int64) (*DesktopAccount, error) {
|
||||
row, err := r.q.MarkDesktopAccountDeleteRequested(ctx, generated.MarkDesktopAccountDeleteRequestedParams{
|
||||
DesktopID: pgUUID(desktopID),
|
||||
WorkspaceID: workspaceID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return desktopAccountFromMarkDeleteRow(row)
|
||||
}
|
||||
|
||||
func (r *desktopAccountRepository) ClearDeleteRequested(ctx context.Context, desktopID uuid.UUID, workspaceID int64) (*DesktopAccount, error) {
|
||||
row, err := r.q.ClearDesktopAccountDeleteRequested(ctx, generated.ClearDesktopAccountDeleteRequestedParams{
|
||||
DesktopID: pgUUID(desktopID),
|
||||
WorkspaceID: workspaceID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return desktopAccountFromClearDeleteRow(row)
|
||||
}
|
||||
|
||||
func desktopAccountFromListRow(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.Health,
|
||||
row.VerifiedAt,
|
||||
row.Tags,
|
||||
row.SyncVersion,
|
||||
row.DeletedAt,
|
||||
row.DeleteRequestedAt,
|
||||
row.CreatedAt,
|
||||
row.UpdatedAt,
|
||||
)
|
||||
}
|
||||
|
||||
func desktopAccountFromGetRow(row generated.GetDesktopAccountByDesktopIDRow) (*DesktopAccount, error) {
|
||||
return buildDesktopAccount(
|
||||
row.DesktopID,
|
||||
row.TenantID,
|
||||
row.WorkspaceID,
|
||||
row.UserID,
|
||||
row.ClientID,
|
||||
row.PlatformID,
|
||||
row.PlatformUid,
|
||||
row.AccountFingerprint,
|
||||
row.DisplayName,
|
||||
row.Health,
|
||||
row.VerifiedAt,
|
||||
row.Tags,
|
||||
row.SyncVersion,
|
||||
row.DeletedAt,
|
||||
row.DeleteRequestedAt,
|
||||
row.CreatedAt,
|
||||
row.UpdatedAt,
|
||||
)
|
||||
}
|
||||
|
||||
func desktopAccountFromIdentityRow(row generated.GetDesktopAccountByIdentityRow) (*DesktopAccount, error) {
|
||||
return buildDesktopAccount(
|
||||
row.DesktopID,
|
||||
row.TenantID,
|
||||
row.WorkspaceID,
|
||||
row.UserID,
|
||||
row.ClientID,
|
||||
row.PlatformID,
|
||||
row.PlatformUid,
|
||||
row.AccountFingerprint,
|
||||
row.DisplayName,
|
||||
row.Health,
|
||||
row.VerifiedAt,
|
||||
row.Tags,
|
||||
row.SyncVersion,
|
||||
row.DeletedAt,
|
||||
row.DeleteRequestedAt,
|
||||
row.CreatedAt,
|
||||
row.UpdatedAt,
|
||||
)
|
||||
}
|
||||
|
||||
func desktopAccountFromUpsertRow(row generated.UpsertDesktopAccountRow) (*DesktopAccount, error) {
|
||||
return buildDesktopAccount(
|
||||
row.DesktopID,
|
||||
row.TenantID,
|
||||
row.WorkspaceID,
|
||||
row.UserID,
|
||||
row.ClientID,
|
||||
row.PlatformID,
|
||||
row.PlatformUid,
|
||||
row.AccountFingerprint,
|
||||
row.DisplayName,
|
||||
row.Health,
|
||||
row.VerifiedAt,
|
||||
row.Tags,
|
||||
row.SyncVersion,
|
||||
row.DeletedAt,
|
||||
row.DeleteRequestedAt,
|
||||
row.CreatedAt,
|
||||
row.UpdatedAt,
|
||||
)
|
||||
}
|
||||
|
||||
func desktopAccountFromPatchRow(row generated.PatchDesktopAccountRow) (*DesktopAccount, error) {
|
||||
return buildDesktopAccount(
|
||||
row.DesktopID,
|
||||
row.TenantID,
|
||||
row.WorkspaceID,
|
||||
row.UserID,
|
||||
row.ClientID,
|
||||
row.PlatformID,
|
||||
row.PlatformUid,
|
||||
row.AccountFingerprint,
|
||||
row.DisplayName,
|
||||
row.Health,
|
||||
row.VerifiedAt,
|
||||
row.Tags,
|
||||
row.SyncVersion,
|
||||
row.DeletedAt,
|
||||
row.DeleteRequestedAt,
|
||||
row.CreatedAt,
|
||||
row.UpdatedAt,
|
||||
)
|
||||
}
|
||||
|
||||
func desktopAccountFromTombstoneRow(row generated.TombstoneDesktopAccountRow) (*DesktopAccount, error) {
|
||||
return buildDesktopAccount(
|
||||
row.DesktopID,
|
||||
row.TenantID,
|
||||
row.WorkspaceID,
|
||||
row.UserID,
|
||||
row.ClientID,
|
||||
row.PlatformID,
|
||||
row.PlatformUid,
|
||||
row.AccountFingerprint,
|
||||
row.DisplayName,
|
||||
row.Health,
|
||||
row.VerifiedAt,
|
||||
row.Tags,
|
||||
row.SyncVersion,
|
||||
row.DeletedAt,
|
||||
row.DeleteRequestedAt,
|
||||
row.CreatedAt,
|
||||
row.UpdatedAt,
|
||||
)
|
||||
}
|
||||
|
||||
func desktopAccountFromMarkDeleteRow(row generated.MarkDesktopAccountDeleteRequestedRow) (*DesktopAccount, error) {
|
||||
return buildDesktopAccount(
|
||||
row.DesktopID,
|
||||
row.TenantID,
|
||||
row.WorkspaceID,
|
||||
row.UserID,
|
||||
row.ClientID,
|
||||
row.PlatformID,
|
||||
row.PlatformUid,
|
||||
row.AccountFingerprint,
|
||||
row.DisplayName,
|
||||
row.Health,
|
||||
row.VerifiedAt,
|
||||
row.Tags,
|
||||
row.SyncVersion,
|
||||
row.DeletedAt,
|
||||
row.DeleteRequestedAt,
|
||||
row.CreatedAt,
|
||||
row.UpdatedAt,
|
||||
)
|
||||
}
|
||||
|
||||
func desktopAccountFromClearDeleteRow(row generated.ClearDesktopAccountDeleteRequestedRow) (*DesktopAccount, error) {
|
||||
return buildDesktopAccount(
|
||||
row.DesktopID,
|
||||
row.TenantID,
|
||||
row.WorkspaceID,
|
||||
row.UserID,
|
||||
row.ClientID,
|
||||
row.PlatformID,
|
||||
row.PlatformUid,
|
||||
row.AccountFingerprint,
|
||||
row.DisplayName,
|
||||
row.Health,
|
||||
row.VerifiedAt,
|
||||
row.Tags,
|
||||
row.SyncVersion,
|
||||
row.DeletedAt,
|
||||
row.DeleteRequestedAt,
|
||||
row.CreatedAt,
|
||||
row.UpdatedAt,
|
||||
)
|
||||
}
|
||||
|
||||
func buildDesktopAccount(
|
||||
desktopID pgtype.UUID,
|
||||
tenantID int64,
|
||||
workspaceID int64,
|
||||
userID int64,
|
||||
clientID pgtype.UUID,
|
||||
platform string,
|
||||
platformUID string,
|
||||
accountFingerprint pgtype.Text,
|
||||
displayName string,
|
||||
health string,
|
||||
verifiedAt pgtype.Timestamptz,
|
||||
tags []byte,
|
||||
syncVersion int64,
|
||||
deletedAt pgtype.Timestamptz,
|
||||
deleteRequestedAt pgtype.Timestamptz,
|
||||
createdAt pgtype.Timestamptz,
|
||||
updatedAt pgtype.Timestamptz,
|
||||
) (*DesktopAccount, error) {
|
||||
tagList, err := unmarshalStringSlice(tags)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unmarshal desktop account tags: %w", err)
|
||||
}
|
||||
|
||||
return &DesktopAccount{
|
||||
DesktopID: uuidFromPG(desktopID),
|
||||
TenantID: tenantID,
|
||||
WorkspaceID: workspaceID,
|
||||
UserID: userID,
|
||||
ClientID: nullableUUID(clientID),
|
||||
Platform: platform,
|
||||
PlatformUID: platformUID,
|
||||
AccountFingerprint: nullableText(accountFingerprint),
|
||||
DisplayName: displayName,
|
||||
Health: health,
|
||||
VerifiedAt: optionalTime(verifiedAt),
|
||||
Tags: tagList,
|
||||
SyncVersion: syncVersion,
|
||||
DeletedAt: optionalTime(deletedAt),
|
||||
DeleteRequestedAt: optionalTime(deleteRequestedAt),
|
||||
CreatedAt: timeFromTimestamp(createdAt),
|
||||
UpdatedAt: timeFromTimestamp(updatedAt),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func legacyStatusFromHealth(health string) string {
|
||||
switch health {
|
||||
case "live":
|
||||
return "active"
|
||||
case "captcha":
|
||||
return "captcha"
|
||||
case "risk":
|
||||
return "risk"
|
||||
default:
|
||||
return "expired"
|
||||
}
|
||||
}
|
||||
|
||||
func legacyStatusPtrFromHealth(health *string) *string {
|
||||
if health == nil {
|
||||
return nil
|
||||
}
|
||||
value := legacyStatusFromHealth(*health)
|
||||
return &value
|
||||
}
|
||||
Reference in New Issue
Block a user