import * as plugins from '../plugins.js'; import type { Reception } from './classes.reception.js'; import { App } from './classes.app.js'; export class AppManager { public receptionRef: Reception; public get db() { return this.receptionRef.db.smartdataDb; } public typedrouter = new plugins.typedrequest.TypedRouter(); public CApp = plugins.smartdata.setDefaultManagerForDoc(this, App); constructor(receptionRefArg: Reception) { this.receptionRef = receptionRefArg; this.receptionRef.typedrouter.addTypedRouter(this.typedrouter); // Handler: Get all global apps (for org owners) this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'getGlobalApps', async (requestArg) => { // Verify JWT await this.receptionRef.jwtManager.verifyJWTAndGetData(requestArg.jwt); // Get all active global apps const globalApps = await this.CApp.getInstances({ type: 'global', 'data.isActive': true, }); const appObjects = await Promise.all( globalApps.map(async (app) => await app.createSavableObject() as plugins.idpInterfaces.data.IGlobalApp) ); return { apps: appObjects, }; } ) ); // Handler: Check if user is global admin this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'checkGlobalAdmin', async (requestArg) => { const user = await this.receptionRef.userManager.getUserByJwt(requestArg.jwt); return { isGlobalAdmin: user?.data?.isGlobalAdmin ?? false, }; } ) ); // Handler: Get global apps with stats (admin only) this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'getGlobalAppStats', async (requestArg) => { await this.verifyGlobalAdmin(requestArg.jwt); // Get all global apps (including inactive) const globalApps = await this.CApp.getInstances({ type: 'global', }); const appsWithStats = await Promise.all( globalApps.map(async (app) => { const connections = await this.receptionRef.appConnectionManager.CAppConnection.getInstances({ 'data.appId': app.id, 'data.status': 'active', }); return { app: await app.createSavableObject() as plugins.idpInterfaces.data.IGlobalApp, connectionCount: connections.length, }; }) ); return { apps: appsWithStats }; } ) ); // Handler: Create global app (admin only) this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'createGlobalApp', async (requestArg) => { const jwtData = await this.verifyGlobalAdmin(requestArg.jwt); // Generate OAuth credentials const clientId = `app-${plugins.smartunique.shortId(12)}`; const clientSecret = plugins.smartunique.shortId(32); const clientSecretHash = await plugins.smarthash.sha256FromString(clientSecret); const app = new App(); app.id = `app-${plugins.smartunique.shortId(8)}`; app.type = 'global'; app.data = { name: requestArg.name, description: requestArg.description, logoUrl: requestArg.logoUrl, appUrl: requestArg.appUrl, category: requestArg.category, isActive: true, createdAt: Date.now(), createdByUserId: jwtData.data.userId, oauthCredentials: { clientId, clientSecretHash, redirectUris: requestArg.redirectUris, allowedScopes: requestArg.allowedScopes, grantTypes: ['authorization_code', 'refresh_token'], }, }; await app.save(); return { app: await app.createSavableObject() as plugins.idpInterfaces.data.IGlobalApp, clientSecret, // Only shown once }; } ) ); // Handler: Update global app (admin only) this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'updateGlobalApp', async (requestArg) => { await this.verifyGlobalAdmin(requestArg.jwt); const app = await this.CApp.getInstance({ id: requestArg.appId }); if (!app) { throw new Error('App not found'); } if (!app.isGlobalApp()) { throw new Error('Can only update global apps'); } // Update allowed fields - cast data to global app type after type guard const appData = app.data as plugins.idpInterfaces.data.IGlobalApp['data']; if (requestArg.updates.name !== undefined) appData.name = requestArg.updates.name; if (requestArg.updates.description !== undefined) appData.description = requestArg.updates.description; if (requestArg.updates.logoUrl !== undefined) appData.logoUrl = requestArg.updates.logoUrl; if (requestArg.updates.appUrl !== undefined) appData.appUrl = requestArg.updates.appUrl; if (requestArg.updates.category !== undefined) appData.category = requestArg.updates.category; if (requestArg.updates.isActive !== undefined) appData.isActive = requestArg.updates.isActive; if (requestArg.updates.redirectUris !== undefined) appData.oauthCredentials.redirectUris = requestArg.updates.redirectUris; if (requestArg.updates.allowedScopes !== undefined) appData.oauthCredentials.allowedScopes = requestArg.updates.allowedScopes; await app.save(); return { app: await app.createSavableObject() as plugins.idpInterfaces.data.IGlobalApp, }; } ) ); // Handler: Delete global app (admin only) this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'deleteGlobalApp', async (requestArg) => { await this.verifyGlobalAdmin(requestArg.jwt); const app = await this.CApp.getInstance({ id: requestArg.appId }); if (!app) { throw new Error('App not found'); } // Get and disconnect all connections const connections = await this.receptionRef.appConnectionManager.CAppConnection.getInstances({ 'data.appId': requestArg.appId, }); for (const connection of connections) { await connection.delete(); } await app.delete(); return { success: true, disconnectedOrganizations: connections.length, }; } ) ); // Handler: Regenerate OAuth credentials (admin only) this.typedrouter.addTypedHandler( new plugins.typedrequest.TypedHandler( 'regenerateAppCredentials', async (requestArg) => { await this.verifyGlobalAdmin(requestArg.jwt); const app = await this.CApp.getInstance({ id: requestArg.appId }); if (!app) { throw new Error('App not found'); } // Generate new credentials const clientId = `app-${plugins.smartunique.shortId(12)}`; const clientSecret = plugins.smartunique.shortId(32); const clientSecretHash = await plugins.smarthash.sha256FromString(clientSecret); app.data.oauthCredentials.clientId = clientId; app.data.oauthCredentials.clientSecretHash = clientSecretHash; await app.save(); return { clientId, clientSecret, // Only shown once }; } ) ); } /** * Verify that the user is a global admin */ private async verifyGlobalAdmin(jwt: string) { const jwtData = await this.receptionRef.jwtManager.verifyJWTAndGetData(jwt); const user = await this.receptionRef.userManager.getUserByJwt(jwt); if (!user?.data?.isGlobalAdmin) { throw new Error('Access denied: Global admin privileges required'); } return jwtData; } /** * Get all global apps */ public async getGlobalApps(): Promise { return await this.CApp.getInstances({ type: 'global', }); } /** * Get app by ID */ public async getAppById(appId: string): Promise { return await this.CApp.getInstance({ id: appId, }); } /** * Seed initial global apps (for development/testing) */ public async seedGlobalApps() { const defaultGlobalApps: Partial[] = [ { id: 'app-foss-global', type: 'global', data: { name: 'foss.global', description: 'Open Source Package Registry and Collaboration Platform', logoUrl: 'https://foss.global/assets/logo.png', appUrl: 'https://foss.global', oauthCredentials: { clientId: 'foss-global-client', clientSecretHash: '', // Will be set when OAuth is configured redirectUris: ['https://foss.global/auth/callback'], allowedScopes: ['openid', 'profile', 'email', 'organizations'], grantTypes: ['authorization_code', 'refresh_token'], }, isActive: true, category: 'Development', createdAt: Date.now(), createdByUserId: 'system', }, }, { id: 'app-task-vc', type: 'global', data: { name: 'task.vc', description: 'Task Management and Project Collaboration', logoUrl: 'https://task.vc/assets/logo.png', appUrl: 'https://task.vc', oauthCredentials: { clientId: 'task-vc-client', clientSecretHash: '', redirectUris: ['https://task.vc/auth/callback'], allowedScopes: ['openid', 'profile', 'email', 'organizations'], grantTypes: ['authorization_code', 'refresh_token'], }, isActive: true, category: 'Productivity', createdAt: Date.now(), createdByUserId: 'system', }, }, ]; for (const appData of defaultGlobalApps) { const existing = await this.CApp.getInstance({ id: appData.id }); if (!existing) { const app = new App(); app.id = appData.id!; app.type = appData.type!; app.data = appData.data as any; await app.save(); } } } }