diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index d25acd7..90bc434 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -38,6 +38,50 @@ export class BundleUpCore { getConfig(): BundleUpConfig { return { ...this.config }; } + + /** + * Backend method to request an authentication token + * @param integrationId - The integration ID provided by user + * @param externalId - The external ID provided by user + * @returns Promise - The authentication token + */ + async requestAuthToken(integrationId: string, externalId: string): Promise { + if (!this.config.apiKey) { + throw new Error('API key is required for authentication'); + } + + this.log('Requesting authentication token', { integrationId, externalId }); + + try { + const response = await fetch('https://auth.bundleup.io/authorize', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.config.apiKey}`, + }, + body: JSON.stringify({ + integrationId, + externalId, + }), + }); + + if (!response.ok) { + throw new Error(`Authentication request failed: ${response.status} ${response.statusText}`); + } + + const data: any = await response.json(); + + if (!data.token) { + throw new Error('Invalid response: token not found'); + } + + this.log('Authentication token received successfully'); + return data.token; + } catch (error) { + this.log('Authentication token request failed', error); + throw error; + } + } } export function createBundleUpPlugin(options: PluginOptions = {}) { diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index 885a9a7..c4ee918 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -10,4 +10,13 @@ export interface BundleMetrics { gzipSize: number; modules: number; chunks: number; +} + +export interface AuthenticationRequest { + integrationId: string; + externalId: string; +} + +export interface AuthenticationResponse { + token: string; } \ No newline at end of file diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index af3c234..d403f25 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -26,6 +26,111 @@ export class ReactBundleUpPlugin extends BundleUpCore { }; }; } + + /** + * Frontend method to open popup window for authentication + * @param token - Authentication token obtained from backend + * @returns Promise - Resolves when authentication is complete + */ + async authenticateWithPopup(token: string): Promise { + return new Promise((resolve, reject) => { + if (!token) { + reject(new Error('Token is required for authentication')); + return; + } + + this.log('Opening authentication popup with token'); + + // Check if we're in a browser environment + if (typeof window === 'undefined') { + reject(new Error('Authentication popup is only available in browser environment')); + return; + } + + // Open popup window + const popup = window.open( + `https://auth.bundleup.io/${token}`, + 'bundleup-auth', + 'width=500,height=600,scrollbars=yes,resizable=yes' + ); + + if (!popup) { + reject(new Error('Failed to open popup window. Please check popup blocker settings.')); + return; + } + + // Listen for messages from popup + const handleMessage = (event: MessageEvent) => { + // Security check: verify origin + if (event.origin !== 'https://auth.bundleup.io') { + return; + } + + this.log('Received message from authentication popup', event.data); + + if (event.data && event.data.type === 'bundleup-auth-success') { + // Authentication successful + cleanup(); + popup.close(); + this.log('Authentication completed successfully'); + resolve(); + } else if (event.data && event.data.type === 'bundleup-auth-error') { + // Authentication failed + cleanup(); + popup.close(); + this.log('Authentication failed', event.data.error); + reject(new Error(event.data.error || 'Authentication failed')); + } + }; + + // Check if popup is closed manually + const checkClosed = setInterval(() => { + if (popup.closed) { + cleanup(); + reject(new Error('Authentication popup was closed by user')); + } + }, 1000); + + const cleanup = () => { + window.removeEventListener('message', handleMessage); + clearInterval(checkClosed); + }; + + // Add message listener + window.addEventListener('message', handleMessage); + + // Handle popup blocked or closed immediately + setTimeout(() => { + if (popup.closed) { + cleanup(); + reject(new Error('Popup was blocked or closed immediately')); + } + }, 100); + }); + } + + /** + * Complete authentication flow: request token and open popup + * @param integrationId - The integration ID + * @param externalId - The external ID + * @returns Promise - Resolves when authentication is complete + */ + async authenticate(integrationId: string, externalId: string): Promise { + try { + this.log('Starting complete authentication flow'); + + // Step 1: Request token from backend + const token = await this.requestAuthToken(integrationId, externalId); + + // Step 2: Open popup with token + await this.authenticateWithPopup(token); + + this.log('Authentication flow completed successfully'); + } catch (error) { + this.log('Authentication flow failed', error); + throw error; + } + } } export function createReactBundleUpPlugin(options: ReactBundleUpOptions = {}) { @@ -34,13 +139,36 @@ export function createReactBundleUpPlugin(options: ReactBundleUpOptions = {}) { // React Hook for BundleUp integration export function useBundleUp(config?: BundleUpConfig) { - const bundleUp = useRef(); + const bundleUp = useRef(); if (!bundleUp.current) { - bundleUp.current = new BundleUpCore(config); + bundleUp.current = new ReactBundleUpPlugin({ bundleUpConfig: config }); } return bundleUp.current; } +// React Hook for authentication +export function useBundleUpAuth(config?: BundleUpConfig) { + const bundleUp = useBundleUp(config); + + const authenticate = async (integrationId: string, externalId: string): Promise => { + return bundleUp.authenticate(integrationId, externalId); + }; + + const authenticateWithToken = async (token: string): Promise => { + return bundleUp.authenticateWithPopup(token); + }; + + const requestToken = async (integrationId: string, externalId: string): Promise => { + return bundleUp.requestAuthToken(integrationId, externalId); + }; + + return { + authenticate, + authenticateWithToken, + requestToken, + }; +} + export * from '@bundleup/common'; \ No newline at end of file diff --git a/packages/react/tsconfig.json b/packages/react/tsconfig.json index 4af17f9..7340615 100644 --- a/packages/react/tsconfig.json +++ b/packages/react/tsconfig.json @@ -2,7 +2,8 @@ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./dist", - "rootDir": "./src" + "rootDir": "./src", + "lib": ["ES2020", "DOM"] }, "include": ["src/**/*"], "exclude": ["node_modules", "dist", "**/*.test.*"]