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

feat(role based view): implemented a role based view for user groups #175

Merged
merged 11 commits into from
Nov 2, 2023
Merged
23 changes: 22 additions & 1 deletion src/services/auth/libs/users.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
]
},
{
"username": "onemac-micro-[email protected]",
"username": "[email protected]",
"attributes": [
{
"Name": "email",
Expand Down Expand Up @@ -172,5 +172,26 @@
"Value": "onemac-micro-statesubmitter"
}
]
},
{
"username": "[email protected]",
"attributes": [
{
"Name": "email",
"Value": "[email protected]"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bad football? This must be Sheffield United's email address 😂

},
{
"Name": "given_name",
"Value": "bad"
},
{
"Name": "family_name",
"Value": "football"
},
{
"Name": "email_verified",
"Value": "true"
}
]
}
]
10 changes: 6 additions & 4 deletions src/services/ui/src/api/useGetUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { Auth } from "aws-amplify";
import { CognitoUserAttributes } from "shared-types";
import { isCmsUser } from "shared-utils";

export const getUser = async () => {
export type OneMacUser = { isCms?: boolean, user: CognitoUserAttributes | null }

export const getUser = async (): Promise<OneMacUser> => {
try {
const authenticatedUser = await Auth.currentAuthenticatedUser();
const attributes = await Auth.userAttributes(authenticatedUser);
Expand All @@ -14,14 +16,14 @@ export const getUser = async () => {
}, {}) as unknown as CognitoUserAttributes;
if (user["custom:cms-roles"]) {
const isCms = isCmsUser(user);
return { user, isCms };
return { user, isCms } satisfies OneMacUser;
} else {
user["custom:cms-roles"] = "";
return { user, isCms: false };
return { user, isCms: false } satisfies OneMacUser;
}
} catch (e) {
console.log({ e });
return { user: null };
return { user: null } satisfies OneMacUser;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

satisfies 😎 TypeScript boss right there!

}
};

Expand Down
14 changes: 14 additions & 0 deletions src/services/ui/src/components/Context/userContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { OneMacUser, useGetUser } from "@/api/useGetUser";
import { PropsWithChildren, createContext, useContext } from "react";

const initialState = { user: null };

export const UserContext = createContext<OneMacUser | undefined>(initialState);
export const UserContextProvider = ({ children }: PropsWithChildren) => {
const { data: userData } = useGetUser();
return (
<UserContext.Provider value={userData}>{children}</UserContext.Provider>
);
};

export const useUserContext = () => useContext(UserContext);
16 changes: 10 additions & 6 deletions src/services/ui/src/components/Layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@ import { Link, NavLink, NavLinkProps, Outlet } from "react-router-dom";
import oneMacLogo from "@/assets/onemac_logo.svg";
import { useMediaQuery } from "@/hooks";
import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/outline";
import { useState } from "react";
import { useMemo, useState } from "react";
import { useGetUser } from "@/api/useGetUser";
import { Auth } from "aws-amplify";
import { AwsCognitoOAuthOpts } from "@aws-amplify/auth/lib-esm/types";
import { Footer } from "../Footer";
import { UsaBanner } from "../UsaBanner";
import { FAQ_TARGET } from "@/routes";
import { useUserContext } from "../Context/userContext";

