diff --git a/extensions/api.d.ts b/extensions/api.d.ts index 0df607ece..6fde2c03f 100644 --- a/extensions/api.d.ts +++ b/extensions/api.d.ts @@ -1,21 +1,23 @@ -import APIError from '@heyputer/backend/src/api/APIError.js'; +import type APIError from '@heyputer/backend/src/api/APIError.js'; import type { WebServerService } from '@heyputer/backend/src/modules/web/WebServerService.js'; -import query from '@heyputer/backend/src/om/query/query'; +import type query from '@heyputer/backend/src/om/query/query'; import type { Actor } from '@heyputer/backend/src/services/auth/Actor.js'; import type { BaseDatabaseAccessService } from '@heyputer/backend/src/services/database/BaseDatabaseAccessService.d.ts'; +import type { EmailService } from '@heyputer/backend/src/services/EmailService.js'; +import type { EntityStoreService } from '@heyputer/backend/src/services/EntityStoreService.js'; import type { MeteringService } from '@heyputer/backend/src/services/MeteringService/MeteringService.ts'; import type { MeteringServiceWrapper } from '@heyputer/backend/src/services/MeteringService/MeteringServiceWrapper.mjs'; -import { DynamoKVStore } from '@heyputer/backend/src/services/repositories/DynamoKVStore/DynamoKVStore.ts'; +import type { DynamoKVStore } from '@heyputer/backend/src/services/repositories/DynamoKVStore/DynamoKVStore.ts'; import type { SUService } from '@heyputer/backend/src/services/SUService.js'; import type { IUser } from '@heyputer/backend/src/services/User.js'; import type { UserService } from '@heyputer/backend/src/services/UserService.d.ts'; -import { Context } from '@heyputer/backend/src/util/context.js'; -import kvjs from '@heyputer/kv.js'; +import type { Context } from '@heyputer/backend/src/util/context.js'; +import type kvjs from '@heyputer/kv.js'; import type { RequestHandler } from 'express'; import type FSNodeContext from '../src/backend/src/filesystem/FSNodeContext.js'; import type helpers from '../src/backend/src/helpers.js'; import type * as ExtensionControllerExports from './ExtensionController/src/ExtensionController.ts'; -import { type EmailService } from '@heyputer/backend/src/services/EmailService.js'; + declare global { namespace Express { interface Request { @@ -98,7 +100,8 @@ interface ServiceNameMap { database: BaseDatabaseAccessService; user: UserService; 'web-server': WebServerService; - 'email': EmailService + 'email': EmailService; + 'es:app': EntityStoreService; } export interface ExtensionEventTypeMap { diff --git a/extensions/app-telemetry/app-user-count.ts b/extensions/app-telemetry/app-user-count.ts index fa04e3423..c446255b0 100644 --- a/extensions/app-telemetry/app-user-count.ts +++ b/extensions/app-telemetry/app-user-count.ts @@ -3,6 +3,46 @@ const { db } = extension.import('data'); const { APIError } = extension.import('core'); const app_es = extension.import('service:es:app') as any; +const DEFAULT_LIMIT = 100; +const MAX_LIMIT = 1000; +const MAX_OFFSET = 100_000; + +const parseIntegerParam = ( + value: unknown, + { + key, + min, + max, + fallback, + }: { key: string, min: number, max: number, fallback: number }, +) => { + if ( value === undefined || value === null ) return fallback; + + const parsed = typeof value === 'number' + ? value + : (typeof value === 'string' && value.trim() !== '' + ? Number(value) + : Number.NaN); + + if ( !Number.isFinite(parsed) || !Number.isInteger(parsed) ) { + throw APIError.create('field_invalid', undefined, { + key, + expected: `an integer between ${min} and ${max}`, + got: value, + }); + } + + if ( parsed < min || parsed > max ) { + throw APIError.create('field_invalid', undefined, { + key, + expected: `an integer between ${min} and ${max}`, + got: parsed, + }); + } + + return parsed; +}; + extension.on('create.interfaces', (event) => { event.createInterface('app-telemetry', { description: 'Provides methods for getting app telemetry', @@ -39,7 +79,20 @@ extension.on('create.interfaces', (event) => { extension.on('create.drivers', event => { event.createDriver('app-telemetry', 'app-telemetry', { - async get_users ({ app_uuid, limit = 100, offset = 0 }: { app_uuid: string, limit: number, offset: number }) { + async get_users ({ app_uuid, limit, offset }: { app_uuid: string, limit?: number, offset?: number }) { + const safeLimit = parseIntegerParam(limit, { + key: 'limit', + min: 1, + max: MAX_LIMIT, + fallback: DEFAULT_LIMIT, + }); + const safeOffset = parseIntegerParam(offset, { + key: 'offset', + min: 0, + max: MAX_OFFSET, + fallback: 0, + }); + // first lets make sure executor owns this app const [result] = (await app_es.select({ predicate: new Eq({ key: 'uid', value: app_uuid }) })); if ( ! result ) { @@ -50,7 +103,7 @@ extension.on('create.drivers', event => { const users: Array<{ username: string, uuid: string }> = await db.read(`SELECT user.username, user.uuid FROM user_to_app_permissions INNER JOIN user ON user_to_app_permissions.user_id = user.id WHERE permission = 'flag:app-is-authenticated' AND app_id=? ORDER BY (dt IS NOT NULL), dt, user_id LIMIT ? OFFSET ?`, - [result.private_meta.mysql_id, limit, offset]); + [result.private_meta.mysql_id, safeLimit, safeOffset]); return users.map(e => { return { user: e.username, user_uuid: e.uuid }; });