🔒 (security) security improvement
This commit is contained in:
@ -1,83 +1,83 @@
|
||||
"use server";
|
||||
import { api } from "@/shared/lib/ky/connector";
|
||||
import { apiErrorHandler } from "@/shared/lib/ky/errorHandler";
|
||||
import { ServerRequestCallback } from "@/shared/types/serverRequestCallback";
|
||||
|
||||
/**
|
||||
* @function SendCallbackToServer
|
||||
* @description Proxies OAuth callback requests from the frontend to the main backend system.
|
||||
* Acts as an intermediary between the client Next.js application and the Elysia server.
|
||||
* Handles the forwarding of OAuth provider callback data for authentication processing.
|
||||
*
|
||||
* @param {string} data - The OAuth callback data received from the provider, typically containing
|
||||
* query parameters such as authorization code, user consent, scopes, state,
|
||||
* and other OAuth-specific information. Usually obtained from
|
||||
* `window.location.search` in browser environments.
|
||||
* @param {string} provider - The name of the OAuth provider/service (e.g., "google", "github",
|
||||
* "facebook"). Used to construct the appropriate backend API endpoint.
|
||||
*
|
||||
* @returns {Promise<Object>} The response data from the backend server after processing the
|
||||
* OAuth callback. Typically contains authentication tokens, user
|
||||
* information, or session data.
|
||||
*
|
||||
* @throws {Error} If the network request fails or the backend returns an error response.
|
||||
* @throws {Error} If the required environment variable APP_DOMAIN is not defined.
|
||||
* @throws {Error} If the provided parameters are invalid or missing.
|
||||
*
|
||||
* @example
|
||||
* // Handling OAuth callback in a React component
|
||||
* useEffect(() => {
|
||||
* const handleOAuthCallback = async () => {
|
||||
* try {
|
||||
* const result = await SendCallbackToServer(window.location.search, "google");
|
||||
* // Handle successful authentication (e.g., store tokens, redirect user)
|
||||
* console.log("Authentication successful:", result);
|
||||
* } catch (error) {
|
||||
* // Handle authentication errors
|
||||
* console.error("Authentication failed:", error);
|
||||
* }
|
||||
* };
|
||||
*
|
||||
* handleOAuthCallback();
|
||||
* }, []);
|
||||
*
|
||||
* @example
|
||||
* // Usage with different providers
|
||||
* await SendCallbackToServer(window.location.search, "github");
|
||||
* await SendCallbackToServer(window.location.search, "facebook");
|
||||
* await SendCallbackToServer(window.location.search, "microsoft");
|
||||
*
|
||||
* @remarks
|
||||
* - This function is specifically designed for OAuth callback handling in a Next.js frontend
|
||||
* acting as a proxy to an Elysia backend.
|
||||
* - The `data` parameter should include the complete query string from the OAuth redirect.
|
||||
* - The callback URI is automatically constructed using the APP_DOMAIN environment variable.
|
||||
* - Ensure APP_DOMAIN is properly configured in your environment variables.
|
||||
*/
|
||||
export const SendCallbackToServer = async (
|
||||
data: string,
|
||||
provider: string
|
||||
): Promise<ServerRequestCallback> => {
|
||||
// Construct the backend and frontend handler URLs
|
||||
const backendHandlerUrl = `auth/${provider}/callback/`;
|
||||
const frontendHandlerUrl = `${process.env
|
||||
.APP_DOMAIN!}/auth/callback/${provider}`;
|
||||
|
||||
try {
|
||||
// Forward the OAuth callback data to the backend for processing
|
||||
const response = await api.get(
|
||||
`${backendHandlerUrl}${data}&callbackURI=${frontendHandlerUrl}`
|
||||
);
|
||||
|
||||
// Parse the JSON response from the backend and return the result
|
||||
const result = await response.json();
|
||||
return {
|
||||
success: true,
|
||||
status: response.status,
|
||||
text: { message: "Callback processed successfully" },
|
||||
data: result,
|
||||
};
|
||||
} catch (error) {
|
||||
return apiErrorHandler(error);
|
||||
}
|
||||
};
|
||||
"use server";
|
||||
import { api } from "@/shared/lib/ky/connector";
|
||||
import { apiErrorHandler } from "@/shared/lib/ky/errorHandler";
|
||||
import { ServerRequestCallback } from "@/shared/types/serverRequestCallback";
|
||||
|
||||
/**
|
||||
* @function SendCallbackToServer
|
||||
* @description Proxies OAuth callback requests from the frontend to the main backend system.
|
||||
* Acts as an intermediary between the client Next.js application and the Elysia server.
|
||||
* Handles the forwarding of OAuth provider callback data for authentication processing.
|
||||
*
|
||||
* @param {string} data - The OAuth callback data received from the provider, typically containing
|
||||
* query parameters such as authorization code, user consent, scopes, state,
|
||||
* and other OAuth-specific information. Usually obtained from
|
||||
* `window.location.search` in browser environments.
|
||||
* @param {string} provider - The name of the OAuth provider/service (e.g., "google", "github",
|
||||
* "facebook"). Used to construct the appropriate backend API endpoint.
|
||||
*
|
||||
* @returns {Promise<Object>} The response data from the backend server after processing the
|
||||
* OAuth callback. Typically contains authentication tokens, user
|
||||
* information, or session data.
|
||||
*
|
||||
* @throws {Error} If the network request fails or the backend returns an error response.
|
||||
* @throws {Error} If the required environment variable APP_DOMAIN is not defined.
|
||||
* @throws {Error} If the provided parameters are invalid or missing.
|
||||
*
|
||||
* @example
|
||||
* // Handling OAuth callback in a React component
|
||||
* useEffect(() => {
|
||||
* const handleOAuthCallback = async () => {
|
||||
* try {
|
||||
* const result = await SendCallbackToServer(window.location.search, "google");
|
||||
* // Handle successful authentication (e.g., store tokens, redirect user)
|
||||
* console.log("Authentication successful:", result);
|
||||
* } catch (error) {
|
||||
* // Handle authentication errors
|
||||
* console.error("Authentication failed:", error);
|
||||
* }
|
||||
* };
|
||||
*
|
||||
* handleOAuthCallback();
|
||||
* }, []);
|
||||
*
|
||||
* @example
|
||||
* // Usage with different providers
|
||||
* await SendCallbackToServer(window.location.search, "github");
|
||||
* await SendCallbackToServer(window.location.search, "facebook");
|
||||
* await SendCallbackToServer(window.location.search, "microsoft");
|
||||
*
|
||||
* @remarks
|
||||
* - This function is specifically designed for OAuth callback handling in a Next.js frontend
|
||||
* acting as a proxy to an Elysia backend.
|
||||
* - The `data` parameter should include the complete query string from the OAuth redirect.
|
||||
* - The callback URI is automatically constructed using the APP_DOMAIN environment variable.
|
||||
* - Ensure APP_DOMAIN is properly configured in your environment variables.
|
||||
*/
|
||||
export const SendCallbackToServer = async (
|
||||
data: string,
|
||||
provider: string
|
||||
): Promise<ServerRequestCallback> => {
|
||||
// Construct the backend and frontend handler URLs
|
||||
const backendHandlerUrl = `auth/${provider}/callback/`;
|
||||
const frontendHandlerUrl = `${process.env
|
||||
.APP_DOMAIN!}/auth/callback/${provider}`;
|
||||
|
||||
try {
|
||||
// Forward the OAuth callback data to the backend for processing
|
||||
const response = await api.get(
|
||||
`${backendHandlerUrl}${data}&callbackURI=${frontendHandlerUrl}`
|
||||
);
|
||||
|
||||
// Parse the JSON response from the backend and return the result
|
||||
const result = await response.json();
|
||||
return {
|
||||
success: true,
|
||||
status: response.status,
|
||||
text: { message: "Callback processed successfully" },
|
||||
data: result,
|
||||
};
|
||||
} catch (error) {
|
||||
return apiErrorHandler(error);
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,56 +1,56 @@
|
||||
"use client";
|
||||
|
||||
import { redirect } from "next/navigation";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import SecurityCheckup from "@/shared/auth/ui/SecurityCheckup";
|
||||
import SecurityCheckupFailed from "@/shared/auth/ui/SecurityCheckupFailed";
|
||||
import LoadingProcess from "../ui/LoadingProcess";
|
||||
|
||||
const OauthCallbackHandler = () => {
|
||||
/**
|
||||
* Create a lit component that will be used in popp, consisting of 3 component flows:
|
||||
* 1. When the user opens it, a browser environment check will be performed.
|
||||
* 2. If it passes, the login component will appear and the user will perform authentication.
|
||||
* 3. If it fails, stop the authentication process and display the warning component, then return the user to the homepage.
|
||||
*/
|
||||
const componentFlowList = {
|
||||
securityCheckup: <SecurityCheckup />,
|
||||
securityCheckupFailed: <SecurityCheckupFailed />,
|
||||
proceedCallback: <LoadingProcess />,
|
||||
};
|
||||
|
||||
// State to set the current page component
|
||||
const [componentFlow, setComponentFlow] = useState(
|
||||
componentFlowList.securityCheckup
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Prevent opening devtools while in authentication page
|
||||
// disableDevtool();
|
||||
|
||||
/**
|
||||
* Check if the window has an opener (i.e., it was opened by another window)
|
||||
* If it does, the security checkup has passed.
|
||||
* If it doesn't, the security checkup has failed and user will be redirected to the homepage.
|
||||
*/
|
||||
if (window.opener) {
|
||||
setComponentFlow(componentFlowList.proceedCallback);
|
||||
} else {
|
||||
setComponentFlow(componentFlowList.securityCheckupFailed);
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
redirect("/");
|
||||
}, 5000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* show the current component flow */}
|
||||
<main>{componentFlow}</main>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default OauthCallbackHandler;
|
||||
"use client";
|
||||
|
||||
import { redirect } from "next/navigation";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import SecurityCheckup from "@/shared/auth/ui/SecurityCheckup";
|
||||
import SecurityCheckupFailed from "@/shared/auth/ui/SecurityCheckupFailed";
|
||||
import LoadingProcess from "../ui/LoadingProcess";
|
||||
|
||||
const OauthCallbackHandler = () => {
|
||||
/**
|
||||
* Create a lit component that will be used in popp, consisting of 3 component flows:
|
||||
* 1. When the user opens it, a browser environment check will be performed.
|
||||
* 2. If it passes, the login component will appear and the user will perform authentication.
|
||||
* 3. If it fails, stop the authentication process and display the warning component, then return the user to the homepage.
|
||||
*/
|
||||
const componentFlowList = {
|
||||
securityCheckup: <SecurityCheckup />,
|
||||
securityCheckupFailed: <SecurityCheckupFailed />,
|
||||
proceedCallback: <LoadingProcess />,
|
||||
};
|
||||
|
||||
// State to set the current page component
|
||||
const [componentFlow, setComponentFlow] = useState(
|
||||
componentFlowList.securityCheckup
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Prevent opening devtools while in authentication page
|
||||
// disableDevtool();
|
||||
|
||||
/**
|
||||
* Check if the window has an opener (i.e., it was opened by another window)
|
||||
* If it does, the security checkup has passed.
|
||||
* If it doesn't, the security checkup has failed and user will be redirected to the homepage.
|
||||
*/
|
||||
if (window.opener) {
|
||||
setComponentFlow(componentFlowList.proceedCallback);
|
||||
} else {
|
||||
setComponentFlow(componentFlowList.securityCheckupFailed);
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
redirect("/");
|
||||
}, 5000);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* show the current component flow */}
|
||||
<main>{componentFlow}</main>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default OauthCallbackHandler;
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
export interface ParamProps {
|
||||
params: { provider: string[] };
|
||||
searchParams:
|
||||
| string
|
||||
| string[][]
|
||||
| Record<string, string>
|
||||
| URLSearchParams
|
||||
| undefined;
|
||||
}
|
||||
export interface ParamProps {
|
||||
params: { provider: string[] };
|
||||
searchParams:
|
||||
| string
|
||||
| string[][]
|
||||
| Record<string, string>
|
||||
| URLSearchParams
|
||||
| undefined;
|
||||
}
|
||||
|
||||
@ -1,73 +1,73 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { addToast, Button, CircularProgress, Link } from "@heroui/react";
|
||||
import { SendCallbackToServer } from "../lib/sendCallbackToServer";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useRunOnce } from "@/shared/hooks/useRunOnce";
|
||||
import { routes } from "@/shared/config/routes";
|
||||
|
||||
const LoadingProcess = () => {
|
||||
// Access the URL parameters
|
||||
const params = useParams();
|
||||
|
||||
// Forward the callback response to the backend server
|
||||
useRunOnce("forwardCallbackResponseToBackend", async () => {
|
||||
try {
|
||||
const response = await SendCallbackToServer(
|
||||
window.location.search,
|
||||
params.provider as string
|
||||
);
|
||||
|
||||
if (response.success) {
|
||||
window.close();
|
||||
} else {
|
||||
addToast({
|
||||
title: "😬 Oops, there's a problem!",
|
||||
description: response.text.message,
|
||||
color: "danger",
|
||||
timeout: 0,
|
||||
endContent: (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
onPress={() => (window.location.href = routes.login)}
|
||||
>
|
||||
Try again
|
||||
</Button>
|
||||
),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
addToast({
|
||||
title: "😵💫 Oops, lost connection!",
|
||||
description: "Check your internet and try again",
|
||||
color: "danger",
|
||||
timeout: 0,
|
||||
endContent: (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
onPress={() => (window.location.href = routes.login)}
|
||||
>
|
||||
Reload
|
||||
</Button>
|
||||
),
|
||||
});
|
||||
}
|
||||
});
|
||||
return (
|
||||
<div className="w-full flex flex-col items-center text-center mt-[26vh]">
|
||||
<CircularProgress aria-label="Loading..." size="lg" />
|
||||
<div className="mt-4">
|
||||
<h1 className="text-lg text-neutral-200">Please wait...</h1>
|
||||
<p className="text-sm text-neutral-400">
|
||||
Your request is being processed
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadingProcess;
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { addToast, Button, CircularProgress, Link } from "@heroui/react";
|
||||
import { SendCallbackToServer } from "../lib/sendCallbackToServer";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useRunOnce } from "@/shared/hooks/useRunOnce";
|
||||
import { routes } from "@/shared/config/routes";
|
||||
|
||||
const LoadingProcess = () => {
|
||||
// Access the URL parameters
|
||||
const params = useParams();
|
||||
|
||||
// Forward the callback response to the backend server
|
||||
useRunOnce("forwardCallbackResponseToBackend", async () => {
|
||||
try {
|
||||
const response = await SendCallbackToServer(
|
||||
window.location.search,
|
||||
params.provider as string
|
||||
);
|
||||
|
||||
if (response.success) {
|
||||
window.close();
|
||||
} else {
|
||||
addToast({
|
||||
title: "😬 Oops, there's a problem!",
|
||||
description: response.text.message,
|
||||
color: "danger",
|
||||
timeout: 0,
|
||||
endContent: (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
onPress={() => (window.location.href = routes.login)}
|
||||
>
|
||||
Try again
|
||||
</Button>
|
||||
),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
addToast({
|
||||
title: "😵💫 Oops, lost connection!",
|
||||
description: "Check your internet and try again",
|
||||
color: "danger",
|
||||
timeout: 0,
|
||||
endContent: (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="flat"
|
||||
onPress={() => (window.location.href = routes.login)}
|
||||
>
|
||||
Reload
|
||||
</Button>
|
||||
),
|
||||
});
|
||||
}
|
||||
});
|
||||
return (
|
||||
<div className="w-full flex flex-col items-center text-center mt-[26vh]">
|
||||
<CircularProgress aria-label="Loading..." size="lg" />
|
||||
<div className="mt-4">
|
||||
<h1 className="text-lg text-neutral-200">Please wait...</h1>
|
||||
<p className="text-sm text-neutral-400">
|
||||
Your request is being processed
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoadingProcess;
|
||||
|
||||
Reference in New Issue
Block a user