Skip to content

Commit

Permalink
Merge pull request #692 from wri/main
Browse files Browse the repository at this point in the history
[MERGE] main -> staging post SS hotfix release.
  • Loading branch information
roguenet authored Nov 20, 2024
2 parents 729373e + ced7725 commit d55f742
Show file tree
Hide file tree
Showing 12 changed files with 242 additions and 144 deletions.
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 @@ -56,22 +55,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 @@ -84,21 +68,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 @@ -110,6 +84,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

0 comments on commit d55f742

Please sign in to comment.