const getLinks = (isAuthenticated: boolean) => {
if (isAuthenticated) {
const getLinks = (isAuthenticated: boolean, role?: boolean) => {
if (isAuthenticated && role) {
return [
{
name: "Home",
Expand Down Expand Up @@ -83,14 +84,17 @@ const ResponsiveNav = ({ isDesktop }: ResponsiveNavProps) => {
const [prevMediaQuery, setPrevMediaQuery] = useState(isDesktop);
const [isOpen, setIsOpen] = useState(false);
const { isLoading, isError, data } = useGetUser();
const userContext = useUserContext();
const role = useMemo(() => {
return userContext?.user?.["custom:cms-roles"] ? true : false;
}, []);

const handleLogin = () => {
const authConfig = Auth.configure();
const { domain, redirectSignIn, responseType } =
authConfig.oauth as AwsCognitoOAuthOpts;
const clientId = authConfig.userPoolWebClientId;
const url = `https://${domain}/oauth2/authorize?redirect_uri=${redirectSignIn}&response_type=${responseType}&client_id=${clientId}`;

window.location.assign(url);
};

Expand All @@ -111,7 +115,7 @@ const ResponsiveNav = ({ isDesktop }: ResponsiveNavProps) => {
if (isDesktop) {
return (
<>
{getLinks(!!data.user).map((link) => (
{getLinks(!!data.user, role).map((link) => (
<NavLink
to={link.link}
target={link.link === "/faq" ? FAQ_TARGET : undefined}
Expand Down Expand Up @@ -149,7 +153,7 @@ const ResponsiveNav = ({ isDesktop }: ResponsiveNavProps) => {
{isOpen && (
<div className="w-full fixed top-[100px] left-0 z-50">
<ul className="font-medium flex flex-col p-4 md:p-0 mt-2 gap-4 rounded-lg bg-accent">
{getLinks(!!data.user).map((link) => (
{getLinks(!!data.user, role).map((link) => (
<li key={link.link}>
<Link
className="block py-2 pl-3 pr-4 text-white rounded"
Expand Down
159 changes: 96 additions & 63 deletions src/services/ui/src/components/UsaBanner/index.tsx
Original file line number Diff line number Diff line change
@@ -1,86 +1,119 @@
import { useState } from "react";
import { useMemo, useState } from "react";
import { ChevronDown, ChevronUp } from "lucide-react";
import { LockIcon } from "../LockIcon";
import { GovernmentBuildingIcon } from "../GovernmentBuildingIcon";
import UsFlag from "@/assets/us_flag_small.png";
import { useMediaQuery } from "@/hooks";
import { useUserContext } from "../Context/userContext";

export const UsaBanner = () => {
const [isOpen, setIsOpen] = useState(false);
const isDesktop = useMediaQuery("(min-width: 640px)");
const userContext = useUserContext();
const role = useMemo(() => {
return userContext?.user?.["custom:cms-roles"] ? false : true;
}, []);

const hasRole = useMemo(() => {
if (role && userContext?.user) {
return true;
} else {
return false;
}
}, []);
return (
<div className="bg-[#f0f0f0]">
{/* Display for Desktop */}
{isDesktop && (
<>
<div className="max-w-screen-xl px-4 py-1 lg:px-8 text-xs mx-auto flex gap-2 items-center">
<>
<div className="bg-[#f0f0f0]">
{/* Display for Desktop */}
{isDesktop && (
<>
<div className="max-w-screen-xl px-4 py-1 lg:px-8 text-xs mx-auto flex gap-2 items-center">
<img
className="w-4 h-[11px]"
src={UsFlag}
alt="A United States Flag icon"
/>
<p>An official website of the United States government</p>
<button
className="flex"
aria-expanded={isOpen}
aria-controls="gov-banner-default-default"
onClick={() => setIsOpen((value) => !value)}
>
<span className="underline text-[#005ea2]">
Here&apos;s how you know
</span>
{!isOpen && <ChevronDown className="w-4 h-4 text-[#005ea2]" />}
{isOpen && <ChevronUp className="w-4 h-4 text-[#005ea2]" />}
</button>
</div>
</>
)}
{/* Display for Mobile */}
{!isDesktop && (
<button
className="w-full flex items-center text-[0.8rem] px-4 py-1 leading-4 gap-2"
onClick={() => setIsOpen((value) => !value)}
>
<img
className="w-4 h-[11px]"
src={UsFlag}
alt="A United States Flag icon"
/>
<p>An official website of the United States government</p>
<button
className="flex"
aria-expanded={isOpen}
aria-controls="gov-banner-default-default"
onClick={() => setIsOpen((value) => !value)}
>
<span className="underline text-[#005ea2]">
Here&apos;s how you know
</span>
{!isOpen && <ChevronDown className="w-4 h-4 text-[#005ea2]" />}
{isOpen && <ChevronUp className="w-4 h-4 text-[#005ea2]" />}
</button>
</div>
</>
)}
{/* Display for Mobile */}
{!isDesktop && (
<button
className="w-full flex items-center text-[0.8rem] px-4 py-1 leading-4 gap-2"
onClick={() => setIsOpen((value) => !value)}
>
<img
className="w-4 h-[11px]"
src={UsFlag}
alt="A United States Flag icon"
/>
<div>
<p>An official website of the United States government</p>
<div className="flex" aria-expanded={isOpen}>
<span className="underline text-[#005ea2] block">
Here&apos;s how you know
</span>
{!isOpen && <ChevronDown className="w-4 h-4 text-[#005ea2]" />}
{isOpen && <ChevronUp className="w-4 h-4 text-[#005ea2]" />}
<div>
<p>An official website of the United States government</p>
<div className="flex" aria-expanded={isOpen}>
<span className="underline text-[#005ea2] block">
Here&apos;s how you know
</span>
{!isOpen && <ChevronDown className="w-4 h-4 text-[#005ea2]" />}
{isOpen && <ChevronUp className="w-4 h-4 text-[#005ea2]" />}
</div>
</div>
</div>
</button>
)}
{isOpen && (
<div className="flex flex-col gap-3 px-3 mt-3 sm:flex-row max-w-screen-lg mx-auto pb-4">
<div className="flex gap-2">
<GovernmentBuildingIcon className="min-w-[40px] min-h-[40px] w-10" />
<p className="text-sm max-w-md">
<strong className="block">Official websites use .gov</strong>A
<strong>.gov</strong> website belongs to an official government
organization in the United States.
</button>
)}
{hasRole && (
<div className="w-full px-4 py-1 lg:px-8 text-xs mx-auto flex gap-2 items-center justify-center bg-red-200 ">
<p className="text-center text-base">
You do not have access to view the application
<a
rel="noreferrer"
href="https://home.idm.cms.gov/signin/login.html"
target="_blank"
className="text-blue-600 inline no-underline"
>
Please visit IDM
</a>{" "}
to request the appropriate user role(s) - FAIL
</p>
</div>
<div className="flex gap-2">
<LockIcon className="min-w-[40px] min-h-[40px] w-10" />
<p className="text-sm max-w-md">
<strong className="block">Secure .gov websites use HTTPS</strong>A
lock (<MiniLock />) or <strong>https://</strong> means you&apos;ve
safely connected to the .gov website. Share sensitive information
only on official, secure websites.
</p>
)}

{isOpen && (
<div className="flex flex-col gap-3 px-3 mt-3 sm:flex-row max-w-screen-lg mx-auto pb-4">
<div className="flex gap-2">
<GovernmentBuildingIcon className="min-w-[40px] min-h-[40px] w-10" />
<p className="text-sm max-w-md">
<strong className="block">Official websites use .gov</strong>A
<strong>.gov</strong> website belongs to an official government
organization in the United States.
</p>
</div>
<div className="flex gap-2">
<LockIcon className="min-w-[40px] min-h-[40px] w-10" />
<p className="text-sm max-w-md">
<strong className="block">
Secure .gov websites use HTTPS
</strong>
A lock (<MiniLock />) or <strong>https://</strong> means
you&apos;ve safely connected to the .gov website. Share
sensitive information only on official, secure websites.
</p>
</div>
</div>
</div>
)}
</div>
)}
</div>
</>
);
};

Expand Down
5 changes: 4 additions & 1 deletion src/services/ui/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ import "./index.css"; // this one second
import { queryClient, router } from "./router";
import { QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { UserContextProvider } from "./components/Context/userContext";

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
<UserContextProvider>
<RouterProvider router={router} />
</UserContextProvider>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</React.StrictMode>
Expand Down
6 changes: 3 additions & 3 deletions src/services/ui/src/pages/dashboard/Lists/spas/consts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const TABLE_COLUMNS = (props?: { isCms?: boolean }): OsTableColumn[] => [
{
field: "state.keyword",
label: "State",
visible: false,
visible: true,
cell: (data) => data.state,
},
{
Expand All @@ -43,9 +43,9 @@ export const TABLE_COLUMNS = (props?: { isCms?: boolean }): OsTableColumn[] => [
: BLANK_VALUE,
},
{
field: props?.isCms ? "cmsStatus.keyword" : "stateStatus.keyword",
field: props?.isCms ? "stateStatus.keyword" : "stateStatus.keyword",
label: "Status",
cell: (data) => (props?.isCms ? data.cmsStatus : data.stateStatus),
cell: (data) => (props?.isCms ? data.stateStatus : data.stateStatus),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lines 46 and 48 here are both saying "If the user is CMS, show them the stateStatus, else, show them the stateStatus". Why wouldn't we want to show them the CMS value if they're CMS?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on what we talked about, I think the check should be more like

(props.isCms && !props.user.[cms-roles].includes(UserRole.HELPDESK) ? cmsStatus : stateStatus

That way we still show CMS statuses to everyone at CMS except the helpdesk as the AC says

},
{
field: "submissionDate",
Expand Down
6 changes: 3 additions & 3 deletions src/services/ui/src/pages/dashboard/Lists/waivers/consts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const TABLE_COLUMNS = (props?: { isCms?: boolean }): OsTableColumn[] => [
{
field: "state.keyword",
label: "State",
visible: false,
visible: true,
cell: (data) => data.state,
},
{
Expand All @@ -44,9 +44,9 @@ export const TABLE_COLUMNS = (props?: { isCms?: boolean }): OsTableColumn[] => [
: BLANK_VALUE,
},
{
field: props?.isCms ? "cmsStatus.keyword" : "stateStatus.keyword",
field: props?.isCms ? "stateStatus.keyword" : "stateStatus.keyword",
label: "Status",
cell: (data) => (props?.isCms ? data.cmsStatus : data.stateStatus),
cell: (data) => (props?.isCms ? data.stateStatus : data.stateStatus),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

},
{
field: "submissionDate",
Expand Down
Loading