Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MERGE] main -> staging post SS hotfix release. #692

Merged
merged 9 commits into from
Nov 20, 2024
21 changes: 18 additions & 3 deletions src/components/extensive/BlurContainer/BlurContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Text from "@/components/elements/Text/Text";

export interface BlurContainerProps {
isBlur: boolean;
textInformation?: string;
textInformation?: string | React.ReactNode;
children: React.ReactNode;
className?: string;
}
Expand All @@ -16,16 +16,31 @@ const BlurContainer = ({ isBlur, textInformation, children, className, ...props
return <>{children}</>;
}

const LoginText = () => (
<>
<a href="/auth/login" className="text-12-semibold underline">
Login to view.
</a>{" "}
If you are a funder or representing a government agency, please{" "}
<a
href="https://terramatchsupport.zendesk.com/hc/en-us/requests/new?ticket_form_id=30623040820123&tf_subject=Account%20Access%20Request%20for%20TerraMatch%20Dashboard&tf_description=Please%20provide%20your%20details%20to%20request%20access%20to%20the%20TerraMatch%20Dashboard.%20Once%20your%20information%20is%20submitted,%20our%20team%20will%20review%20your%20request%20and%20set%20up%20an%20account%20for%20you.%20You%E2%80%99ll%20receive%20an%20email%20with%20further%20instructions%20once%20your%20account%20is%20ready"
className="text-12-semibold underline"
>
click here to request an account.
</a>
</>
);

return (
<div className={tw("relative w-full text-black", className)}>
<div
className={`absolute left-0 top-0 flex h-full w-full items-center justify-center rounded-xl ${
isBlur ? "z-[1] bg-[#9d9a9a29] backdrop-blur-sm" : ""
}`}
>
<When condition={isBlur && textInformation}>
<When condition={isBlur && !!textInformation}>
<Text variant="text-12-semibold" className="h-fit w-fit max-w-[80%] rounded-lg bg-white px-4 py-3">
{textInformation}
{typeof textInformation === "string" ? textInformation : <LoginText />}
</Text>
</When>
</div>
Expand Down
4 changes: 3 additions & 1 deletion src/components/extensive/PageElements/Card/PageCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import EmptyField, { EmptyFieldProps } from "@/components/elements/Field/EmptyFi
import Paper from "@/components/elements/Paper/Paper";
import Text from "@/components/elements/Text/Text";
import ToolTip from "@/components/elements/Tooltip/Tooltip";
import { useMyUser } from "@/connections/User";
import { NO_DATA_INFORMATION } from "@/constants/dashboardConsts";
import { withFrameworkShow } from "@/context/framework.provider";
import { TextVariants } from "@/types/common";
Expand Down Expand Up @@ -55,6 +56,7 @@ const PageCard = ({
const [collapseSubtile, setCollapseSubtile] = useState(true);
const [subtitleText, setSubtitleText] = useState(subtitle);
const t = useT();
const [, { user }] = useMyUser();

const maxLength = 278;

Expand All @@ -68,7 +70,7 @@ const PageCard = ({

return (
<Paper {...props}>
<BlurContainer isBlur={!isUserAllowed} textInformation={NO_DATA_INFORMATION}>
<BlurContainer isBlur={!isUserAllowed} textInformation={user !== undefined ? NO_DATA_INFORMATION : <></>}>
<When condition={!!title || !!headerChildren}>
<div className="flex flex-wrap justify-between">
<When condition={!!title}>
Expand Down
9 changes: 7 additions & 2 deletions src/components/generic/Sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@ import { MENU_PLACEMENT_RIGHT_TOP } from "@/components/elements/Menu/MenuVariant
import Text from "@/components/elements/Text/Text";
import Tooltip from "@/components/elements/Tooltip/Tooltip";
import Icon, { IconNames } from "@/components/extensive/Icon/Icon";
import { useLogin } from "@/connections/Login";
import { useLogout } from "@/hooks/logout";

const Sidebar = () => {
const router = useRouter();
const logout = useLogout();
const [, { isLoggedIn }] = useLogin();

const t = useT();

return (
Expand Down Expand Up @@ -81,8 +86,8 @@ const Sidebar = () => {
{
id: "1",
render: () => (
<Text variant="text-14" className="flex items-center">
{t("Sign out")}
<Text variant="text-14" className="flex cursor-pointer items-center" onClick={logout}>
{isLoggedIn ? t("Sign out") : t("Sign in")}
</Text>
)
}
Expand Down
174 changes: 96 additions & 78 deletions src/middleware.page.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as Sentry from "@sentry/nextjs";
import { NextRequest } from "next/server";
import { NextRequest, NextResponse } from "next/server";

import { isAdmin, UserRole } from "@/admin/apiProvider/utils/user";
import { OrganisationDto, UserDto } from "@/generated/v3/userService/userServiceSchemas";
Expand All @@ -14,90 +14,108 @@ export async function middleware(request: NextRequest) {
const accessToken = request.cookies.get("accessToken")?.value;
const middlewareCache = request.cookies.get(MiddlewareCacheKey)?.value;

if (!!accessToken && !!middlewareCache) {
//If middleware result is cached bypass api call to improve performance
matcher.when(middlewareCache.includes("admin"))?.redirect(middlewareCache);
matcher.exact("/")?.redirect(middlewareCache);
matcher.startWith("/auth")?.redirect(middlewareCache);
matcher.next();
// Allow unauthenticated access to dashboard routes
if (request.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.next();
}

if (accessToken && middlewareCache) {
// Only redirect for specific routes when cached middleware exists
const shouldRedirect = request.nextUrl.pathname === "/" || request.nextUrl.pathname.startsWith("/auth");

if (shouldRedirect) {
matcher.redirect(middlewareCache);
return matcher.getResult();
}
return NextResponse.next();
}

if (!accessToken) {
matcher.startWith("/home")?.redirect("/");
matcher.startWith("/auth")?.next();
matcher.exact("/")?.next();

if (!matcher.hasMatch()) {
matcher.redirect("/auth/login");
}
return matcher.getResult();
}

await matcher.if(
!accessToken,
async () => {
//Not logged-in
// The redux store isn't available yet at this point, so we do a quick manual users/me fetch
// to get the data we need to resolve routing.
const result = await fetch(resolveUrl("/users/v3/users/me"), {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`
}
});
const json = await result.json();

const user = json.data.attributes as UserDto;
const {
id: organisationId,
meta: { userStatus }
} = json.data.relationships?.org?.data ?? { meta: {} };
const organisation: OrganisationDto | undefined = json.included?.[0]?.attributes;

// Early return for unverified email
if (!user?.emailAddressVerifiedAt) {
matcher.redirect(`/auth/signup/confirm?email=${user?.emailAddress}`);
return matcher.getResult();
}

matcher.startWith("/home")?.redirect("/");
matcher.startWith("/auth")?.next();
matcher.exact("/")?.next();
const userIsAdmin = isAdmin(user?.primaryRole as UserRole);
const userIsFunderOrGovernment = user?.primaryRole === "funder" || user?.primaryRole === "government";

matcher.redirect("/auth/login");
},
async () => {
// The redux store isn't available yet at this point, so we do a quick manual users/me fetch
// to get the data we need to resolve routing.
const result = await fetch(resolveUrl("/users/v3/users/me"), {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${accessToken}`
}
});
const json = await result.json();

const user = json.data.attributes as UserDto;
const {
id: organisationId,
meta: { userStatus }
} = json.data.relationships?.org?.data ?? { meta: {} };
const organisation: OrganisationDto | undefined = json.included?.[0]?.attributes;

matcher.if(
!user?.emailAddressVerifiedAt,
() => {
//Email is not verified
matcher.redirect(`/auth/signup/confirm?email=${user?.emailAddress}`);
},
() => {
//Email is verified
const userIsAdmin = isAdmin(user?.primaryRole as UserRole);

matcher.when(user != null && userIsAdmin)?.redirect(`/admin`, { cacheResponse: true });

matcher
.when(organisation != null && organisation.status !== "draft")
?.startWith("/organization/create")
?.redirect(`/organization/create/confirm`);

matcher.when(organisation == null)?.redirect(`/organization/assign`);

matcher.when(organisation?.status === "draft")?.redirect(`/organization/create`);

matcher.when(userStatus === "requested")?.redirect(`/organization/status/pending`);

matcher
.when(organisationId != null)
?.exact("/organization")
?.redirect(`/organization/${organisationId}`);

matcher.when(organisation?.status === "rejected")?.redirect(`/organization/status/rejected`);

matcher.exact("/")?.redirect(`/home`);

matcher.startWith("/auth")?.redirect("/home");

if (!userIsAdmin && organisation?.status === "approved" && userStatus !== "requested") {
//Cache result if user has and approved org
matcher.next().cache("/home");
} else {
matcher.next();
}
}
);
// Always handle funder/government users first
if (userIsFunderOrGovernment) {
if (!request.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/dashboard/learn-more?tab=about-us", request.url));
}
return NextResponse.next();
}

// Handle admin users
if (userIsAdmin) {
// Allow admins to access dashboard routes
if (request.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.next();
}
);

// Default admin redirect for non-dashboard routes
matcher.redirect(`/admin`, { cacheResponse: true });
return matcher.getResult();
}

matcher
.when(organisation != null && organisation.status !== "draft")
?.startWith("/organization/create")
?.redirect(`/organization/create/confirm`);

matcher.when(organisation == null)?.redirect(`/organization/assign`);

matcher.when(organisation?.status === "draft")?.redirect(`/organization/create`);

matcher.when(userStatus === "requested")?.redirect(`/organization/status/pending`);

matcher
.when(organisationId != null)
?.exact("/organization")
?.redirect(`/organization/${organisationId}`);

matcher.when(organisation?.status === "rejected")?.redirect(`/organization/status/rejected`);

matcher.exact("/")?.redirect(`/home`);

matcher.startWith("/auth")?.redirect("/home");

if (organisation?.status === "approved" && userStatus !== "requested") {
// Cache result if user has an approved org
matcher.next().cache("/home");
} else {
matcher.next();
}
} catch (error) {
Sentry.captureException(error);
matcher.redirect("/"); //To be redirected to a custom error page
Expand Down
85 changes: 53 additions & 32 deletions src/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import App from "next/app";
import dynamic from "next/dynamic";
import { useRouter } from "next/router";
import nookies from "nookies";
import { Else, If, Then } from "react-if";
import { Provider as ReduxProvider } from "react-redux";

import Toast from "@/components/elements/Toast/Toast";
Expand Down Expand Up @@ -50,22 +49,7 @@ const _App = ({ Component, ...rest }: AppProps) => {
setClientSideTranslations(props);
setupYup(t);

if (isAdmin)
return (
<ReduxProvider store={store}>
<WrappedQueryClientProvider>
<LoadingProvider>
<NotificationProvider>
<ModalProvider>
<ModalRoot />
<Component {...pageProps} />
</ModalProvider>
</NotificationProvider>
</LoadingProvider>
</WrappedQueryClientProvider>
</ReduxProvider>
);
else
if (isOnDashboards) {
return (
<ReduxProvider store={store}>
<ToastProvider>
Expand All @@ -78,21 +62,11 @@ const _App = ({ Component, ...rest }: AppProps) => {
<NavbarProvider>
<ModalRoot />
<Toast />
<If condition={isOnDashboards}>
<Then>
<DashboardAnalyticsWrapper>
<DashboardLayout>
<Component {...pageProps} />
</DashboardLayout>
</DashboardAnalyticsWrapper>
</Then>
<Else>
<MainLayout>
<Component {...pageProps} />
<CookieBanner />
</MainLayout>
</Else>
</If>
<DashboardAnalyticsWrapper>
<DashboardLayout>
<Component {...pageProps} />
</DashboardLayout>
</DashboardAnalyticsWrapper>
</NavbarProvider>
</ModalProvider>
</NotificationProvider>
Expand All @@ -104,6 +78,53 @@ const _App = ({ Component, ...rest }: AppProps) => {
</ToastProvider>
</ReduxProvider>
);
}

// For admin pages (not dashboard)
if (isAdmin) {
return (
<ReduxProvider store={store}>
<WrappedQueryClientProvider>
<LoadingProvider>
<NotificationProvider>
<ModalProvider>
<ModalRoot />
<Component {...pageProps} />
</ModalProvider>
</NotificationProvider>
</LoadingProvider>
</WrappedQueryClientProvider>
</ReduxProvider>
);
}

return (
<ReduxProvider store={store}>
<ToastProvider>
<WrappedQueryClientProvider>
<Hydrate state={pageProps.dehydratedState}>
<RouteHistoryProvider>
<LoadingProvider>
<NotificationProvider>
<ModalProvider>
<NavbarProvider>
<ModalRoot />
<Toast />
<MainLayout>
<Component {...pageProps} />
<CookieBanner />
</MainLayout>
</NavbarProvider>
</ModalProvider>
</NotificationProvider>
</LoadingProvider>
</RouteHistoryProvider>
</Hydrate>
<ReactQueryDevtools initialIsOpen={false} />
</WrappedQueryClientProvider>
</ToastProvider>
</ReduxProvider>
);
};

_App.getInitialProps = wrapper.getInitialAppProps(store => async (context: AppContext) => {
Expand Down
Loading