feat(desktop): report current user online clients
This commit is contained in:
@@ -203,6 +203,7 @@ export interface RuntimeControllerSnapshot {
|
||||
lastPullStatus: 'idle' | 'success' | 'failed'
|
||||
lastAccountsSyncAt: number
|
||||
lastServerTime: string | null
|
||||
onlineClientCount: number | null
|
||||
lastError: string | null
|
||||
}
|
||||
|
||||
@@ -251,6 +252,7 @@ interface RuntimeState {
|
||||
lastPullStatus: 'idle' | 'success' | 'failed'
|
||||
lastAccountsSyncAt: number
|
||||
lastServerTime: string | null
|
||||
onlineClientCount: number | null
|
||||
lastError: string | null
|
||||
activitySeq: number
|
||||
}
|
||||
@@ -280,6 +282,7 @@ const state: RuntimeState = {
|
||||
lastPullStatus: 'idle',
|
||||
lastAccountsSyncAt: 0,
|
||||
lastServerTime: null,
|
||||
onlineClientCount: null,
|
||||
lastError: null,
|
||||
activitySeq: 0,
|
||||
}
|
||||
@@ -532,6 +535,7 @@ export function getRuntimeControllerSnapshot(): RuntimeControllerSnapshot {
|
||||
lastPullStatus: state.lastPullStatus,
|
||||
lastAccountsSyncAt: state.lastAccountsSyncAt,
|
||||
lastServerTime: state.lastServerTime,
|
||||
onlineClientCount: state.onlineClientCount,
|
||||
lastError: state.lastError,
|
||||
}
|
||||
}
|
||||
@@ -627,6 +631,7 @@ function clearRuntimeState(): void {
|
||||
state.lastPullStatus = 'idle'
|
||||
state.lastAccountsSyncAt = 0
|
||||
state.lastServerTime = null
|
||||
state.onlineClientCount = null
|
||||
state.lastError = null
|
||||
state.publishFallbackBackoffUntil = 0
|
||||
setSchedulerQueueDepth(localQueueDepth())
|
||||
@@ -977,6 +982,8 @@ async function sendHeartbeat(source: 'startup' | 'timer'): Promise<void> {
|
||||
state.lastHeartbeatAt = Date.now()
|
||||
state.lastHeartbeatStatus = 'success'
|
||||
state.lastServerTime = response.server_time
|
||||
state.onlineClientCount =
|
||||
response.current_user_online_client_count ?? response.online_client_count ?? null
|
||||
state.lastError = null
|
||||
|
||||
noteTransportHeartbeat(true)
|
||||
|
||||
@@ -144,6 +144,10 @@ function createLiveRuntimeSnapshot(controller: RuntimeControllerSnapshot) {
|
||||
channel: controller.client?.channel ?? 'dev',
|
||||
},
|
||||
] as const
|
||||
const onlineClientCount =
|
||||
typeof controller.onlineClientCount === 'number'
|
||||
? Math.max(0, controller.onlineClientCount)
|
||||
: clients.filter((item) => item.status === 'online').length
|
||||
|
||||
const accounts = controller.accounts.map((account) =>
|
||||
createRuntimeAccountView(controller, account, {
|
||||
@@ -185,7 +189,7 @@ function createLiveRuntimeSnapshot(controller: RuntimeControllerSnapshot) {
|
||||
lastFullSyncAt: controller.lastAccountsSyncAt || now,
|
||||
},
|
||||
summary: {
|
||||
onlineClients: clients.filter((item) => item.status === 'online').length,
|
||||
onlineClients: onlineClientCount,
|
||||
accountsBound: accounts.length,
|
||||
queuedTasks: tasks.filter((item) => item.status === 'queued').length,
|
||||
issuesOpen:
|
||||
|
||||
@@ -53,7 +53,7 @@ const subsystemEntries = computed(() =>
|
||||
})),
|
||||
)
|
||||
|
||||
const clientCount = computed(() => snapshot.value?.clients.length ?? 0)
|
||||
const clientCount = computed(() => snapshot.value?.summary.onlineClients ?? 0)
|
||||
const appVersion = computed(() => snapshot.value?.app.version ?? '0.1.0')
|
||||
const hasServerSettingChanged = computed(
|
||||
() => normalizeServerURLInput(serverBaseURL.value) !== apiBaseURL.value,
|
||||
|
||||
@@ -115,6 +115,8 @@ export interface DesktopClientHeartbeatRequest {
|
||||
export interface DesktopClientHeartbeatResponse {
|
||||
client: DesktopClientInfo
|
||||
server_time: string
|
||||
online_client_count?: number
|
||||
current_user_online_client_count?: number
|
||||
}
|
||||
|
||||
export interface DesktopClientRotateResponse {
|
||||
|
||||
@@ -84,8 +84,10 @@ type RotateDesktopClientResponse struct {
|
||||
}
|
||||
|
||||
type HeartbeatDesktopClientResponse struct {
|
||||
Client DesktopClientView `json:"client"`
|
||||
ServerTime time.Time `json:"server_time"`
|
||||
Client DesktopClientView `json:"client"`
|
||||
ServerTime time.Time `json:"server_time"`
|
||||
OnlineClientCount int `json:"online_client_count"`
|
||||
CurrentUserOnlineClientCount int `json:"current_user_online_client_count"`
|
||||
}
|
||||
|
||||
type OfflineDesktopClientResponse struct {
|
||||
@@ -128,7 +130,7 @@ func (s *DesktopClientService) Register(ctx context.Context, actor auth.Actor, r
|
||||
return nil, response.ErrInternal(50072, "desktop_client_register_failed", "failed to persist desktop client")
|
||||
}
|
||||
|
||||
markDesktopClientPresent(ctx, s.redis, client.ID)
|
||||
markDesktopClientPresentForUser(ctx, s.redis, client.ID, client.TenantID, client.WorkspaceID, client.UserID)
|
||||
|
||||
return &RegisterDesktopClientResponse{
|
||||
ClientID: client.ID.String(),
|
||||
@@ -160,7 +162,7 @@ func (s *DesktopClientService) Rotate(ctx context.Context, client *repository.De
|
||||
return nil, response.ErrInternal(50074, "desktop_client_rotate_failed", "failed to rotate desktop client token")
|
||||
}
|
||||
|
||||
markDesktopClientPresent(ctx, s.redis, updated.ID)
|
||||
markDesktopClientPresentForUser(ctx, s.redis, updated.ID, updated.TenantID, updated.WorkspaceID, updated.UserID)
|
||||
|
||||
return &RotateDesktopClientResponse{
|
||||
ClientToken: rawToken,
|
||||
@@ -190,8 +192,12 @@ func (s *DesktopClientService) Heartbeat(ctx context.Context, client *repository
|
||||
return nil, response.ErrInternal(50075, "desktop_client_heartbeat_failed", "failed to persist desktop client heartbeat")
|
||||
}
|
||||
|
||||
markDesktopClientPresent(ctx, s.redis, updated.ID)
|
||||
markDesktopClientPresentForUser(ctx, s.redis, updated.ID, updated.TenantID, updated.WorkspaceID, updated.UserID)
|
||||
markDesktopAccountsPresent(ctx, s.redis, updated.ID, parseDesktopAccountIDs(req.AccountIDs))
|
||||
onlineClientCount, err := s.currentUserOnlineClientCount(ctx, updated)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if req.Startup && s.taskSvc != nil {
|
||||
if err := s.taskSvc.recoverClientTasksOnStartup(ctx, updated); err != nil {
|
||||
@@ -200,8 +206,10 @@ func (s *DesktopClientService) Heartbeat(ctx context.Context, client *repository
|
||||
}
|
||||
|
||||
return &HeartbeatDesktopClientResponse{
|
||||
Client: buildDesktopClientView(updated),
|
||||
ServerTime: time.Now().UTC(),
|
||||
Client: buildDesktopClientView(updated),
|
||||
ServerTime: time.Now().UTC(),
|
||||
OnlineClientCount: onlineClientCount,
|
||||
CurrentUserOnlineClientCount: onlineClientCount,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -218,7 +226,7 @@ func (s *DesktopClientService) Revoke(ctx context.Context, client *repository.De
|
||||
return nil, response.ErrInternal(50076, "desktop_client_revoke_failed", "failed to revoke desktop client")
|
||||
}
|
||||
|
||||
clearDesktopClientPresenceState(ctx, s.redis, updated.ID)
|
||||
clearDesktopClientPresenceStateForUser(ctx, s.redis, updated.ID, updated.TenantID, updated.WorkspaceID, updated.UserID)
|
||||
if s.taskSvc != nil {
|
||||
if err := s.taskSvc.recoverClientTasksOnDisconnect(ctx, updated); err != nil {
|
||||
return nil, err
|
||||
@@ -234,7 +242,7 @@ func (s *DesktopClientService) Offline(ctx context.Context, client *repository.D
|
||||
return nil, response.ErrUnauthorized(40108, "desktop_client_missing", "desktop client context is required")
|
||||
}
|
||||
|
||||
clearDesktopClientPresenceState(ctx, s.redis, client.ID)
|
||||
clearDesktopClientPresenceStateForUser(ctx, s.redis, client.ID, client.TenantID, client.WorkspaceID, client.UserID)
|
||||
if s.taskSvc != nil {
|
||||
if err := s.taskSvc.recoverClientTasksOnDisconnect(ctx, client); err != nil {
|
||||
return nil, err
|
||||
@@ -246,6 +254,45 @@ func (s *DesktopClientService) Offline(ctx context.Context, client *repository.D
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *DesktopClientService) currentUserOnlineClientCount(ctx context.Context, client *repository.DesktopClient) (int, error) {
|
||||
if count, ok := loadDesktopUserOnlineClientCount(ctx, s.redis, client.TenantID, client.WorkspaceID, client.UserID, time.Now().UTC()); ok {
|
||||
return count, nil
|
||||
}
|
||||
|
||||
clients, err := s.repo.ListByUser(ctx, client.TenantID, client.WorkspaceID, client.UserID)
|
||||
if err != nil {
|
||||
return 0, response.ErrInternal(50077, "desktop_client_online_count_failed", "failed to inspect online desktop clients")
|
||||
}
|
||||
|
||||
return countOnlineDesktopClients(ctx, s.redis, clients, time.Now().UTC()), nil
|
||||
}
|
||||
|
||||
func countOnlineDesktopClients(ctx context.Context, redis *goredis.Client, clients []repository.DesktopClient, now time.Time) int {
|
||||
if len(clients) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
clientIDs := make([]uuid.UUID, 0, len(clients))
|
||||
for _, client := range clients {
|
||||
if client.ID == uuid.Nil || client.RevokedAt != nil {
|
||||
continue
|
||||
}
|
||||
clientIDs = append(clientIDs, client.ID)
|
||||
}
|
||||
presence := loadDesktopClientPresence(ctx, redis, clientIDs)
|
||||
|
||||
onlineCount := 0
|
||||
for _, client := range clients {
|
||||
if client.ID == uuid.Nil || client.RevokedAt != nil {
|
||||
continue
|
||||
}
|
||||
if resolveDesktopClientOnline(&client, presence[client.ID], presence != nil, now) {
|
||||
onlineCount++
|
||||
}
|
||||
}
|
||||
return onlineCount
|
||||
}
|
||||
|
||||
func newDesktopClientToken() (string, []byte, error) {
|
||||
raw := make([]byte, 32)
|
||||
if _, err := rand.Read(raw); err != nil {
|
||||
|
||||
@@ -27,6 +27,13 @@ func desktopClientAccountsPresenceKey(clientID uuid.UUID) string {
|
||||
return "desktop:presence:client_accounts:" + clientID.String()
|
||||
}
|
||||
|
||||
func desktopUserClientsPresenceKey(tenantID, workspaceID, userID int64) string {
|
||||
return "desktop:presence:user_clients:" +
|
||||
strconv.FormatInt(tenantID, 10) + ":" +
|
||||
strconv.FormatInt(workspaceID, 10) + ":" +
|
||||
strconv.FormatInt(userID, 10)
|
||||
}
|
||||
|
||||
func markDesktopClientPresent(ctx context.Context, redis *goredis.Client, clientID uuid.UUID) {
|
||||
if redis == nil {
|
||||
return
|
||||
@@ -35,6 +42,25 @@ func markDesktopClientPresent(ctx context.Context, redis *goredis.Client, client
|
||||
_ = redis.Set(ctx, desktopClientPresenceKey(clientID), time.Now().UTC().Format(time.RFC3339), desktopClientPresenceTTL).Err()
|
||||
}
|
||||
|
||||
func markDesktopClientPresentForUser(ctx context.Context, redis *goredis.Client, clientID uuid.UUID, tenantID, workspaceID, userID int64) {
|
||||
markDesktopClientPresent(ctx, redis, clientID)
|
||||
if redis == nil || clientID == uuid.Nil || tenantID == 0 || workspaceID == 0 || userID == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
staleBefore := strconv.FormatInt(now.Add(-desktopClientPresenceTTL).UnixMilli(), 10)
|
||||
key := desktopUserClientsPresenceKey(tenantID, workspaceID, userID)
|
||||
pipe := redis.Pipeline()
|
||||
pipe.ZRemRangeByScore(ctx, key, "-inf", staleBefore)
|
||||
pipe.ZAdd(ctx, key, goredis.Z{
|
||||
Score: float64(now.UnixMilli()),
|
||||
Member: clientID.String(),
|
||||
})
|
||||
pipe.Expire(ctx, key, desktopClientPresenceTTL*4)
|
||||
_, _ = pipe.Exec(ctx)
|
||||
}
|
||||
|
||||
func clearDesktopClientPresence(ctx context.Context, redis *goredis.Client, clientID uuid.UUID) {
|
||||
if redis == nil {
|
||||
return
|
||||
@@ -43,6 +69,20 @@ func clearDesktopClientPresence(ctx context.Context, redis *goredis.Client, clie
|
||||
_ = redis.Del(ctx, desktopClientPresenceKey(clientID)).Err()
|
||||
}
|
||||
|
||||
func clearDesktopClientPresenceStateForUser(ctx context.Context, redis *goredis.Client, clientID uuid.UUID, tenantID, workspaceID, userID int64) {
|
||||
clearDesktopClientPresenceState(ctx, redis, clientID)
|
||||
if redis == nil || clientID == uuid.Nil || tenantID == 0 || workspaceID == 0 || userID == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
staleBefore := strconv.FormatInt(time.Now().UTC().Add(-desktopClientPresenceTTL).UnixMilli(), 10)
|
||||
key := desktopUserClientsPresenceKey(tenantID, workspaceID, userID)
|
||||
pipe := redis.Pipeline()
|
||||
pipe.ZRem(ctx, key, clientID.String())
|
||||
pipe.ZRemRangeByScore(ctx, key, "-inf", staleBefore)
|
||||
_, _ = pipe.Exec(ctx)
|
||||
}
|
||||
|
||||
func loadDesktopClientPresence(ctx context.Context, redis *goredis.Client, clientIDs []uuid.UUID) map[uuid.UUID]bool {
|
||||
if redis == nil || len(clientIDs) == 0 {
|
||||
return nil
|
||||
@@ -75,6 +115,22 @@ func loadDesktopClientPresence(ctx context.Context, redis *goredis.Client, clien
|
||||
return result
|
||||
}
|
||||
|
||||
func loadDesktopUserOnlineClientCount(ctx context.Context, redis *goredis.Client, tenantID, workspaceID, userID int64, now time.Time) (int, bool) {
|
||||
if redis == nil || tenantID == 0 || workspaceID == 0 || userID == 0 {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
staleBefore := strconv.FormatInt(now.UTC().Add(-desktopClientPresenceTTL).UnixMilli(), 10)
|
||||
key := desktopUserClientsPresenceKey(tenantID, workspaceID, userID)
|
||||
pipe := redis.Pipeline()
|
||||
pipe.ZRemRangeByScore(ctx, key, "-inf", staleBefore)
|
||||
countCmd := pipe.ZCard(ctx, key)
|
||||
if _, err := pipe.Exec(ctx); err != nil && err != goredis.Nil {
|
||||
return 0, false
|
||||
}
|
||||
return int(countCmd.Val()), true
|
||||
}
|
||||
|
||||
func markDesktopAccountsPresent(ctx context.Context, redis *goredis.Client, clientID uuid.UUID, accountIDs []uuid.UUID) {
|
||||
if redis == nil {
|
||||
return
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/alicebob/miniredis/v2"
|
||||
"github.com/google/uuid"
|
||||
goredis "github.com/redis/go-redis/v9"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/tenant/repository"
|
||||
)
|
||||
|
||||
func TestDesktopUserOnlineClientCountPrefersRedisPresenceIndex(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
server := miniredis.RunT(t)
|
||||
redis := goredis.NewClient(&goredis.Options{Addr: server.Addr()})
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, redis.Close())
|
||||
})
|
||||
|
||||
tenantID := int64(7)
|
||||
workspaceID := int64(11)
|
||||
userID := int64(23)
|
||||
firstClientID := uuid.New()
|
||||
secondClientID := uuid.New()
|
||||
|
||||
markDesktopClientPresentForUser(ctx, redis, firstClientID, tenantID, workspaceID, userID)
|
||||
markDesktopClientPresentForUser(ctx, redis, secondClientID, tenantID, workspaceID, userID)
|
||||
|
||||
count, ok := loadDesktopUserOnlineClientCount(ctx, redis, tenantID, workspaceID, userID, time.Now().UTC())
|
||||
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, 2, count)
|
||||
}
|
||||
|
||||
func TestDesktopClientServiceOnlineCountReturnsRedisCountBeforeRepoFallback(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
server := miniredis.RunT(t)
|
||||
redis := goredis.NewClient(&goredis.Options{Addr: server.Addr()})
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, redis.Close())
|
||||
})
|
||||
|
||||
client := &repository.DesktopClient{
|
||||
ID: uuid.New(),
|
||||
TenantID: 7,
|
||||
WorkspaceID: 11,
|
||||
UserID: 23,
|
||||
}
|
||||
markDesktopClientPresentForUser(ctx, redis, client.ID, client.TenantID, client.WorkspaceID, client.UserID)
|
||||
markDesktopClientPresentForUser(ctx, redis, uuid.New(), client.TenantID, client.WorkspaceID, client.UserID)
|
||||
|
||||
count, err := (&DesktopClientService{redis: redis}).currentUserOnlineClientCount(ctx, client)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, count)
|
||||
}
|
||||
|
||||
func TestDesktopUserOnlineClientCountPrunesStaleRedisPresence(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
server := miniredis.RunT(t)
|
||||
redis := goredis.NewClient(&goredis.Options{Addr: server.Addr()})
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, redis.Close())
|
||||
})
|
||||
|
||||
tenantID := int64(7)
|
||||
workspaceID := int64(11)
|
||||
userID := int64(23)
|
||||
key := desktopUserClientsPresenceKey(tenantID, workspaceID, userID)
|
||||
now := time.Date(2026, 5, 6, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
require.NoError(t, redis.ZAdd(ctx, key,
|
||||
goredis.Z{Score: float64(now.Add(-2 * desktopClientPresenceTTL).UnixMilli()), Member: uuid.NewString()},
|
||||
goredis.Z{Score: float64(now.Add(-time.Second).UnixMilli()), Member: uuid.NewString()},
|
||||
).Err())
|
||||
|
||||
count, ok := loadDesktopUserOnlineClientCount(ctx, redis, tenantID, workspaceID, userID, now)
|
||||
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, 1, count)
|
||||
}
|
||||
@@ -63,6 +63,7 @@ type DesktopClientRepository interface {
|
||||
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)
|
||||
ListByUser(ctx context.Context, tenantID, workspaceID, userID int64) ([]DesktopClient, error)
|
||||
}
|
||||
|
||||
type desktopClientRepository struct {
|
||||
@@ -156,6 +157,23 @@ func (r *desktopClientRepository) ListByWorkspace(ctx context.Context, workspace
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return desktopClientsFromGenerated(rows), nil
|
||||
}
|
||||
|
||||
func (r *desktopClientRepository) ListByUser(ctx context.Context, tenantID, workspaceID, userID int64) ([]DesktopClient, error) {
|
||||
rows, err := r.q.ListDesktopClientsByUser(ctx, generated.ListDesktopClientsByUserParams{
|
||||
TenantID: tenantID,
|
||||
WorkspaceID: workspaceID,
|
||||
UserID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return desktopClientsFromGenerated(rows), nil
|
||||
}
|
||||
|
||||
func desktopClientsFromGenerated(rows []generated.DesktopClient) []DesktopClient {
|
||||
items := make([]DesktopClient, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
item := desktopClientFromGenerated(row)
|
||||
@@ -163,7 +181,7 @@ func (r *desktopClientRepository) ListByWorkspace(ctx context.Context, workspace
|
||||
items = append(items, *item)
|
||||
}
|
||||
}
|
||||
return items, nil
|
||||
return items
|
||||
}
|
||||
|
||||
func desktopClientFromGenerated(row generated.DesktopClient) *DesktopClient {
|
||||
|
||||
@@ -131,6 +131,57 @@ func (q *Queries) HeartbeatDesktopClient(ctx context.Context, arg HeartbeatDeskt
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listDesktopClientsByUser = `-- name: ListDesktopClientsByUser :many
|
||||
SELECT id, tenant_id, workspace_id, user_id, token_hash, device_name, os, cpu_arch, client_version, channel, created_at, last_seen_at, last_rotated_at, revoked_at
|
||||
FROM desktop_clients
|
||||
WHERE tenant_id = $1
|
||||
AND workspace_id = $2
|
||||
AND user_id = $3
|
||||
AND revoked_at IS NULL
|
||||
ORDER BY last_seen_at DESC NULLS LAST, created_at DESC
|
||||
`
|
||||
|
||||
type ListDesktopClientsByUserParams struct {
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
WorkspaceID int64 `json:"workspace_id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListDesktopClientsByUser(ctx context.Context, arg ListDesktopClientsByUserParams) ([]DesktopClient, error) {
|
||||
rows, err := q.db.Query(ctx, listDesktopClientsByUser, arg.TenantID, arg.WorkspaceID, arg.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []DesktopClient
|
||||
for rows.Next() {
|
||||
var i DesktopClient
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.TenantID,
|
||||
&i.WorkspaceID,
|
||||
&i.UserID,
|
||||
&i.TokenHash,
|
||||
&i.DeviceName,
|
||||
&i.Os,
|
||||
&i.CpuArch,
|
||||
&i.ClientVersion,
|
||||
&i.Channel,
|
||||
&i.CreatedAt,
|
||||
&i.LastSeenAt,
|
||||
&i.LastRotatedAt,
|
||||
&i.RevokedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listDesktopClientsByWorkspace = `-- name: ListDesktopClientsByWorkspace :many
|
||||
SELECT id, tenant_id, workspace_id, user_id, token_hash, device_name, os, cpu_arch, client_version, channel, created_at, last_seen_at, last_rotated_at, revoked_at
|
||||
FROM desktop_clients
|
||||
|
||||
@@ -117,6 +117,7 @@ type Querier interface {
|
||||
ListCompetitors(ctx context.Context, arg ListCompetitorsParams) ([]ListCompetitorsRow, error)
|
||||
ListDesktopAccountsByClient(ctx context.Context, arg ListDesktopAccountsByClientParams) ([]ListDesktopAccountsByClientRow, error)
|
||||
ListDesktopAccountsByUser(ctx context.Context, arg ListDesktopAccountsByUserParams) ([]ListDesktopAccountsByUserRow, error)
|
||||
ListDesktopClientsByUser(ctx context.Context, arg ListDesktopClientsByUserParams) ([]DesktopClient, error)
|
||||
ListDesktopClientsByWorkspace(ctx context.Context, workspaceID int64) ([]DesktopClient, error)
|
||||
ListImageAssets(ctx context.Context, arg ListImageAssetsParams) ([]ListImageAssetsRow, error)
|
||||
ListImageFolders(ctx context.Context, tenantID int64) ([]ListImageFoldersRow, error)
|
||||
|
||||
@@ -89,3 +89,12 @@ FROM desktop_clients
|
||||
WHERE workspace_id = sqlc.arg(workspace_id)
|
||||
AND revoked_at IS NULL
|
||||
ORDER BY last_seen_at DESC NULLS LAST, created_at DESC;
|
||||
|
||||
-- name: ListDesktopClientsByUser :many
|
||||
SELECT *
|
||||
FROM desktop_clients
|
||||
WHERE tenant_id = sqlc.arg(tenant_id)
|
||||
AND workspace_id = sqlc.arg(workspace_id)
|
||||
AND user_id = sqlc.arg(user_id)
|
||||
AND revoked_at IS NULL
|
||||
ORDER BY last_seen_at DESC NULLS LAST, created_at DESC;
|
||||
|
||||
@@ -49,6 +49,10 @@ func (s *stubDesktopClientRepo) ListByWorkspace(ctx context.Context, workspaceID
|
||||
panic("unexpected call")
|
||||
}
|
||||
|
||||
func (s *stubDesktopClientRepo) ListByUser(ctx context.Context, tenantID, workspaceID, userID int64) ([]repository.DesktopClient, error) {
|
||||
panic("unexpected call")
|
||||
}
|
||||
|
||||
func TestDesktopClientMiddleware_RequiresBearerToken(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
|
||||
Reference in New Issue
Block a user