Files
geo/server/internal/tenant/app/desktop_account_service.go
T
root b16e9f0bd1 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.
2026-04-19 14:18:20 +08:00

206 lines
7.6 KiB
Go

package app
import (
"context"
"errors"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/geo-platform/tenant-api/internal/shared/auth"
"github.com/geo-platform/tenant-api/internal/shared/response"
"github.com/geo-platform/tenant-api/internal/tenant/repository"
)
type DesktopAccountService struct {
repo repository.DesktopAccountRepository
}
func NewDesktopAccountService(repo repository.DesktopAccountRepository) *DesktopAccountService {
return &DesktopAccountService{repo: repo}
}
type DesktopAccountView struct {
ID string `json:"id"`
Platform string `json:"platform"`
PlatformUID string `json:"platform_uid"`
DisplayName string `json:"display_name"`
Health string `json:"health"`
ClientID *string `json:"client_id"`
AccountFingerprint *string `json:"account_fingerprint"`
VerifiedAt *time.Time `json:"verified_at"`
Tags []string `json:"tags"`
SyncVersion int64 `json:"sync_version"`
DeletedAt *time.Time `json:"deleted_at"`
DeleteRequestedAt *time.Time `json:"delete_requested_at"`
}
type UpsertDesktopAccountRequest struct {
Platform string `json:"platform" binding:"required"`
PlatformUID string `json:"platform_uid" binding:"required"`
AccountFingerprint *string `json:"account_fingerprint"`
DisplayName string `json:"display_name" binding:"required"`
Health string `json:"health" binding:"required,oneof=live expired captcha risk"`
VerifiedAt *time.Time `json:"verified_at"`
Tags []string `json:"tags"`
IfSyncVersion *int64 `json:"if_sync_version"`
}
type PatchDesktopAccountRequest struct {
DisplayName *string `json:"display_name"`
Health *string `json:"health" binding:"omitempty,oneof=live expired captcha risk"`
VerifiedAt *time.Time `json:"verified_at"`
Tags *[]string `json:"tags"`
IfSyncVersion int64 `json:"if_sync_version" binding:"required"`
}
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.ListByClient(ctx, client.WorkspaceID, client.ID)
if err != nil {
return nil, response.ErrInternal(50081, "desktop_accounts_query_failed", "failed to list desktop accounts")
}
items := make([]DesktopAccountView, 0, len(rows))
for _, item := range rows {
items = append(items, buildDesktopAccountView(&item))
}
return items, nil
}
func (s *DesktopAccountService) Upsert(ctx context.Context, client *repository.DesktopClient, req UpsertDesktopAccountRequest) (*DesktopAccountView, error) {
if client == nil {
return nil, response.ErrUnauthorized(40108, "desktop_client_missing", "desktop client context is required")
}
if req.IfSyncVersion != nil {
existing, err := s.repo.GetByIdentity(ctx, client.WorkspaceID, 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")
}
if existing == nil || errors.Is(err, pgx.ErrNoRows) {
return nil, response.ErrConflict(40981, "desktop_account_sync_version_required_existing", "if_sync_version is only valid when account already exists")
}
}
account, err := s.repo.Upsert(ctx, repository.UpsertDesktopAccountParams{
DesktopID: uuid.New(),
TenantID: client.TenantID,
WorkspaceID: client.WorkspaceID,
UserID: client.UserID,
ClientID: client.ID,
Platform: req.Platform,
PlatformUID: req.PlatformUID,
AccountFingerprint: req.AccountFingerprint,
DisplayName: req.DisplayName,
Health: req.Health,
VerifiedAt: req.VerifiedAt,
Tags: req.Tags,
IfSyncVersion: req.IfSyncVersion,
})
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, response.ErrConflict(40982, "desktop_account_sync_conflict", "desktop account sync version conflict")
}
return nil, response.ErrInternal(50083, "desktop_account_upsert_failed", "failed to upsert desktop account")
}
view := buildDesktopAccountView(account)
return &view, nil
}
func (s *DesktopAccountService) Patch(ctx context.Context, client *repository.DesktopClient, desktopID uuid.UUID, req PatchDesktopAccountRequest) (*DesktopAccountView, error) {
if client == nil {
return nil, response.ErrUnauthorized(40108, "desktop_client_missing", "desktop client context is required")
}
account, err := s.repo.Patch(ctx, repository.PatchDesktopAccountParams{
DesktopID: desktopID,
WorkspaceID: client.WorkspaceID,
DisplayName: req.DisplayName,
Health: req.Health,
VerifiedAt: req.VerifiedAt,
Tags: req.Tags,
IfSyncVersion: req.IfSyncVersion,
})
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, response.ErrConflict(40982, "desktop_account_sync_conflict", "desktop account sync version conflict")
}
return nil, response.ErrInternal(50084, "desktop_account_patch_failed", "failed to patch desktop account")
}
view := buildDesktopAccountView(account)
return &view, nil
}
func (s *DesktopAccountService) Tombstone(ctx context.Context, client *repository.DesktopClient, desktopID uuid.UUID, ifSyncVersion int64) (*DesktopAccountView, error) {
if client == nil {
return nil, response.ErrUnauthorized(40108, "desktop_client_missing", "desktop client context is required")
}
account, err := s.repo.Tombstone(ctx, desktopID, client.WorkspaceID, ifSyncVersion)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, response.ErrConflict(40982, "desktop_account_sync_conflict", "desktop account sync version conflict")
}
return nil, response.ErrInternal(50085, "desktop_account_delete_failed", "failed to delete desktop account")
}
view := buildDesktopAccountView(account)
return &view, nil
}
func (s *DesktopAccountService) RequestDelete(ctx context.Context, actor auth.Actor, desktopID uuid.UUID, undo bool) (*DesktopAccountView, error) {
if actor.PrimaryWorkspaceID == 0 {
return nil, response.ErrUnauthorized(40132, "workspace_scope_missing", "workspace context is required")
}
var (
account *repository.DesktopAccount
err error
)
if undo {
account, err = s.repo.ClearDeleteRequested(ctx, desktopID, actor.PrimaryWorkspaceID)
} else {
account, err = s.repo.MarkDeleteRequested(ctx, desktopID, actor.PrimaryWorkspaceID)
}
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, response.ErrNotFound(40481, "desktop_account_not_found", "desktop account not found")
}
return nil, response.ErrInternal(50086, "desktop_account_request_delete_failed", "failed to update desktop account delete request")
}
view := buildDesktopAccountView(account)
return &view, nil
}
func buildDesktopAccountView(account *repository.DesktopAccount) DesktopAccountView {
var clientID *string
if account.ClientID != nil {
value := account.ClientID.String()
clientID = &value
}
return DesktopAccountView{
ID: account.DesktopID.String(),
Platform: account.Platform,
PlatformUID: account.PlatformUID,
DisplayName: account.DisplayName,
Health: account.Health,
ClientID: clientID,
AccountFingerprint: account.AccountFingerprint,
VerifiedAt: account.VerifiedAt,
Tags: account.Tags,
SyncVersion: account.SyncVersion,
DeletedAt: account.DeletedAt,
DeleteRequestedAt: account.DeleteRequestedAt,
}
}