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,174 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/tenant/repository/generated"
|
||||
)
|
||||
|
||||
type DesktopClient struct {
|
||||
ID uuid.UUID
|
||||
TenantID int64
|
||||
WorkspaceID int64
|
||||
UserID int64
|
||||
TokenHash []byte
|
||||
DeviceName *string
|
||||
OS *string
|
||||
CPUArch *string
|
||||
ClientVersion *string
|
||||
Channel *string
|
||||
CreatedAt time.Time
|
||||
LastSeenAt *time.Time
|
||||
LastRotatedAt *time.Time
|
||||
RevokedAt *time.Time
|
||||
}
|
||||
|
||||
type RegisterDesktopClientParams struct {
|
||||
ID uuid.UUID
|
||||
TenantID int64
|
||||
WorkspaceID int64
|
||||
UserID int64
|
||||
TokenHash []byte
|
||||
DeviceName *string
|
||||
OS *string
|
||||
CPUArch *string
|
||||
ClientVersion *string
|
||||
Channel *string
|
||||
}
|
||||
|
||||
type HeartbeatDesktopClientParams struct {
|
||||
ID uuid.UUID
|
||||
WorkspaceID int64
|
||||
DeviceName *string
|
||||
OS *string
|
||||
CPUArch *string
|
||||
ClientVersion *string
|
||||
Channel *string
|
||||
}
|
||||
|
||||
type RotateDesktopClientTokenParams struct {
|
||||
ID uuid.UUID
|
||||
WorkspaceID int64
|
||||
TokenHash []byte
|
||||
}
|
||||
|
||||
type DesktopClientRepository interface {
|
||||
Register(ctx context.Context, params RegisterDesktopClientParams) (*DesktopClient, error)
|
||||
GetByTokenHash(ctx context.Context, tokenHash []byte) (*DesktopClient, error)
|
||||
RotateToken(ctx context.Context, params RotateDesktopClientTokenParams) (*DesktopClient, error)
|
||||
Heartbeat(ctx context.Context, params HeartbeatDesktopClientParams) (*DesktopClient, error)
|
||||
Revoke(ctx context.Context, id uuid.UUID, workspaceID int64) (*DesktopClient, error)
|
||||
ListByWorkspace(ctx context.Context, workspaceID int64) ([]DesktopClient, error)
|
||||
}
|
||||
|
||||
type desktopClientRepository struct {
|
||||
q generated.Querier
|
||||
}
|
||||
|
||||
func NewDesktopClientRepository(db generated.DBTX) DesktopClientRepository {
|
||||
return &desktopClientRepository{q: newQuerier(db)}
|
||||
}
|
||||
|
||||
func (r *desktopClientRepository) Register(ctx context.Context, params RegisterDesktopClientParams) (*DesktopClient, error) {
|
||||
row, err := r.q.RegisterDesktopClient(ctx, generated.RegisterDesktopClientParams{
|
||||
ID: pgUUID(params.ID),
|
||||
TenantID: params.TenantID,
|
||||
WorkspaceID: params.WorkspaceID,
|
||||
UserID: params.UserID,
|
||||
TokenHash: params.TokenHash,
|
||||
DeviceName: pgText(params.DeviceName),
|
||||
Os: pgText(params.OS),
|
||||
CpuArch: pgText(params.CPUArch),
|
||||
ClientVersion: pgText(params.ClientVersion),
|
||||
Channel: pgText(params.Channel),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return desktopClientFromGenerated(row), nil
|
||||
}
|
||||
|
||||
func (r *desktopClientRepository) GetByTokenHash(ctx context.Context, tokenHash []byte) (*DesktopClient, error) {
|
||||
row, err := r.q.GetDesktopClientByTokenHash(ctx, tokenHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return desktopClientFromGenerated(row), nil
|
||||
}
|
||||
|
||||
func (r *desktopClientRepository) RotateToken(ctx context.Context, params RotateDesktopClientTokenParams) (*DesktopClient, error) {
|
||||
row, err := r.q.RotateDesktopClientToken(ctx, generated.RotateDesktopClientTokenParams{
|
||||
ID: pgUUID(params.ID),
|
||||
WorkspaceID: params.WorkspaceID,
|
||||
TokenHash: params.TokenHash,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return desktopClientFromGenerated(row), nil
|
||||
}
|
||||
|
||||
func (r *desktopClientRepository) Heartbeat(ctx context.Context, params HeartbeatDesktopClientParams) (*DesktopClient, error) {
|
||||
row, err := r.q.HeartbeatDesktopClient(ctx, generated.HeartbeatDesktopClientParams{
|
||||
ID: pgUUID(params.ID),
|
||||
WorkspaceID: params.WorkspaceID,
|
||||
DeviceName: pgText(params.DeviceName),
|
||||
Os: pgText(params.OS),
|
||||
CpuArch: pgText(params.CPUArch),
|
||||
ClientVersion: pgText(params.ClientVersion),
|
||||
Channel: pgText(params.Channel),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return desktopClientFromGenerated(row), nil
|
||||
}
|
||||
|
||||
func (r *desktopClientRepository) Revoke(ctx context.Context, id uuid.UUID, workspaceID int64) (*DesktopClient, error) {
|
||||
row, err := r.q.RevokeDesktopClient(ctx, generated.RevokeDesktopClientParams{
|
||||
ID: pgUUID(id),
|
||||
WorkspaceID: workspaceID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return desktopClientFromGenerated(row), nil
|
||||
}
|
||||
|
||||
func (r *desktopClientRepository) ListByWorkspace(ctx context.Context, workspaceID int64) ([]DesktopClient, error) {
|
||||
rows, err := r.q.ListDesktopClientsByWorkspace(ctx, workspaceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items := make([]DesktopClient, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
item := desktopClientFromGenerated(row)
|
||||
if item != nil {
|
||||
items = append(items, *item)
|
||||
}
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func desktopClientFromGenerated(row generated.DesktopClient) *DesktopClient {
|
||||
return &DesktopClient{
|
||||
ID: uuidFromPG(row.ID),
|
||||
TenantID: row.TenantID,
|
||||
WorkspaceID: row.WorkspaceID,
|
||||
UserID: row.UserID,
|
||||
TokenHash: row.TokenHash,
|
||||
DeviceName: nullableText(row.DeviceName),
|
||||
OS: nullableText(row.Os),
|
||||
CPUArch: nullableText(row.CpuArch),
|
||||
ClientVersion: nullableText(row.ClientVersion),
|
||||
Channel: nullableText(row.Channel),
|
||||
CreatedAt: timeFromTimestamp(row.CreatedAt),
|
||||
LastSeenAt: optionalTime(row.LastSeenAt),
|
||||
LastRotatedAt: optionalTime(row.LastRotatedAt),
|
||||
RevokedAt: optionalTime(row.RevokedAt),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user