import { config } from 'dotenv'; import { ConfidentialClientApplication } from '@azure/msal-node'; import PocketBase from 'pocketbase'; import { GraphTokenCache, BackendAuthConfig } from './types'; // Load environment variables from shared secrets directory config({ path: '/home/admin/secrets/.env' }); /** * Configuration Manager * Exposes frontend-safe configuration loaded from environment */ export class AuthConfigManager { /** * Get frontend configuration (safe to expose to browser) */ static getFrontendConfig() { return { pbUrl: process.env.PB_URL!, provider: 'microsoft', collection: 'Users', }; } /** * Get all backend secrets (never expose to frontend) */ static getBackendConfig() { return { clientId: process.env.CLIENT_ID!, tenantId: process.env.TENANT_ID!, clientSecret: process.env.CLIENT_SECRET!, pbDb: process.env.PB_DB!, }; } } /** * Microsoft Graph Token Management (Backend) * Handles token acquisition and caching for backend Graph API calls */ export class GraphTokenManager { private cca: ConfidentialClientApplication; private cache: GraphTokenCache | null = null; private config: Required; constructor(config: BackendAuthConfig) { this.config = { clientId: config.clientId || process.env.CLIENT_ID || '', tenantId: config.tenantId || process.env.TENANT_ID || '', clientSecret: config.clientSecret || process.env.CLIENT_SECRET || '', }; this.cca = new ConfidentialClientApplication({ auth: { clientId: this.config.clientId, authority: `https://login.microsoftonline.com/${this.config.tenantId}`, clientSecret: this.config.clientSecret, }, }); } /** * Get Graph token (from cache or acquire new) */ async getToken(): Promise { const now = Date.now(); // Check cache validity (with 60s buffer for expiration) if (this.cache && this.cache.expiresOn - 60000 > now) { return this.cache.token; } try { const result = await this.cca.acquireTokenByClientCredential({ scopes: ['https://graph.microsoft.com/.default'], }); if (!result?.accessToken) { throw new Error('Failed to acquire Graph token'); } const expiresOn = result.expiresOn ? new Date(result.expiresOn).getTime() : now + 55 * 60 * 1000; // Default 55 minutes this.cache = { token: result.accessToken, expiresOn, }; return result.accessToken; } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error'; throw new Error(`Failed to acquire Graph token: ${message}`); } } /** * Get token with expiration info */ async getTokenWithExpiry(): Promise<{ token: string; expiresOnISO: string }> { const token = await this.getToken(); const expiresOn = this.cache?.expiresOn || Date.now(); return { token, expiresOnISO: new Date(expiresOn).toISOString(), }; } /** * Check if token is cached and valid */ isTokenValid(): boolean { if (!this.cache) return false; const now = Date.now(); return this.cache.expiresOn - 60000 > now; } /** * Clear cache (force refresh on next call) */ clearCache(): void { this.cache = null; } } /** * PocketBase Token Validation (Backend) * Validates and uses user PocketBase tokens */ export class PocketBaseValidator { private pb: PocketBase; constructor(pbUrl?: string) { this.pb = new PocketBase(pbUrl || process.env.PB_DB!); } /** * Set user token and validate it */ async validateUserToken(token: string): Promise { try { this.pb.authStore.save(token, null); await this.pb.collection('Users').authRefresh(); return true; } catch (error) { console.error('Token validation failed:', error instanceof Error ? error.message : error); return false; } } /** * Get user record from token */ async getUserRecord(token: string): Promise { try { this.pb.authStore.save(token, null); const record = this.pb.authStore.record || this.pb.authStore.model; return record; } catch (error) { console.error('Failed to get user record:', error); return null; } } /** * Get PocketBase instance */ getPocketBase(): PocketBase { return this.pb; } } /** * Combined backend auth manager */ export class BackendAuth { graphTokenManager: GraphTokenManager; pbValidator: PocketBaseValidator; constructor(config: BackendAuthConfig, pbUrl?: string) { this.graphTokenManager = new GraphTokenManager(config); this.pbValidator = new PocketBaseValidator(pbUrl); } /** * Middleware to validate PocketBase token in requests * Usage: app.use('/*', (c, next) => backendAuth.validateTokenMiddleware(c, next)) */ async validateTokenMiddleware(c: any, next: any): Promise { const token = c.req.header('Authorization')?.replace('Bearer ', '') || (await c.req.json().catch(() => ({})))?.pbToken; if (token) { const isValid = await this.pbValidator.validateUserToken(token); if (!isValid) { return c.json({ error: 'Invalid token' }, 401); } } return next(); } }