Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions packages/common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> - The authentication token
*/
async requestAuthToken(integrationId: string, externalId: string): Promise<string> {
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 = {}) {
Expand Down
9 changes: 9 additions & 0 deletions packages/common/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
132 changes: 130 additions & 2 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> - Resolves when authentication is complete
*/
async authenticateWithPopup(token: string): Promise<void> {
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<void> - Resolves when authentication is complete
*/
async authenticate(integrationId: string, externalId: string): Promise<void> {
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 = {}) {
Expand All @@ -34,13 +139,36 @@ export function createReactBundleUpPlugin(options: ReactBundleUpOptions = {}) {

// React Hook for BundleUp integration
export function useBundleUp(config?: BundleUpConfig) {
const bundleUp = useRef<BundleUpCore>();
const bundleUp = useRef<ReactBundleUpPlugin>();

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<void> => {
return bundleUp.authenticate(integrationId, externalId);
};

const authenticateWithToken = async (token: string): Promise<void> => {
return bundleUp.authenticateWithPopup(token);
};

const requestToken = async (integrationId: string, externalId: string): Promise<string> => {
return bundleUp.requestAuthToken(integrationId, externalId);
};

return {
authenticate,
authenticateWithToken,
requestToken,
};
}

export * from '@bundleup/common';
3 changes: 2 additions & 1 deletion packages/react/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
"rootDir": "./src",
"lib": ["ES2020", "DOM"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.*"]
Expand Down