From 9d5c592294ca3a5ea03fd2f8fa01a7d010f100e6 Mon Sep 17 00:00:00 2001 From: Jared Henderson Date: Tue, 7 Nov 2023 13:16:05 -0500 Subject: [PATCH] api: query admins super admin endpoint for internal reporting --- .../Api/PairQL/SuperAdmin/QueryAdmins.swift | 95 +++++++++++++++++++ .../Api/PairQL/SuperAdmin/SuperAdmin.swift | 7 ++ 2 files changed, 102 insertions(+) create mode 100644 api/Sources/Api/PairQL/SuperAdmin/QueryAdmins.swift diff --git a/api/Sources/Api/PairQL/SuperAdmin/QueryAdmins.swift b/api/Sources/Api/PairQL/SuperAdmin/QueryAdmins.swift new file mode 100644 index 00000000..adc91935 --- /dev/null +++ b/api/Sources/Api/PairQL/SuperAdmin/QueryAdmins.swift @@ -0,0 +1,95 @@ +import DuetSQL +import Gertie +import PairQL + +struct QueryAdmins: Pair { + static var auth: ClientAuth = .superAdmin + + struct AdminData: PairOutput { + struct Child: PairNestable { + struct Installation: PairNestable { + let userId: Int + let appVersion: String + let filterVersion: String + let modelIdentifier: String + let appReleaseChannel: ReleaseChannel + let createdAt: Date + } + + let name: String + let keyloggingEnabled: Bool + let screenshotsEnabled: Bool + let numKeychains: Int + let numKeys: Int + let numActivityItems: Int + let installations: [Installation] + let createdAt: Date + } + + let id: Admin.Id + let email: EmailAddress + let subscriptionId: Admin.SubscriptionId? + let subscriptionStatus: Admin.SubscriptionStatus + let numNotifications: Int + let numKeychains: Int + let children: [Child] + let createdAt: Date + } + + typealias Output = [AdminData] +} + +// resolver + +extension QueryAdmins: NoInputResolver { + static func resolve(in context: Context) async throws -> Output { + let admins = try await Admin.query().all() + return try await admins.concurrentMap { admin in + async let notifications = admin.notifications() + async let keychains = admin.keychains() + async let children = admin.users() + return .init( + id: admin.id, + email: admin.email, + subscriptionId: admin.subscriptionId, + subscriptionStatus: admin.subscriptionStatus, + numNotifications: (try await notifications).count, + numKeychains: (try await keychains).count, + children: try await (try await children).concurrentMap { child in + async let keychains = child.keychains() + async let devices = child.devices() + var numKeys = 0 + for keychain in try await keychains { + let keys = try await keychain.keys() + numKeys += keys.count + } + let deviceIds = try await devices.map(\.id) + async let screenshots = Screenshot.query().where(.userDeviceId |=| deviceIds).all() + async let keystrokes = KeystrokeLine.query().where(.userDeviceId |=| deviceIds).all() + let numActivityItems = (try await screenshots).count + (try await keystrokes).count + return .init( + name: child.name, + keyloggingEnabled: child.keyloggingEnabled, + screenshotsEnabled: child.screenshotsEnabled, + numKeychains: (try await keychains).count, + numKeys: numKeys, + numActivityItems: numActivityItems, + installations: try await (try await devices).concurrentMap { device in + let adminDevice = try await device.adminDevice() + return .init( + userId: device.numericId, + appVersion: device.appVersion, + filterVersion: adminDevice.filterVersion?.string ?? "unknown", + modelIdentifier: adminDevice.modelIdentifier, + appReleaseChannel: adminDevice.appReleaseChannel, + createdAt: device.createdAt + ) + }, + createdAt: child.createdAt + ) + }, + createdAt: admin.createdAt + ) + } + } +} diff --git a/api/Sources/Api/PairQL/SuperAdmin/SuperAdmin.swift b/api/Sources/Api/PairQL/SuperAdmin/SuperAdmin.swift index ac478deb..b735edf3 100644 --- a/api/Sources/Api/PairQL/SuperAdmin/SuperAdmin.swift +++ b/api/Sources/Api/PairQL/SuperAdmin/SuperAdmin.swift @@ -7,12 +7,16 @@ enum SuperAdminRoute: PairRoute { enum AuthedSuperAdminRoute: PairRoute { case createRelease(CreateRelease.Input) + case queryAdmins static let router = OneOf { Route(/Self.createRelease) { Operation(CreateRelease.self) Body(.input(CreateRelease.self)) } + Route(/Self.queryAdmins) { + Operation(QueryAdmins.self) + } } } @@ -36,6 +40,9 @@ extension SuperAdminRoute: RouteResponder { case .createRelease(let input): let output = try await CreateRelease.resolve(with: input, in: context) return try await respond(with: output) + case .queryAdmins: + let output = try await QueryAdmins.resolve(in: context) + return try await respond(with: output) } } }