learning_ai_common_plat/dashboards/admin-web/src/lib/platform-client.ts

734 lines
22 KiB
TypeScript

/**
* Platform Service API client for the admin dashboard.
* Uses @bytelyst/api-client shared package.
*
* Replaces direct Cosmos DB calls for audit logging.
*/
import { createApiClient } from '@bytelyst/api-client';
const platformApi = createApiClient({
baseUrl: `${process.env.PLATFORM_SERVICE_URL || 'http://localhost:4003'}/api`,
defaultHeaders: {
'x-product-id': process.env.PRODUCT_ID || 'lysnrai',
},
});
// ── Audit ───────────────────────────────────────────────────────
export async function queryAudit(
options: {
category?: string;
userId?: string;
action?: string;
days?: number;
limit?: number;
offset?: number;
} = {}
) {
const params = new URLSearchParams();
if (options.category) params.set('category', options.category);
if (options.userId) params.set('userId', options.userId);
if (options.action) params.set('action', options.action);
if (options.days) params.set('days', String(options.days));
if (options.limit) params.set('limit', String(options.limit));
if (options.offset) params.set('offset', String(options.offset));
const qs = params.toString();
return platformApi.fetch<{ records: unknown[]; count: number }>(`/audit${qs ? `?${qs}` : ''}`);
}
export async function getAuditStats(days = 30) {
return platformApi.fetch<{ stats: Record<string, number>; days: number }>(
`/audit/stats?days=${days}`
);
}
export async function logAudit(input: {
userId: string;
action: string;
category?: string;
details?: Record<string, unknown>;
ipAddress?: string;
userAgent?: string;
}) {
return platformApi.fetch<{ accepted: boolean }>('/audit', {
method: 'POST',
body: JSON.stringify(input),
});
}
// ── Auth (proxied to platform-service) ──────────────────────────
export async function loginViaService(email: string, password: string, productId: string) {
return platformApi.fetch<{
accessToken: string;
refreshToken: string;
user: { id: string; email: string; role: string; plan: string; displayName: string };
}>('/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password, productId }),
});
}
export async function forgotPasswordViaService(email: string, productId: string) {
return platformApi.fetch<{ message: string }>('/auth/forgot-password', {
method: 'POST',
body: JSON.stringify({ email, productId }),
});
}
export async function changePasswordViaService(
token: string,
currentPassword: string,
newPassword: string
) {
return platformApi.fetch<{ message: string }>('/auth/change-password', {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
body: JSON.stringify({ currentPassword, newPassword }),
});
}
export async function deleteAccountViaService(token: string, password: string) {
return platformApi.fetch<{ message: string }>('/auth/account', {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` },
body: JSON.stringify({ password }),
});
}
export async function getMeViaService(token: string) {
return platformApi.fetch<{
id: string;
email: string;
role: string;
plan: string;
displayName: string;
}>('/auth/me', {
headers: { Authorization: `Bearer ${token}` },
});
}
export async function verifyTokenViaService(token: string) {
return platformApi.fetch<{
valid: boolean;
payload: Record<string, unknown> | null;
}>('/auth/verify', {
method: 'POST',
body: JSON.stringify({ token }),
});
}
// ── Admin User Management ─────────────────────────────────────
export interface UserDoc {
id: string;
productId: string;
email: string;
plan: 'free' | 'pro' | 'enterprise';
role: 'super_admin' | 'admin' | 'viewer' | 'user';
displayName: string;
status: 'active' | 'disabled';
lastLoginAt: string | null;
createdAt: string;
updatedAt: string;
}
export async function listUsers(
token: string,
limit = 100,
offset = 0
): Promise<{ users: UserDoc[] }> {
return platformApi.fetch<{ users: UserDoc[] }>(`/auth/users?limit=${limit}&offset=${offset}`, {
headers: { Authorization: `Bearer ${token}` },
});
}
export async function getUserCounts(
token: string
): Promise<{ total: number; byPlan: Record<string, number> }> {
return platformApi.fetch<{ total: number; byPlan: Record<string, number> }>('/auth/users/count', {
headers: { Authorization: `Bearer ${token}` },
});
}
export async function getUser(token: string, id: string): Promise<UserDoc> {
return platformApi.fetch<UserDoc>(`/auth/users/${id}`, {
headers: { Authorization: `Bearer ${token}` },
});
}
export async function updateUser(
token: string,
id: string,
updates: Record<string, unknown>
): Promise<UserDoc> {
return platformApi.fetch<UserDoc>(`/auth/users/${id}`, {
method: 'PUT',
headers: { Authorization: `Bearer ${token}` },
body: JSON.stringify(updates),
});
}
export async function deleteUser(token: string, id: string): Promise<{ success: boolean }> {
return platformApi.fetch<{ success: boolean }>(`/auth/users/${id}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` },
});
}
export async function registerUser(input: {
email: string;
password: string;
displayName: string;
role?: string;
productId: string;
}): Promise<{ user: UserDoc; accessToken: string; refreshToken: string }> {
return platformApi.fetch<{ user: UserDoc; accessToken: string; refreshToken: string }>(
'/auth/register',
{ method: 'POST', body: JSON.stringify(input) }
);
}
// ── Products ──────────────────────────────────────────────────
export interface ProductDoc {
id: string;
productId: string;
displayName: string;
licensePrefix: string;
packageName: string;
defaultPlan: 'free' | 'pro';
trialDays: number;
deviceLimits: { free: number; pro: number; enterprise: number };
websiteUrl: string;
status: 'active' | 'disabled';
createdAt: string;
updatedAt: string;
}
export async function listProducts(): Promise<{ products: ProductDoc[] }> {
return platformApi.fetch<{ products: ProductDoc[] }>('/products');
}
export async function getProduct(id: string): Promise<ProductDoc> {
return platformApi.fetch<ProductDoc>(`/products/${id}`);
}
export async function createProduct(input: Record<string, unknown>): Promise<ProductDoc> {
return platformApi.fetch<ProductDoc>('/products', {
method: 'POST',
body: JSON.stringify(input),
});
}
export async function updateProduct(
id: string,
updates: Record<string, unknown>
): Promise<ProductDoc> {
return platformApi.fetch<ProductDoc>(`/products/${id}`, {
method: 'PUT',
body: JSON.stringify(updates),
});
}
// ── Plans ─────────────────────────────────────────────────────
export interface PlanConfig {
id: string;
productId: string;
name: string;
displayName: string;
price: number;
tokens: number;
words: number;
dictations: number;
features: string[];
stripePriceId?: string;
active: boolean;
createdAt: string;
updatedAt: string;
}
export async function listPlans(productId: string): Promise<{ plans: PlanConfig[] }> {
return platformApi.fetch<{ plans: PlanConfig[] }>(`/plans`, {
headers: { 'x-product-id': productId },
});
}
export async function seedPlans(productId: string): Promise<{ plans: PlanConfig[] }> {
return platformApi.fetch<{ plans: PlanConfig[] }>('/plans/seed', {
method: 'POST',
headers: { 'x-product-id': productId },
});
}
// ── Product Onboarding ────────────────────────────────────────
/**
* Onboard a new product: seed default plans + kill_switch flag.
* Called automatically after product creation.
*/
export async function onboardProduct(productId: string): Promise<{
plans: PlanConfig[];
flags: { key: string; enabled: boolean }[];
}> {
// 1. Seed default plans (free, pro, enterprise)
const planResult = await seedPlans(productId);
// 2. Create kill_switch flag (disabled by default)
const flagResults: { key: string; enabled: boolean }[] = [];
try {
const flag = await platformApi.fetch<{ key: string; enabled: boolean }>('/flags', {
method: 'POST',
headers: { 'x-product-id': productId },
body: JSON.stringify({
key: 'kill_switch',
enabled: false,
description: 'Emergency kill switch — disables the product when enabled',
platforms: [],
segments: [],
percentage: 100,
}),
});
flagResults.push(flag);
} catch {
// kill_switch may already exist — that's fine
}
return { plans: planResult.plans, flags: flagResults };
}
// ── Feature Flags ─────────────────────────────────────────────
export interface FlagDoc {
id: string;
productId: string;
key: string;
description: string;
enabled: boolean;
platforms: string[];
segments: string[];
percentage: number;
createdAt: string;
updatedAt: string;
}
export async function listFlags(): Promise<{ flags: FlagDoc[] }> {
return platformApi.fetch<{ flags: FlagDoc[] }>('/flags');
}
export async function createFlag(input: Record<string, unknown>): Promise<FlagDoc> {
return platformApi.fetch<FlagDoc>('/flags', {
method: 'POST',
body: JSON.stringify(input),
});
}
export async function updateFlag(key: string, updates: Record<string, unknown>): Promise<FlagDoc> {
return platformApi.fetch<FlagDoc>(`/flags/${encodeURIComponent(key)}`, {
method: 'PUT',
body: JSON.stringify(updates),
});
}
export async function deleteFlag(key: string): Promise<void> {
await platformApi.fetch<void>(`/flags/${encodeURIComponent(key)}`, { method: 'DELETE' });
}
export async function evaluateFlag(
key: string,
userId: string
): Promise<{ enabled: boolean; key: string; userId: string }> {
return platformApi.fetch<{ enabled: boolean; key: string; userId: string }>(
`/flags/evaluate?key=${encodeURIComponent(key)}&userId=${encodeURIComponent(userId)}`
);
}
// ── API Tokens ───────────────────────────────────────────────
export interface ApiTokenResponse {
id: string;
productId: string;
userId: string;
userName: string;
name: string;
prefix: string;
status: 'active' | 'revoked' | 'expired';
scopes: string[];
createdAt: string;
expiresAt: string;
lastUsed: string | null;
}
export async function listTokens(token: string): Promise<{ tokens: ApiTokenResponse[] }> {
return platformApi.fetch<{ tokens: ApiTokenResponse[] }>('/tokens', {
headers: { Authorization: `Bearer ${token}` },
});
}
export async function createToken(
token: string,
input: { name: string; scopes?: string[]; expiresInDays?: number }
): Promise<ApiTokenResponse & { rawToken: string }> {
return platformApi.fetch<ApiTokenResponse & { rawToken: string }>('/tokens', {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
body: JSON.stringify(input),
});
}
export async function revokeToken(token: string, tokenId: string): Promise<{ success: boolean }> {
return platformApi.fetch<{ success: boolean }>(`/tokens/${tokenId}`, {
method: 'PATCH',
headers: { Authorization: `Bearer ${token}` },
body: JSON.stringify({ action: 'revoke' }),
});
}
export async function deleteToken(token: string, tokenId: string): Promise<{ success: boolean }> {
return platformApi.fetch<{ success: boolean }>(`/tokens/${tokenId}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` },
});
}
export async function countActiveTokens(token: string): Promise<{ count: number }> {
return platformApi.fetch<{ count: number }>('/tokens/count', {
headers: { Authorization: `Bearer ${token}` },
});
}
// ── Themes ───────────────────────────────────────────────────
export interface ThemeDoc {
id: string;
productId: string;
name: string;
description: string | null;
ios: Record<string, string>;
android: Record<string, string>;
desktop: Record<string, string>;
is_active: boolean;
is_default: boolean;
version: string;
created_at: string;
updated_at: string;
created_by: string | null;
}
export async function listThemes(token: string): Promise<ThemeDoc[]> {
return platformApi.fetch<ThemeDoc[]>('/themes', {
headers: { Authorization: `Bearer ${token}` },
});
}
export async function getTheme(token: string, id: string): Promise<ThemeDoc> {
return platformApi.fetch<ThemeDoc>(`/themes/${id}`, {
headers: { Authorization: `Bearer ${token}` },
});
}
export async function createTheme(
token: string,
input: Record<string, unknown>
): Promise<ThemeDoc> {
return platformApi.fetch<ThemeDoc>('/themes', {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
body: JSON.stringify(input),
});
}
export async function updateTheme(
token: string,
id: string,
updates: Record<string, unknown>
): Promise<ThemeDoc> {
return platformApi.fetch<ThemeDoc>(`/themes/${id}`, {
method: 'PUT',
headers: { Authorization: `Bearer ${token}` },
body: JSON.stringify(updates),
});
}
export async function deleteTheme(token: string, id: string): Promise<{ success: boolean }> {
return platformApi.fetch<{ success: boolean }>(`/themes/${id}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` },
});
}
export async function activateTheme(token: string, id: string): Promise<ThemeDoc> {
return platformApi.fetch<ThemeDoc>(`/themes/${id}/activate`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
});
}
export async function getActiveTheme(productId: string): Promise<ThemeDoc> {
return platformApi.fetch<ThemeDoc>(`/themes/active?productId=${encodeURIComponent(productId)}`);
}
// ── Notifications ─────────────────────────────────────────────
export interface NotificationPrefs {
pushEnabled: boolean;
emailEnabled: boolean;
categories: Record<string, boolean>;
}
export async function getNotificationPrefs(userId: string): Promise<NotificationPrefs | null> {
try {
return await platformApi.fetch<NotificationPrefs>(`/notifications/prefs/${userId}`);
} catch {
return null;
}
}
export async function listDevices(userId: string) {
return platformApi.fetch<{ devices: unknown[] }>(`/notifications/devices/${userId}`);
}
export async function listNotifications(limit = 50, offset = 0) {
return platformApi.fetch<{ notifications: unknown[]; count: number }>(
`/notifications?limit=${limit}&offset=${offset}`
);
}
// ── Telemetry ────────────────────────────────────────────────────
export interface TelemetryEvent {
id: string;
productId: string;
userId?: string;
anonymousInstallId?: string;
sessionId: string;
platform: string;
channel: string;
osFamily: string;
osVersion?: string;
deviceModel?: string;
appVersion: string;
buildNumber: string;
releaseChannel: string;
eventType: string;
module: string;
feature?: string;
eventName: string;
errorDomain?: string;
errorCode?: string;
message?: string;
tags?: Record<string, string>;
metrics?: Record<string, number>;
context?: Record<string, unknown>;
occurredAt: string;
receivedAt: string;
}
export interface TelemetryCluster {
id: string;
fingerprint: string;
platform: string;
channel: string;
module: string;
eventName: string;
affectedVersions: Array<{
appVersion: string;
buildNumber: string;
count: number;
lastSeenAt: string;
}>;
firstSeenAt: string;
lastSeenAt: string;
totalCount: number;
affectedUserIds: string[];
affectedInstallIds: string[];
affectedOsFamilies: string[];
sampleErrorDomain?: string;
sampleErrorCode?: string;
sampleMessage?: string;
severity: string;
status?: string;
resolvedBy?: string;
resolvedAt?: string;
}
export interface TelemetryMetrics {
totalEventsIngested: number;
totalEventsRejected: number;
totalBatchRequests: number;
totalRateLimited: number;
totalPiiBlocked: number;
totalDuplicatesDropped: number;
uptimeSince: string;
}
export async function queryTelemetryEvents(
token: string,
filters: Record<string, string> = {}
): Promise<{ events: TelemetryEvent[]; total: number; continuationToken?: string }> {
const params = new URLSearchParams(filters);
return platformApi.fetch<{ events: TelemetryEvent[]; total: number; continuationToken?: string }>(
`/telemetry/query?${params.toString()}`,
{ headers: { Authorization: `Bearer ${token}` } }
);
}
export async function queryTelemetryClusters(
token: string,
filters: Record<string, string> = {}
): Promise<{ clusters: TelemetryCluster[]; total: number }> {
const params = new URLSearchParams(filters);
return platformApi.fetch<{ clusters: TelemetryCluster[]; total: number }>(
`/telemetry/clusters?${params.toString()}`,
{ headers: { Authorization: `Bearer ${token}` } }
);
}
// ── Telemetry Policies ──────────────────────────────────────────
export interface TelemetryPolicy {
id: string;
productId: string;
name: string;
description?: string;
enabled: boolean;
priority: number;
eventTypes: string[];
modules: string[];
samplingRate: number;
targeting: {
platforms?: string[];
channels?: string[];
osFamilies?: string[];
appVersions?: string[];
buildNumbers?: string[];
buildNumberRange?: { min?: number; max?: number };
userIds?: string[];
anonymousInstallIds?: string[];
countryCodes?: string[];
regionCodes?: string[];
releaseChannels?: string[];
percentage?: number;
};
startsAt?: string;
expiresAt?: string;
createdAt: string;
updatedAt: string;
createdBy?: string;
}
export async function listTelemetryPolicies(
token: string
): Promise<{ policies: TelemetryPolicy[] }> {
return platformApi.fetch<{ policies: TelemetryPolicy[] }>('/telemetry/policies', {
headers: { Authorization: `Bearer ${token}` },
});
}
export async function createTelemetryPolicy(
token: string,
input: Record<string, unknown>
): Promise<TelemetryPolicy> {
return platformApi.fetch<TelemetryPolicy>('/telemetry/policies', {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
body: JSON.stringify(input),
});
}
export async function updateTelemetryPolicy(
token: string,
id: string,
updates: Record<string, unknown>
): Promise<TelemetryPolicy> {
return platformApi.fetch<TelemetryPolicy>(`/telemetry/policies/${id}`, {
method: 'PUT',
headers: { Authorization: `Bearer ${token}` },
body: JSON.stringify(updates),
});
}
export async function deleteTelemetryPolicy(
token: string,
id: string
): Promise<{ success: boolean }> {
return platformApi.fetch<{ success: boolean }>(`/telemetry/policies/${id}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` },
});
}
// ── Telemetry Policy Preview ─────────────────────────────────────
export async function previewTelemetryPolicy(
token: string,
targeting: Record<string, unknown>
): Promise<{ matchedClients: number; totalClients: number; sampleSize: number }> {
return platformApi.fetch<{ matchedClients: number; totalClients: number; sampleSize: number }>(
'/telemetry/policies/preview',
{
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
body: JSON.stringify({ targeting }),
}
);
}
// ── Telemetry Cluster Actions ────────────────────────────────────
export async function updateClusterStatus(
token: string,
clusterId: string,
pk: string,
status: 'open' | 'resolved' | 'ignored'
): Promise<TelemetryCluster> {
return platformApi.fetch<TelemetryCluster>(
`/telemetry/clusters/${encodeURIComponent(clusterId)}?pk=${encodeURIComponent(pk)}`,
{
method: 'PATCH',
headers: { Authorization: `Bearer ${token}` },
body: JSON.stringify({ status }),
}
);
}
export async function getTelemetryMetrics(token: string): Promise<TelemetryMetrics> {
return platformApi.fetch<TelemetryMetrics>('/telemetry/metrics', {
headers: { Authorization: `Bearer ${token}` },
});
}
export interface GeoDistributionEntry {
countryCode: string;
count: number;
}
export async function getTelemetryGeoDistribution(
token: string,
from?: string,
to?: string
): Promise<{ distribution: GeoDistributionEntry[] }> {
const params = new URLSearchParams();
if (from) params.set('from', from);
if (to) params.set('to', to);
const qs = params.toString() ? `?${params.toString()}` : '';
return platformApi.fetch<{ distribution: GeoDistributionEntry[] }>(`/telemetry/geo${qs}`, {
headers: { Authorization: `Bearer ${token}` },
});
}
export async function eraseTelemetryUser(
token: string,
userId: string
): Promise<{ userId: string; eventsDeleted: number; clustersUpdated: number }> {
return platformApi.fetch<{ userId: string; eventsDeleted: number; clustersUpdated: number }>(
`/telemetry/user/${encodeURIComponent(userId)}`,
{
method: 'DELETE',
headers: { Authorization: `Bearer ${token}` },
}
);
}