feat(auth): add Google authentication endpoints and hooks (#51)
- Implemented `getGoogleAuthUrl` and `postGoogleCallback` in auth API. - Added corresponding hooks `useGoogleAuth` and `useGoogleCallback` for handling Google authentication. feat(api): enhance API structure with mentors and upload services - Created `mentors` and `upload` API modules with respective services for managing mentor details and file uploads. - Added session management utilities to handle authentication tokens in cookies. feat(users): expand user service with detailed user management - Enhanced user service to include detailed user information and update capabilities. - Added hooks for fetching and updating user data. feat(hooks): introduce hooks for mentors and upload functionalities - Added hooks for fetching mentor details and updating mentor information. - Implemented hooks for file upload, avatar upload, and CV upload with validation. refactor(types): organize and extend type definitions - Introduced new types for mentors and enhanced existing user types. - Re-exported user-related types for better accessibility. chore: update index files to include new services and hooks - Updated index files to export new services and hooks for mentors and uploads.
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
import { FC, ReactElement, useEffect } from 'react';
|
||||
|
||||
let globalIsProcessed = false;
|
||||
|
||||
export const GoogleOAuthPopupPage: FC = (): ReactElement => {
|
||||
|
||||
useEffect(() => {
|
||||
if (globalIsProcessed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const callBackend = async (code: string, state: string) => {
|
||||
if (globalIsProcessed) {
|
||||
return;
|
||||
}
|
||||
|
||||
globalIsProcessed = true;
|
||||
|
||||
try {
|
||||
const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:4099';
|
||||
let callbackUrl;
|
||||
|
||||
if (baseUrl.endsWith('/v1')) {
|
||||
callbackUrl = `${baseUrl}/auth/google/callback`;
|
||||
} else {
|
||||
callbackUrl = `${baseUrl}/v1/auth/google/callback`;
|
||||
}
|
||||
|
||||
const url = new URL(callbackUrl);
|
||||
url.searchParams.append('code', code);
|
||||
url.searchParams.append('state', state);
|
||||
url.searchParams.append('redirect_uri', `${window.location.origin}/auth/google-oauth-popup`);
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
mode: 'cors',
|
||||
credentials: 'omit',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText} - ${errorText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
window.opener?.postMessage(
|
||||
{
|
||||
type: 'GOOGLE_OAUTH_SUCCESS',
|
||||
payload: data,
|
||||
},
|
||||
window.location.origin
|
||||
);
|
||||
window.close();
|
||||
|
||||
} catch (error) {
|
||||
globalIsProcessed = false;
|
||||
|
||||
window.opener?.postMessage(
|
||||
{
|
||||
type: 'GOOGLE_OAUTH_ERROR',
|
||||
error: `Failed to process OAuth callback: ${error instanceof Error ? error.message : String(error)}`,
|
||||
},
|
||||
window.location.origin
|
||||
);
|
||||
window.close();
|
||||
}
|
||||
};
|
||||
|
||||
const handleOAuthResponse = () => {
|
||||
const isPopup = window.opener && window.opener !== window;
|
||||
|
||||
const detectJsonResponse = () => {
|
||||
try {
|
||||
const bodyText = document.body.innerText || document.body.textContent || '';
|
||||
const trimmedText = bodyText.trim();
|
||||
|
||||
if (trimmedText.startsWith('{') && trimmedText.endsWith('}')) {
|
||||
const parsedJson = JSON.parse(trimmedText);
|
||||
|
||||
if (parsedJson && typeof parsedJson === 'object') {
|
||||
const hasAccessToken = parsedJson.access_token || parsedJson.token || parsedJson.accessToken;
|
||||
|
||||
if (hasAccessToken) {
|
||||
return parsedJson;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const checkOAuthParams = () => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const code = urlParams.get('code');
|
||||
const state = urlParams.get('state');
|
||||
const error = urlParams.get('error');
|
||||
|
||||
if (error) {
|
||||
if (isPopup) {
|
||||
window.opener?.postMessage(
|
||||
{
|
||||
type: 'GOOGLE_OAUTH_ERROR',
|
||||
error: error,
|
||||
},
|
||||
window.location.origin
|
||||
);
|
||||
window.close();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (code && state) {
|
||||
if (isPopup) {
|
||||
callBackend(code, state);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const immediateJson = detectJsonResponse();
|
||||
if (immediateJson && isPopup) {
|
||||
window.opener?.postMessage(
|
||||
{
|
||||
type: 'GOOGLE_OAUTH_SUCCESS',
|
||||
payload: immediateJson,
|
||||
},
|
||||
window.location.origin
|
||||
);
|
||||
window.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkOAuthParams()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let attempts = 0;
|
||||
const maxAttempts = 50;
|
||||
|
||||
const checkForJson = () => {
|
||||
attempts++;
|
||||
const jsonResponse = detectJsonResponse();
|
||||
|
||||
if (jsonResponse && isPopup) {
|
||||
window.opener?.postMessage(
|
||||
{
|
||||
type: 'GOOGLE_OAUTH_SUCCESS',
|
||||
payload: jsonResponse,
|
||||
},
|
||||
window.location.origin
|
||||
);
|
||||
window.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (attempts < maxAttempts) {
|
||||
setTimeout(checkForJson, 500);
|
||||
} else if (isPopup) {
|
||||
window.opener?.postMessage(
|
||||
{
|
||||
type: 'GOOGLE_OAUTH_ERROR',
|
||||
error: 'Timeout waiting for response',
|
||||
},
|
||||
window.location.origin
|
||||
);
|
||||
window.close();
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(checkForJson, 1000);
|
||||
};
|
||||
|
||||
// Small delay to ensure DOM is ready
|
||||
setTimeout(handleOAuthResponse, 100);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-16 w-16 border-b-2 border-primary-500 mx-auto mb-4"></div>
|
||||
<h2 className="text-lg font-semibold text-primary-500 mb-2">
|
||||
Memproses Login Google...
|
||||
</h2>
|
||||
<p className="text-gray-600 text-sm">
|
||||
Jangan tutup jendela ini.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GoogleOAuthPopupPage;
|
||||
Reference in New Issue
Block a user