From 8002c8d76cd991649d7e91b71ec11dd4e90d3c53 Mon Sep 17 00:00:00 2001 From: Robinson-Taiwo Date: Fri, 16 Aug 2024 19:44:16 +0100 Subject: [PATCH 1/2] Fix: implement links generation --- src/actions/inviteMembers.ts | 81 ++++++++++++++++ src/app/invite/page.tsx | 97 +++++++++++++++++++ .../common/modals/invite-member/index.tsx | 63 +++++++++--- 3 files changed, 225 insertions(+), 16 deletions(-) create mode 100644 src/app/invite/page.tsx diff --git a/src/actions/inviteMembers.ts b/src/actions/inviteMembers.ts index 2856a9c0d..db484c5d2 100644 --- a/src/actions/inviteMembers.ts +++ b/src/actions/inviteMembers.ts @@ -88,3 +88,84 @@ export const fetchOrganizations = async () => { }; } }; + +export const acceptInvite = async (inviteLink: string) => { + const apiUrl = await getApiUrl(); + const session = await auth(); + + // Extract the token from the invite link + const token = inviteLink.split("?")[1]; // Assuming the token is the only query parameter + + if (!token) { + return { + error: "Invalid invite link. No token found.", + }; + } + + try { + const response = await axios.post( + `${apiUrl}/api/v1/organisations/invites/accept`, + { + invite_token_guid: token, + }, + { + headers: { + Authorization: `Bearer ${session?.access_token}`, + }, + }, + ); + + // Handle the response as needed + return { + data: response.data, + status: response.status, + }; + } catch (error) { + return axios.isAxiosError(error) && error.response + ? { + error: error.response.data.message || "Failed to accept invite.", + status: error.response.status, + } + : { + error: "An unexpected error occurred.", + }; + } +}; + +export const generateInviteLink = async ( + org_id: string, + invite_token: string, +) => { + const apiUrl = await getApiUrl(); + const session = await auth(); + + try { + const response = await axios.get( + `${apiUrl}/api/v1/organisations/${org_id}/invites`, + { + params: { invite_token }, + headers: { + Authorization: `Bearer ${session?.access_token}`, + }, + }, + ); + + // Extract the invite link from the nested data object + const inviteLink = response.data.data.invite_link; + + return { + data: inviteLink, + status: response.status, + }; + } catch (error) { + return axios.isAxiosError(error) && error.response + ? { + error: + error.response.data.message || "Failed to generate invite link.", + status: error.response.status, + } + : { + error: "An unexpected error occurred.", + }; + } +}; diff --git a/src/app/invite/page.tsx b/src/app/invite/page.tsx new file mode 100644 index 000000000..7ab942fa0 --- /dev/null +++ b/src/app/invite/page.tsx @@ -0,0 +1,97 @@ +import { GetServerSideProps } from "next"; +import { useRouter } from "next/router"; +import { useEffect } from "react"; + +import { acceptInvite } from "~/actions/inviteMembers"; + +const AcceptInvitePage = ({ + valid, + statusCode, + message, +}: { + valid: boolean; + statusCode: number; + message: string; +}) => { + const router = useRouter(); + + useEffect(() => { + if (valid) { + if (statusCode === 200) { + // Redirect to login page if the user was successfully added + router.push("/login"); + } else if (statusCode === 202) { + // Redirect to registration page if the invite requires registration + router.push("/register"); + } + } + }, [valid, statusCode, router]); + + return ( +
+ {valid ? ( +
Processing invite... Redirecting...
+ ) : ( +
Error: {message}
+ )} +
+ ); +}; + +// This function runs server-side before the page is sent to the client +export const getServerSideProps: GetServerSideProps = async (context) => { + const { query } = context; + const token = Object.keys(query)[0]; // Extract the token directly from the URL's query string + + if (!token) { + return { + props: { + valid: false, + statusCode: 422, + message: "Invalid invite link.", + }, + }; + } + + const inviteLink = `http://localhost:3000/invite?${token}`; + + try { + const result = await acceptInvite(inviteLink); + + if (result.status === 200 || result.status === 202) { + return { + props: { + valid: true, + statusCode: result.status, + message: "Invite accepted successfully!", + }, + }; + } else if (result.status === 422) { + return { + props: { + valid: false, + statusCode: 422, + message: "Invalid invite code.", + }, + }; + } + + return { + props: { + valid: false, + statusCode: result.status, + message: "Unexpected response from the server.", + }, + }; + } catch { + return { + props: { + valid: false, + statusCode: 500, + message: "An error occurred while processing your invite.", + }, + }; + } +}; + +export default AcceptInvitePage; diff --git a/src/components/common/modals/invite-member/index.tsx b/src/components/common/modals/invite-member/index.tsx index eddb553db..f79ca57b9 100644 --- a/src/components/common/modals/invite-member/index.tsx +++ b/src/components/common/modals/invite-member/index.tsx @@ -1,10 +1,13 @@ "use client"; -import axios from "axios"; import { Link2Icon } from "lucide-react"; import { useCallback, useState } from "react"; -import { fetchOrganizations, inviteMembers } from "~/actions/inviteMembers"; +import { + fetchOrganizations, + generateInviteLink, + inviteMembers, +} from "~/actions/inviteMembers"; import CustomButton from "~/components/common/common-button/common-button"; import { Dialog, @@ -76,7 +79,7 @@ const InviteMemberModal: React.FC = ({ show, onClose }) => { } else { toast({ title: "Success", - description: "Your invite has been sent successfully to members email", + description: "Your invite has been sent successfully to members' email", variant: "default", }); setEmails(""); @@ -84,20 +87,47 @@ const InviteMemberModal: React.FC = ({ show, onClose }) => { } }; + const clearError = () => setTimeout(() => setError(""), 3000); const handleInviteWithLink = async () => { - try { - const response = await axios.post("/api/v1/invite/create"); - if (response.status === 200) { - setInviteLink(response.data.invite_link); - setLinkGenerated(true); - navigator.clipboard.writeText(response.data.invite_link); - toast({ - title: "Invite Link", - description: "Invite link copied to clipboard!", - }); + if (!organization) { + setError("Please select an organization first."); + clearError(); + return; + } + + const inviteResponse = await inviteMembers(emails, organization); + if (inviteResponse?.error) { + setError(inviteResponse.error); + clearError(); + return; + } + + const { data: inviteLinkData, error: inviteLinkError } = + await generateInviteLink(organization, inviteResponse.data.invite_token); + + if (inviteLinkError) { + setError(inviteLinkError); + clearError(); + } else { + setInviteLink(inviteLinkData); // Correctly store the invite link + setLinkGenerated(true); + + try { + // Ensure the document is focused before attempting to write to the clipboard + if (document.hasFocus()) { + await navigator.clipboard.writeText(inviteLinkData); + toast({ + title: "Invite Link", + description: "Invite link copied to clipboard!", + }); + } else { + setError("Failed to copy invite link. Please manually copy it."); + clearError(); + } + } catch { + setError("Failed to copy invite link to clipboard."); + clearError(); } - } catch { - setError("Failed to generate invite link."); } }; @@ -165,10 +195,11 @@ const InviteMemberModal: React.FC = ({ show, onClose }) => { {linkGenerated && ( -
+
Invite link: {inviteLink}
)} + {error &&
{error}
} From def184d84b9f07e7cfa2fdd3d78643dbda3344b7 Mon Sep 17 00:00:00 2001 From: Robinson-Taiwo Date: Fri, 16 Aug 2024 20:01:26 +0100 Subject: [PATCH 2/2] Fix: delete conflicting file --- src/app/invite/page.tsx | 97 ----------------------------------------- 1 file changed, 97 deletions(-) delete mode 100644 src/app/invite/page.tsx diff --git a/src/app/invite/page.tsx b/src/app/invite/page.tsx deleted file mode 100644 index 7ab942fa0..000000000 --- a/src/app/invite/page.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { GetServerSideProps } from "next"; -import { useRouter } from "next/router"; -import { useEffect } from "react"; - -import { acceptInvite } from "~/actions/inviteMembers"; - -const AcceptInvitePage = ({ - valid, - statusCode, - message, -}: { - valid: boolean; - statusCode: number; - message: string; -}) => { - const router = useRouter(); - - useEffect(() => { - if (valid) { - if (statusCode === 200) { - // Redirect to login page if the user was successfully added - router.push("/login"); - } else if (statusCode === 202) { - // Redirect to registration page if the invite requires registration - router.push("/register"); - } - } - }, [valid, statusCode, router]); - - return ( -
- {valid ? ( -
Processing invite... Redirecting...
- ) : ( -
Error: {message}
- )} -
- ); -}; - -// This function runs server-side before the page is sent to the client -export const getServerSideProps: GetServerSideProps = async (context) => { - const { query } = context; - const token = Object.keys(query)[0]; // Extract the token directly from the URL's query string - - if (!token) { - return { - props: { - valid: false, - statusCode: 422, - message: "Invalid invite link.", - }, - }; - } - - const inviteLink = `http://localhost:3000/invite?${token}`; - - try { - const result = await acceptInvite(inviteLink); - - if (result.status === 200 || result.status === 202) { - return { - props: { - valid: true, - statusCode: result.status, - message: "Invite accepted successfully!", - }, - }; - } else if (result.status === 422) { - return { - props: { - valid: false, - statusCode: 422, - message: "Invalid invite code.", - }, - }; - } - - return { - props: { - valid: false, - statusCode: result.status, - message: "Unexpected response from the server.", - }, - }; - } catch { - return { - props: { - valid: false, - statusCode: 500, - message: "An error occurred while processing your invite.", - }, - }; - } -}; - -export default AcceptInvitePage;