From e4fed315b842534052df4cb4cca3583bd8c1130d Mon Sep 17 00:00:00 2001 From: Mahmoud Moravej <55846417+mahmoudmoravej@users.noreply.github.com> Date: Sun, 25 Feb 2024 16:35:57 -0500 Subject: [PATCH] add authentication context + ssr (#57) * add authentication context + ssr * fix lint --- app/@types/graphql/schema.ts | 85 ++++++++-- .../apolloClientProvider.tsx} | 3 +- app/contexts/apollo/apolloServerProvider.tsx | 15 ++ ...r.tsx => AuthenticationClientProvider.tsx} | 12 +- .../AuthenticationServerProvider.tsx | 22 +++ .../authentication/authenticationContext.ts | 8 +- app/contexts/index.ts | 6 +- app/entry.client.tsx | 10 +- app/entry.server.tsx | 33 +++- app/models/user.ts | 3 +- app/routes/_auth.login.($id).tsx | 3 +- app/routes/_dashboard.cycles._index/route.tsx | 10 +- .../route.tsx | 95 +++++------ .../route.tsx | 10 +- .../route.tsx | 6 +- .../route.tsx | 23 +-- app/routes/_dashboard.tsx | 25 +-- .../_dashboard.visions._index/route.tsx | 13 +- app/routes/_site._index.tsx | 2 +- app/routes/auth.google.callback.tsx | 2 +- app/routes/auth.google.tsx | 2 +- app/routesData.tsx | 150 ++++++++++-------- app/services/auth.strategies/google.ts | 6 +- .../graphql/getLoggedInUserInfo.gql | 3 + app/widgets/layout/dashboard-navbar.tsx | 19 +-- app/widgets/layout/firstpage-footer.tsx | 1 + app/widgets/layout/sidenav.tsx | 30 +--- remix.env.d.ts | 5 +- 28 files changed, 341 insertions(+), 261 deletions(-) rename app/contexts/{appApollo/appApolloProvider.tsx => apollo/apolloClientProvider.tsx} (88%) create mode 100644 app/contexts/apollo/apolloServerProvider.tsx rename app/contexts/authentication/{authenticationProvider.tsx => AuthenticationClientProvider.tsx} (50%) create mode 100644 app/contexts/authentication/AuthenticationServerProvider.tsx diff --git a/app/@types/graphql/schema.ts b/app/@types/graphql/schema.ts index af22601..4717808 100644 --- a/app/@types/graphql/schema.ts +++ b/app/@types/graphql/schema.ts @@ -1059,7 +1059,7 @@ export type IndividualsQueryVariables = Exact<{ }>; -export type IndividualsQuery = { __typename?: 'Query', individuals: { __typename?: 'IndividualConnection', nodes?: Array<{ __typename?: 'Individual', id: number, fullname?: string | null, jobTitle?: string | null, jobLevelId?: string | null, isManager: boolean } | null> | null }, managerInfo: { __typename?: 'Individual', id: number, fullname?: string | null, jobTitle?: string | null } }; +export type IndividualsQuery = { __typename?: 'Query', individuals: { __typename?: 'IndividualConnection', nodes?: Array<{ __typename?: 'Individual', id: number, fullname?: string | null, jobTitle?: string | null, jobLevelId?: string | null, isManager: boolean } | null> | null }, managerInfo?: { __typename?: 'Individual', id: number, fullname?: string | null, jobTitle?: string | null } }; export type FindVisionQueryVariables = Exact<{ id: Scalars['ID']['input']; @@ -1106,7 +1106,7 @@ export type VisionFragmentFragment = { __typename?: 'Vision', id: number, vision export type GetLoggedInUserInfoQueryVariables = Exact<{ [key: string]: never; }>; -export type GetLoggedInUserInfoQuery = { __typename?: 'Query', myInfo: { __typename?: 'UserInfo', UserId: number, Individual?: { __typename?: 'Individual', id: number, isManager: boolean, organizationId: number } | null } }; +export type GetLoggedInUserInfoQuery = { __typename?: 'Query', myInfo: { __typename?: 'UserInfo', UserId: number, Individual?: { __typename?: 'Individual', id: number, isManager: boolean, organizationId: number } | null, Organization?: { __typename?: 'Organization', isPersonal: boolean } | null } }; export const ActivityFragmentFragmentDoc = gql` fragment ActivityFragment on Activity { @@ -1266,7 +1266,7 @@ export const FindActivityDocument = gql` * }, * }); */ -export function useFindActivityQuery(baseOptions: Apollo.QueryHookOptions) { +export function useFindActivityQuery(baseOptions: Apollo.QueryHookOptions & ({ variables: FindActivityQueryVariables; skip?: boolean; } | { skip: boolean; }) ) { const options = {...defaultOptions, ...baseOptions} return Apollo.useQuery(FindActivityDocument, options); } @@ -1274,8 +1274,13 @@ export function useFindActivityLazyQuery(baseOptions?: Apollo.LazyQueryHookOptio const options = {...defaultOptions, ...baseOptions} return Apollo.useLazyQuery(FindActivityDocument, options); } +export function useFindActivitySuspenseQuery(baseOptions?: Apollo.SuspenseQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useSuspenseQuery(FindActivityDocument, options); + } export type FindActivityQueryHookResult = ReturnType; export type FindActivityLazyQueryHookResult = ReturnType; +export type FindActivitySuspenseQueryHookResult = ReturnType; export type FindActivityQueryResult = Apollo.QueryResult; export const UpdateActivityDocument = gql` mutation UpdateActivity($input: ActivityUpdateInput!) { @@ -1336,7 +1341,7 @@ export const FindCycleDocument = gql` * }, * }); */ -export function useFindCycleQuery(baseOptions: Apollo.QueryHookOptions) { +export function useFindCycleQuery(baseOptions: Apollo.QueryHookOptions & ({ variables: FindCycleQueryVariables; skip?: boolean; } | { skip: boolean; }) ) { const options = {...defaultOptions, ...baseOptions} return Apollo.useQuery(FindCycleDocument, options); } @@ -1344,8 +1349,13 @@ export function useFindCycleLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions< const options = {...defaultOptions, ...baseOptions} return Apollo.useLazyQuery(FindCycleDocument, options); } +export function useFindCycleSuspenseQuery(baseOptions?: Apollo.SuspenseQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useSuspenseQuery(FindCycleDocument, options); + } export type FindCycleQueryHookResult = ReturnType; export type FindCycleLazyQueryHookResult = ReturnType; +export type FindCycleSuspenseQueryHookResult = ReturnType; export type FindCycleQueryResult = Apollo.QueryResult; export const UpdateCycleDocument = gql` mutation UpdateCycle($input: CycleUpdateInput!) { @@ -1418,8 +1428,13 @@ export function useCyclesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions(CyclesDocument, options); } +export function useCyclesSuspenseQuery(baseOptions?: Apollo.SuspenseQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useSuspenseQuery(CyclesDocument, options); + } export type CyclesQueryHookResult = ReturnType; export type CyclesLazyQueryHookResult = ReturnType; +export type CyclesSuspenseQueryHookResult = ReturnType; export type CyclesQueryResult = Apollo.QueryResult; export const CreateCycleDocument = gql` mutation createCycle($input: CycleCreateInput!) { @@ -1528,7 +1543,7 @@ ${AdviceFragmentFragmentDoc}`; * }, * }); */ -export function useCoachIndividualQuery(baseOptions: Apollo.QueryHookOptions) { +export function useCoachIndividualQuery(baseOptions: Apollo.QueryHookOptions & ({ variables: CoachIndividualQueryVariables; skip?: boolean; } | { skip: boolean; }) ) { const options = {...defaultOptions, ...baseOptions} return Apollo.useQuery(CoachIndividualDocument, options); } @@ -1536,8 +1551,13 @@ export function useCoachIndividualLazyQuery(baseOptions?: Apollo.LazyQueryHookOp const options = {...defaultOptions, ...baseOptions} return Apollo.useLazyQuery(CoachIndividualDocument, options); } +export function useCoachIndividualSuspenseQuery(baseOptions?: Apollo.SuspenseQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useSuspenseQuery(CoachIndividualDocument, options); + } export type CoachIndividualQueryHookResult = ReturnType; export type CoachIndividualLazyQueryHookResult = ReturnType; +export type CoachIndividualSuspenseQueryHookResult = ReturnType; export type CoachIndividualQueryResult = Apollo.QueryResult; export const FindIndividualDocument = gql` query findIndividual($id: ID!) { @@ -1569,7 +1589,7 @@ export const FindIndividualDocument = gql` * }, * }); */ -export function useFindIndividualQuery(baseOptions: Apollo.QueryHookOptions) { +export function useFindIndividualQuery(baseOptions: Apollo.QueryHookOptions & ({ variables: FindIndividualQueryVariables; skip?: boolean; } | { skip: boolean; }) ) { const options = {...defaultOptions, ...baseOptions} return Apollo.useQuery(FindIndividualDocument, options); } @@ -1577,8 +1597,13 @@ export function useFindIndividualLazyQuery(baseOptions?: Apollo.LazyQueryHookOpt const options = {...defaultOptions, ...baseOptions} return Apollo.useLazyQuery(FindIndividualDocument, options); } +export function useFindIndividualSuspenseQuery(baseOptions?: Apollo.SuspenseQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useSuspenseQuery(FindIndividualDocument, options); + } export type FindIndividualQueryHookResult = ReturnType; export type FindIndividualLazyQueryHookResult = ReturnType; +export type FindIndividualSuspenseQueryHookResult = ReturnType; export type FindIndividualQueryResult = Apollo.QueryResult; export const UpdateIndividualDocument = gql` mutation UpdateIndividual($input: IndividualUpdateInput!) { @@ -1743,7 +1768,7 @@ export const IndividualActivitiesDocument = gql` * }, * }); */ -export function useIndividualActivitiesQuery(baseOptions: Apollo.QueryHookOptions) { +export function useIndividualActivitiesQuery(baseOptions: Apollo.QueryHookOptions & ({ variables: IndividualActivitiesQueryVariables; skip?: boolean; } | { skip: boolean; }) ) { const options = {...defaultOptions, ...baseOptions} return Apollo.useQuery(IndividualActivitiesDocument, options); } @@ -1751,8 +1776,13 @@ export function useIndividualActivitiesLazyQuery(baseOptions?: Apollo.LazyQueryH const options = {...defaultOptions, ...baseOptions} return Apollo.useLazyQuery(IndividualActivitiesDocument, options); } +export function useIndividualActivitiesSuspenseQuery(baseOptions?: Apollo.SuspenseQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useSuspenseQuery(IndividualActivitiesDocument, options); + } export type IndividualActivitiesQueryHookResult = ReturnType; export type IndividualActivitiesLazyQueryHookResult = ReturnType; +export type IndividualActivitiesSuspenseQueryHookResult = ReturnType; export type IndividualActivitiesQueryResult = Apollo.QueryResult; export const CreateIndividualDocument = gql` mutation createIndividual($input: IndividualCreateInput!) { @@ -1823,8 +1853,13 @@ export function useGetManagersLazyQuery(baseOptions?: Apollo.LazyQueryHookOption const options = {...defaultOptions, ...baseOptions} return Apollo.useLazyQuery(GetManagersDocument, options); } +export function useGetManagersSuspenseQuery(baseOptions?: Apollo.SuspenseQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useSuspenseQuery(GetManagersDocument, options); + } export type GetManagersQueryHookResult = ReturnType; export type GetManagersLazyQueryHookResult = ReturnType; +export type GetManagersSuspenseQueryHookResult = ReturnType; export type GetManagersQueryResult = Apollo.QueryResult; export const IndividualsDocument = gql` query individuals($managerId: ID, $fetchManagerId: ID!, $fetchManagerDetails: Boolean = false, $isManager: Boolean) { @@ -1864,7 +1899,7 @@ export const IndividualsDocument = gql` * }, * }); */ -export function useIndividualsQuery(baseOptions: Apollo.QueryHookOptions) { +export function useIndividualsQuery(baseOptions: Apollo.QueryHookOptions & ({ variables: IndividualsQueryVariables; skip?: boolean; } | { skip: boolean; }) ) { const options = {...defaultOptions, ...baseOptions} return Apollo.useQuery(IndividualsDocument, options); } @@ -1872,8 +1907,13 @@ export function useIndividualsLazyQuery(baseOptions?: Apollo.LazyQueryHookOption const options = {...defaultOptions, ...baseOptions} return Apollo.useLazyQuery(IndividualsDocument, options); } +export function useIndividualsSuspenseQuery(baseOptions?: Apollo.SuspenseQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useSuspenseQuery(IndividualsDocument, options); + } export type IndividualsQueryHookResult = ReturnType; export type IndividualsLazyQueryHookResult = ReturnType; +export type IndividualsSuspenseQueryHookResult = ReturnType; export type IndividualsQueryResult = Apollo.QueryResult; export const FindVisionDocument = gql` query findVision($id: ID!) { @@ -1914,7 +1954,7 @@ ${CycleFragmentFragmentDoc}`; * }, * }); */ -export function useFindVisionQuery(baseOptions: Apollo.QueryHookOptions) { +export function useFindVisionQuery(baseOptions: Apollo.QueryHookOptions & ({ variables: FindVisionQueryVariables; skip?: boolean; } | { skip: boolean; }) ) { const options = {...defaultOptions, ...baseOptions} return Apollo.useQuery(FindVisionDocument, options); } @@ -1922,8 +1962,13 @@ export function useFindVisionLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions const options = {...defaultOptions, ...baseOptions} return Apollo.useLazyQuery(FindVisionDocument, options); } +export function useFindVisionSuspenseQuery(baseOptions?: Apollo.SuspenseQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useSuspenseQuery(FindVisionDocument, options); + } export type FindVisionQueryHookResult = ReturnType; export type FindVisionLazyQueryHookResult = ReturnType; +export type FindVisionSuspenseQueryHookResult = ReturnType; export type FindVisionQueryResult = Apollo.QueryResult; export const UpdateVisionDocument = gql` mutation UpdateVision($input: VisionUpdateInput!) { @@ -2026,7 +2071,7 @@ export const VisionsDocument = gql` * }, * }); */ -export function useVisionsQuery(baseOptions: Apollo.QueryHookOptions) { +export function useVisionsQuery(baseOptions: Apollo.QueryHookOptions & ({ variables: VisionsQueryVariables; skip?: boolean; } | { skip: boolean; }) ) { const options = {...defaultOptions, ...baseOptions} return Apollo.useQuery(VisionsDocument, options); } @@ -2034,8 +2079,13 @@ export function useVisionsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions(VisionsDocument, options); } +export function useVisionsSuspenseQuery(baseOptions?: Apollo.SuspenseQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useSuspenseQuery(VisionsDocument, options); + } export type VisionsQueryHookResult = ReturnType; export type VisionsLazyQueryHookResult = ReturnType; +export type VisionsSuspenseQueryHookResult = ReturnType; export type VisionsQueryResult = Apollo.QueryResult; export const CreateVisionDocument = gql` mutation CreateVision($input: VisionCreateInput!) { @@ -2108,7 +2158,7 @@ export const GetVisionTypesAndCyclesDocument = gql` * }, * }); */ -export function useGetVisionTypesAndCyclesQuery(baseOptions: Apollo.QueryHookOptions) { +export function useGetVisionTypesAndCyclesQuery(baseOptions: Apollo.QueryHookOptions & ({ variables: GetVisionTypesAndCyclesQueryVariables; skip?: boolean; } | { skip: boolean; }) ) { const options = {...defaultOptions, ...baseOptions} return Apollo.useQuery(GetVisionTypesAndCyclesDocument, options); } @@ -2116,8 +2166,13 @@ export function useGetVisionTypesAndCyclesLazyQuery(baseOptions?: Apollo.LazyQue const options = {...defaultOptions, ...baseOptions} return Apollo.useLazyQuery(GetVisionTypesAndCyclesDocument, options); } +export function useGetVisionTypesAndCyclesSuspenseQuery(baseOptions?: Apollo.SuspenseQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useSuspenseQuery(GetVisionTypesAndCyclesDocument, options); + } export type GetVisionTypesAndCyclesQueryHookResult = ReturnType; export type GetVisionTypesAndCyclesLazyQueryHookResult = ReturnType; +export type GetVisionTypesAndCyclesSuspenseQueryHookResult = ReturnType; export type GetVisionTypesAndCyclesQueryResult = Apollo.QueryResult; export const GetLoggedInUserInfoDocument = gql` query getLoggedInUserInfo { @@ -2128,6 +2183,9 @@ export const GetLoggedInUserInfoDocument = gql` isManager organizationId } + Organization { + isPersonal + } } } `; @@ -2155,6 +2213,11 @@ export function useGetLoggedInUserInfoLazyQuery(baseOptions?: Apollo.LazyQueryHo const options = {...defaultOptions, ...baseOptions} return Apollo.useLazyQuery(GetLoggedInUserInfoDocument, options); } +export function useGetLoggedInUserInfoSuspenseQuery(baseOptions?: Apollo.SuspenseQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useSuspenseQuery(GetLoggedInUserInfoDocument, options); + } export type GetLoggedInUserInfoQueryHookResult = ReturnType; export type GetLoggedInUserInfoLazyQueryHookResult = ReturnType; +export type GetLoggedInUserInfoSuspenseQueryHookResult = ReturnType; export type GetLoggedInUserInfoQueryResult = Apollo.QueryResult; \ No newline at end of file diff --git a/app/contexts/appApollo/appApolloProvider.tsx b/app/contexts/apollo/apolloClientProvider.tsx similarity index 88% rename from app/contexts/appApollo/appApolloProvider.tsx rename to app/contexts/apollo/apolloClientProvider.tsx index 2afd6a4..bf694ce 100644 --- a/app/contexts/appApollo/appApolloProvider.tsx +++ b/app/contexts/apollo/apolloClientProvider.tsx @@ -3,7 +3,7 @@ import { ApolloProvider } from "@apollo/client"; import { useAuthenticationContext } from "../authentication/authenticationContext"; import { useMemo } from "react"; -export const AppApolloProvider = ({ +export const ApolloClientProvider = ({ children, }: { children: React.ReactNode; @@ -11,7 +11,6 @@ export const AppApolloProvider = ({ const { user } = useAuthenticationContext(); const client = useMemo(() => { - console.log("Client is created!!!"); //TODO: return getApolloClient( sessionStorage.getItem("graphql_url"), user?.jwt_token, diff --git a/app/contexts/apollo/apolloServerProvider.tsx b/app/contexts/apollo/apolloServerProvider.tsx new file mode 100644 index 0000000..1b50a82 --- /dev/null +++ b/app/contexts/apollo/apolloServerProvider.tsx @@ -0,0 +1,15 @@ +import { + ApolloClient, + ApolloProvider, + NormalizedCacheObject, +} from "@apollo/client"; + +export const ApolloServerProvider = ({ + client, + children, +}: { + client: ApolloClient; + children: React.ReactNode; +}) => { + return {children}; +}; diff --git a/app/contexts/authentication/authenticationProvider.tsx b/app/contexts/authentication/AuthenticationClientProvider.tsx similarity index 50% rename from app/contexts/authentication/authenticationProvider.tsx rename to app/contexts/authentication/AuthenticationClientProvider.tsx index 4191b2f..25ca68e 100644 --- a/app/contexts/authentication/authenticationProvider.tsx +++ b/app/contexts/authentication/AuthenticationClientProvider.tsx @@ -3,15 +3,21 @@ import { useState } from "react"; import { User } from "~/models/user"; import { AuthenticationContext } from "./authenticationContext"; -export const AuthenticationProvider = ({ +export const AuthenticationClientProvider = ({ children, }: { children: React.ReactNode; }) => { - const [user, setUser] = useState(); + const [user, setUser] = useState(window.__USER_STATE__); return ( - + {children} ); diff --git a/app/contexts/authentication/AuthenticationServerProvider.tsx b/app/contexts/authentication/AuthenticationServerProvider.tsx new file mode 100644 index 0000000..ffa70f6 --- /dev/null +++ b/app/contexts/authentication/AuthenticationServerProvider.tsx @@ -0,0 +1,22 @@ +import { User } from "~/models/user"; +import { AuthenticationContext } from "./authenticationContext"; + +export const AuthenticationServerProvider = ({ + user, + children, +}: { + user: User | null; + children: React.ReactNode; +}) => { + return ( + {}, + isAuthenticated: user != null, + }} + > + {children} + + ); +}; diff --git a/app/contexts/authentication/authenticationContext.ts b/app/contexts/authentication/authenticationContext.ts index 455bbd5..cd331cc 100644 --- a/app/contexts/authentication/authenticationContext.ts +++ b/app/contexts/authentication/authenticationContext.ts @@ -2,11 +2,13 @@ import React from "react"; import { User } from "~/models/user"; export const AuthenticationContext = React.createContext<{ - user?: User; - setUser: React.Dispatch>; + user: User | null; + setUser: React.Dispatch>; + isAuthenticated: boolean; }>({ - user: undefined, + user: null, setUser: () => {}, + isAuthenticated: false, }); export function useAuthenticationContext() { diff --git a/app/contexts/index.ts b/app/contexts/index.ts index ec77cfa..9cd1698 100644 --- a/app/contexts/index.ts +++ b/app/contexts/index.ts @@ -1,3 +1,5 @@ export * from "./authentication/authenticationContext"; -export * from "./authentication/authenticationProvider"; -export * from "./appApollo/appApolloProvider"; +export * from "./authentication/AuthenticationClientProvider"; +export * from "./authentication/AuthenticationServerProvider"; +export * from "./apollo/apolloClientProvider"; +export * from "./apollo/apolloServerProvider"; diff --git a/app/entry.client.tsx b/app/entry.client.tsx index 47212d8..e63201e 100644 --- a/app/entry.client.tsx +++ b/app/entry.client.tsx @@ -8,19 +8,19 @@ import { ThemeProvider } from "@material-tailwind/react"; import { RemixBrowser } from "@remix-run/react"; import { startTransition, StrictMode } from "react"; import { hydrateRoot } from "react-dom/client"; -import { AppApolloProvider, AuthenticationProvider } from "./contexts"; +import { ApolloClientProvider, AuthenticationClientProvider } from "./contexts"; startTransition(() => { hydrateRoot( document, - - + + - - + + , ); }); diff --git a/app/entry.server.tsx b/app/entry.server.tsx index 4687bc3..4f75e2c 100644 --- a/app/entry.server.tsx +++ b/app/entry.server.tsx @@ -12,11 +12,13 @@ import { RemixServer } from "@remix-run/react"; import isbot from "isbot"; import { renderToPipeableStream } from "react-dom/server"; -import { ApolloProvider } from "@apollo/client"; +import { ApolloClient, NormalizedCacheObject } from "@apollo/client"; import { getDataFromTree } from "@apollo/client/react/ssr"; import { authenticator } from "./services/auth.server"; import type { ReactElement } from "react"; import * as utils from "./utils"; +import { AuthenticationServerProvider, ApolloServerProvider } from "./contexts"; +import { User } from "./models/user"; const ABORT_DELAY = 5_000; // process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; //TODO: remove this line. It is dangerous. We have it because there is an issue with the SSL certificate chain of render.com in production @@ -152,9 +154,10 @@ async function wrapRemixServerWithApollo( remixServer: ReactElement, request: Request, ) { - const client = await buildApolloClient(request); + const user = await authenticator.isAuthenticated(request); + const client = await buildApolloClient(user); - const app = {remixServer}; + const app = getServerApp(user, client, remixServer); await getDataFromTree(app); const initialState = client.extract(); @@ -164,9 +167,9 @@ async function wrapRemixServerWithApollo( {app} in a string literal +} +function buildApolloClient(user: User | null) { return utils.getApolloClient( process.env.GRAPHQL_SCHEMA_URL, user?.jwt_token, user?.organization_id.toString(), ); } + +function getServerApp( + user: User | null, + client: ApolloClient, + remixServer: ReactElement, +) { + return ( + + {remixServer} + + ); +} diff --git a/app/models/user.ts b/app/models/user.ts index 5f43fb4..4d04284 100644 --- a/app/models/user.ts +++ b/app/models/user.ts @@ -3,7 +3,8 @@ export type User = { jwt_token: string; name: string; user_id: number; - individual_id?: number; + individual_id: number; organization_id: number; is_manager?: boolean; + isPersonal: boolean; }; diff --git a/app/routes/_auth.login.($id).tsx b/app/routes/_auth.login.($id).tsx index 702afe8..c6876a8 100644 --- a/app/routes/_auth.login.($id).tsx +++ b/app/routes/_auth.login.($id).tsx @@ -86,7 +86,7 @@ export function SignIn() {
@@ -167,6 +167,7 @@ export function SignIn() {
diff --git a/app/routes/_dashboard.cycles._index/route.tsx b/app/routes/_dashboard.cycles._index/route.tsx index d0d739c..db3735c 100644 --- a/app/routes/_dashboard.cycles._index/route.tsx +++ b/app/routes/_dashboard.cycles._index/route.tsx @@ -11,21 +11,13 @@ import { CardFooter, Spinner, } from "@material-tailwind/react"; -import { LoaderFunction, redirect } from "@remix-run/node"; + import { Link } from "@remix-run/react"; -import { authenticator } from "~/services/auth.server"; import { noNull } from "~/utils"; import { AssignMissedActivitiesButton } from "~/components/AssignMissedActivitiesButton"; const TABLE_HEAD = ["Title", "From", "To", ""]; -export let loader: LoaderFunction = async ({ request }) => { - //we should completely change the following appraoch - let user = await authenticator.isAuthenticated(request); - if (!user) return redirect("/login"); - else return { user }; -}; - export default function Cycles() { let pageTitle = "Cycles"; let pageSubTitle = ""; diff --git a/app/routes/_dashboard.individuals.$id.coach/route.tsx b/app/routes/_dashboard.individuals.$id.coach/route.tsx index f76b4a1..b35771d 100644 --- a/app/routes/_dashboard.individuals.$id.coach/route.tsx +++ b/app/routes/_dashboard.individuals.$id.coach/route.tsx @@ -19,15 +19,20 @@ import { noNull } from "~/utils"; import { GenerateCycleSummaryButton } from "./components/GenerateCycleSummaryButton"; import { DefaultSkeleton } from "~/components/DefaultSkeleton"; +import { useAuthenticationContext } from "~/contexts"; export default function IndividualCoach() { - const { id } = useParams(); - if (id == null) throw new Error("id is null"); + const { id: idParam } = useParams(); + if (idParam == null) throw new Error("id is null"); const [open, setOpen] = useState(0); const [isOnGneratingAdvice, setIsOnGneratingAdvice] = useState(false); const [adviceList, setAdviceList] = useState( null, ); + const { user } = useAuthenticationContext(); + + let id = idParam == "me" ? user?.individual_id.toString() : idParam; + if (id == null) throw new Error("id is null"); const { data, loading, error } = useCoachIndividualQuery({ variables: { id: id }, @@ -127,21 +132,20 @@ export default function IndividualCoach() { - - - {isOnGneratingAdvice ? ( - - ) : cycle.advice?.activitySummary ? ( -
-                    {cycle.advice.activitySummary}
-                  
- ) : ( - "-" - )} -
+
+                
+                  {isOnGneratingAdvice ? (
+                    
+                  ) : cycle.advice?.activitySummary ? (
+                    cycle.advice.activitySummary
+                  ) : (
+                    "-"
+                  )}
+                
+              
@@ -165,20 +169,20 @@ export default function IndividualCoach() { - - {isOnGneratingAdvice ? ( - - ) : cycle.advice?.visionSummary ? ( -
-                    {cycle.advice.visionSummary}
-                  
- ) : ( - "-" - )} -
+
+                
+                  {isOnGneratingAdvice ? (
+                    
+                  ) : cycle.advice?.visionSummary ? (
+                    cycle.advice.visionSummary
+                  ) : (
+                    "-"
+                  )}
+                
+              
@@ -186,21 +190,20 @@ export default function IndividualCoach() { Coaching Advice - - - {isOnGneratingAdvice ? ( - - ) : cycle.advice?.result ? ( -
-                    {cycle.advice.result}
-                  
- ) : ( - "-" - )} -
+
+                
+                  {isOnGneratingAdvice ? (
+                    
+                  ) : cycle.advice?.result ? (
+                    cycle.advice.result
+                  ) : (
+                    "-"
+                  )}
+                
+              
diff --git a/app/routes/_dashboard.individuals.($id).activities/route.tsx b/app/routes/_dashboard.individuals.($id).activities/route.tsx index 0516912..bc85e62 100644 --- a/app/routes/_dashboard.individuals.($id).activities/route.tsx +++ b/app/routes/_dashboard.individuals.($id).activities/route.tsx @@ -20,10 +20,9 @@ import { Select, Option, } from "@material-tailwind/react"; -import { LoaderFunction, redirect } from "@remix-run/node"; import { Link, useParams, useLocation, useNavigate } from "@remix-run/react"; import { useState } from "react"; -import { authenticator } from "~/services/auth.server"; + import { AnalyzeButton, ImportModal } from "./components"; import { noNull } from "~/utils"; @@ -44,13 +43,6 @@ const TABS: { label: string; value: FilterType }[] = [ ]; const TABLE_HEAD = ["Activity", "Analyzed?", "Date", "Cycle", ""]; -export let loader: LoaderFunction = async ({ request }) => { - //we should completely change the following appraoch - let user = await authenticator.isAuthenticated(request); - if (!user) return redirect("/login"); - return null; -}; - export default function Activities() { let { id: individualId } = useParams(); if (individualId == null) throw new Error("id is null"); diff --git a/app/routes/_dashboard.individuals.($id).visions/route.tsx b/app/routes/_dashboard.individuals.($id).visions/route.tsx index b080dc9..09c6535 100644 --- a/app/routes/_dashboard.individuals.($id).visions/route.tsx +++ b/app/routes/_dashboard.individuals.($id).visions/route.tsx @@ -1,8 +1,4 @@ -import Visions, { - loader as visionsloader, -} from "../_dashboard.visions._index/route"; - -export let loader = visionsloader; +import Visions from "../_dashboard.visions._index/route"; export default function IndividualVisions() { return ; diff --git a/app/routes/_dashboard.individuals._index.($id)/route.tsx b/app/routes/_dashboard.individuals._index.($id)/route.tsx index 0c85c6e..d1219fe 100644 --- a/app/routes/_dashboard.individuals._index.($id)/route.tsx +++ b/app/routes/_dashboard.individuals._index.($id)/route.tsx @@ -23,11 +23,9 @@ import { Tooltip, Spinner, } from "@material-tailwind/react"; -import { LoaderFunction, redirect } from "@remix-run/node"; -import { Link, useLoaderData, useNavigate, useParams } from "@remix-run/react"; +import { Link, useNavigate, useParams } from "@remix-run/react"; import { useState } from "react"; -import { User } from "~/models/user"; -import { authenticator } from "~/services/auth.server"; +import { useAuthenticationContext } from "~/contexts"; type FilterType = "all" | "managers" | "ICs"; @@ -48,27 +46,16 @@ const TABS: { label: string; value: FilterType }[] = [ const TABLE_HEAD = ["Name", "Level", "Role", ""]; -export let loader: LoaderFunction = async ({ request }) => { - //we should completely change the following appraoch - let user = await authenticator.isAuthenticated(request); - if (!user) return redirect("/login"); - else return { user }; -}; - -function useUser() { - const data = useLoaderData<{ user?: User }>(); - return data.user; -} - export default function Individuals() { let { id: managerId } = useParams(); - const user = useUser(); + const { user } = useAuthenticationContext(); const [filter, setFilter] = useState("all"); + if (user == null) throw new Error("User is null"); let pageTitle = "People"; let pageSubTitle = ""; - if (managerId == "myteam" && user?.individual_id != null) { + if (managerId == "myteam") { managerId = user?.individual_id.toString(); } diff --git a/app/routes/_dashboard.tsx b/app/routes/_dashboard.tsx index 100b5f9..d7cc6a6 100644 --- a/app/routes/_dashboard.tsx +++ b/app/routes/_dashboard.tsx @@ -3,49 +3,40 @@ import { IconButton } from "@material-tailwind/react"; import { Cog6ToothIcon } from "@heroicons/react/24/solid"; import { Sidenav, DashboardNavbar, Footer } from "~/widgets/layout"; -import routes from "~/routesData"; +import { getRoutes } from "~/routesData"; import { authenticator } from "~/services/auth.server"; -import { LoaderFunctionArgs } from "@remix-run/node"; +import { LoaderFunctionArgs, redirect } from "@remix-run/node"; import { useEffect } from "react"; -import { User } from "~/models/user"; import { Settings } from "~/models/settings"; import { useAuthenticationContext } from "~/contexts/authentication/authenticationContext"; export async function loader({ request }: LoaderFunctionArgs) { let user = await authenticator.isAuthenticated(request); + if (!user && request.url.indexOf("/login") == -1) return redirect("/login"); //TODO: any danger of infinite loop? + const settings: Settings = { graphql_url: process.env.GRAPHQL_SCHEMA_URL! }; return { user, settings }; } -function useUser() { - const data = useLoaderData<{ user?: User; settings: Settings }>(); - return data.user; -} - function useSettings() { - const data = useLoaderData<{ user?: User; settings: Settings }>(); + const data = useLoaderData(); return data.settings; } export default function Dashboard() { //TODO: change the followings to get value from context - const sidenavType = routes == null ? "dark" : "white"; // this fake comparison is to avoid TS error only. - const user = useUser(); + const sidenavType = getRoutes == null ? "dark" : "white"; // this fake comparison is to avoid TS error only. const settings = useSettings(); - const { setUser: setAuthContextUser } = useAuthenticationContext(); + const { user } = useAuthenticationContext(); useEffect(() => { sessionStorage.setItem("graphql_url", settings.graphql_url); }, [settings.graphql_url]); - useEffect(() => { - setAuthContextUser(user ? user : undefined); - }, [user, setAuthContextUser]); - return (
{ - //we should completely change the following appraoch - let user = await authenticator.isAuthenticated(request); - if (!user) return redirect("/login"); - return null; -}; - export default function Visions() { let { id: individualId } = useParams(); @@ -88,8 +79,8 @@ export default function Visions() { filter === "personal" ? VisionFilterLevel.PersonalOnly : filter === "organizational" - ? VisionFilterLevel.OrganizationalOnly - : undefined, + ? VisionFilterLevel.OrganizationalOnly + : undefined, }, fetchPolicy: "network-only", }); diff --git a/app/routes/_site._index.tsx b/app/routes/_site._index.tsx index cd10810..62153f9 100644 --- a/app/routes/_site._index.tsx +++ b/app/routes/_site._index.tsx @@ -205,7 +205,7 @@ export function Home() { > I agree the  Terms and Conditions diff --git a/app/routes/auth.google.callback.tsx b/app/routes/auth.google.callback.tsx index 4938f26..24e2116 100644 --- a/app/routes/auth.google.callback.tsx +++ b/app/routes/auth.google.callback.tsx @@ -3,7 +3,7 @@ import { authenticator } from "~/services/auth.server"; export let loader = ({ request }: LoaderFunctionArgs) => { return authenticator.authenticate("google", request, { - successRedirect: "/individuals", + successRedirect: "/individuals/me/coach", // failureRedirect: "/login?error", we ingore using it now and we prefer to see the exception. later we should fix it. // https://github.com/sergiodxa/remix-auth?tab=readme-ov-file#errors-handling }); diff --git a/app/routes/auth.google.tsx b/app/routes/auth.google.tsx index 0e818a4..f873428 100644 --- a/app/routes/auth.google.tsx +++ b/app/routes/auth.google.tsx @@ -3,6 +3,6 @@ import { authenticator } from "~/services/auth.server"; export async function action({ request }: ActionFunctionArgs) { return authenticator.authenticate("google", request, { - successRedirect: "/individuals", + successRedirect: "/individuals/me/coach", }); } diff --git a/app/routesData.tsx b/app/routesData.tsx index b678f50..7dfc53d 100644 --- a/app/routesData.tsx +++ b/app/routesData.tsx @@ -12,6 +12,7 @@ import { LightBulbIcon, DocumentChartBarIcon, } from "@heroicons/react/24/solid"; +import { User } from "./models/user"; const icon = { className: "w-5 h-5 text-inherit", @@ -20,76 +21,101 @@ const icon = { export type RouteData = { layout?: string; title?: string; + level?: "personal" | "organization"; pages: { icon: JSX.Element; name: string; path: string; + level?: "personal" | "organization"; }[]; }; -export const routes: RouteData[] = [ - { - pages: [ - { - icon: , - name: "Home", - path: "/", - }, - { - icon: , - name: "Coach me!", - path: "/individuals/#{myId}/coach", - }, - ], - }, - { - title: "Organization", - pages: [ - { - icon: , - name: "My team", - path: "/individuals/myteam", - }, - { - icon: , - name: "People", - path: "/individuals", - }, - { - icon: , - name: "Visions", - path: "/visions", - }, - { - icon: , - name: "Cycles", - path: "/cycles", - }, - ], - }, - { - title: "My profile", - pages: [ - { - icon: , - name: "My activities", - path: "/individuals/#{myId}/activities", - }, - { - icon: , - name: "My Visions", - path: "/individuals/#{myId}/visions", - }, - { - icon: , - name: "My settings", - path: "/individuals/#{myId}/edit", - }, - ], - }, -]; +export function getRoutes(user: User | null): RouteData[] { + if (user == null) return []; + const routes: RouteData[] = [ + { + pages: [ + { + icon: , + name: "Home", + path: "/", + }, + { + icon: , + name: "Coach me!", + path: `/individuals/me/coach`, + }, + ], + }, + { + title: "Organization", + level: "organization", + pages: [ + { + icon: , + name: "My team", + path: "/individuals/myteam", + }, + { + icon: , + name: "People", + path: "/individuals", + }, + { + icon: , + name: "Visions", + path: "/visions", + }, + { + icon: , + name: "Cycles", + path: "/cycles", + }, + ], + }, + { + title: "My profile", + pages: [ + { + icon: , + name: "My activities", + path: `/individuals/${user.individual_id}/activities`, + }, + { + icon: , + name: "My Visions", + path: `/individuals/${user.individual_id}/visions`, + }, + { + icon: , + name: "My settings", + path: `/individuals/${user.individual_id}/edit`, + }, + { + icon: , + name: "Cycles", + path: "/cycles", + level: "personal", + }, + ], + }, + ]; -export default routes; + return routes + .filter( + (section) => + section.level == undefined || + section.level == (user.isPersonal ? "personal" : "organization"), + ) + .map((section) => ({ + ...section, + pages: section.pages.filter( + (page) => + page.level == undefined || + page.level == (user.isPersonal ? "personal" : "organization"), + ), + })); +} export const teamData: { img: string; diff --git a/app/services/auth.strategies/google.ts b/app/services/auth.strategies/google.ts index 1b8bc63..dfc04eb 100644 --- a/app/services/auth.strategies/google.ts +++ b/app/services/auth.strategies/google.ts @@ -51,7 +51,7 @@ export let googleStrategy = new GoogleStrategy( const individual = myInfo.Individual; - return { + const user: User = { email: profile.emails[0].value, jwt_token: jwt_token, name: profile.displayName, @@ -59,7 +59,9 @@ export let googleStrategy = new GoogleStrategy( user_id: myInfo.UserId, is_manager: individual.isManager, organization_id: individual.organizationId, - } as User; + isPersonal: myInfo.Organization!.isPersonal, + }; + return user; } catch (error: any) { const msg = "Error fetching loggined in user info through API. Details: " + diff --git a/app/services/auth.strategies/graphql/getLoggedInUserInfo.gql b/app/services/auth.strategies/graphql/getLoggedInUserInfo.gql index 236e9b4..161a266 100644 --- a/app/services/auth.strategies/graphql/getLoggedInUserInfo.gql +++ b/app/services/auth.strategies/graphql/getLoggedInUserInfo.gql @@ -6,5 +6,8 @@ query getLoggedInUserInfo { isManager organizationId } + Organization { + isPersonal + } } } diff --git a/app/widgets/layout/dashboard-navbar.tsx b/app/widgets/layout/dashboard-navbar.tsx index 256860a..332b5a2 100644 --- a/app/widgets/layout/dashboard-navbar.tsx +++ b/app/widgets/layout/dashboard-navbar.tsx @@ -6,28 +6,18 @@ import { IconButton, Breadcrumbs, Input, - Menu, - MenuHandler, - MenuList, - MenuItem, - Avatar, } from "@material-tailwind/react"; import { UserCircleIcon, Cog6ToothIcon, - BellIcon, - ClockIcon, - CreditCardIcon, Bars3Icon, } from "@heroicons/react/24/solid"; import { signOutClient } from "~/utils"; import { useApolloClient } from "@apollo/client"; -import { useAuthenticationContext } from "~/contexts"; export function DashboardNavbar() { const { pathname } = useLocation(); const apolloClient = useApolloClient(); - const { setUser: setAuthContextUser } = useAuthenticationContext(); let [layout, page] = pathname.split("/").filter((el) => el !== ""); const fixedNavbar = false; @@ -80,7 +70,6 @@ export function DashboardNavbar() { variant="text" color="blue-gray" className="grid xl:hidden" - onClick={() => {}} > @@ -90,8 +79,8 @@ export function DashboardNavbar() { color="blue-gray" className="hidden items-center gap-1 px-4 normal-case xl:flex" onClick={() => { - setAuthContextUser(undefined); - signOutClient(apolloClient); + // setAuthContextUser(undefined); it is not a good practice. It reloads the already loaded/cached pages with empty user data which raises errors in another render before the full redirect happens. + signOutClient(apolloClient); // i guess we don't need this one as well. }} > @@ -105,7 +94,7 @@ export function DashboardNavbar() { - + {/* @@ -182,7 +171,7 @@ export function DashboardNavbar() {
- + */} {}}> diff --git a/app/widgets/layout/firstpage-footer.tsx b/app/widgets/layout/firstpage-footer.tsx index 0f72850..79890db 100644 --- a/app/widgets/layout/firstpage-footer.tsx +++ b/app/widgets/layout/firstpage-footer.tsx @@ -163,6 +163,7 @@ FirstPageFooter.defaultProps = { Creative Tim diff --git a/app/widgets/layout/sidenav.tsx b/app/widgets/layout/sidenav.tsx index 1b1c28b..df76ee2 100644 --- a/app/widgets/layout/sidenav.tsx +++ b/app/widgets/layout/sidenav.tsx @@ -4,23 +4,8 @@ import { XMarkIcon } from "@heroicons/react/24/outline"; import { Button, IconButton, Typography } from "@material-tailwind/react"; import { RouteData } from "~/routesData"; -import { useLoaderData, useNavigate } from "@remix-run/react"; +import { useNavigate } from "@remix-run/react"; import { useEffect, useState } from "react"; -import { LoaderFunction, redirect } from "@remix-run/node"; -import { authenticator } from "~/services/auth.server"; -import { User } from "~/models/user"; - -export let loader: LoaderFunction = async ({ request }) => { - //we should completely change the following appraoch - let user = await authenticator.isAuthenticated(request); - if (!user) return redirect("/login"); - else return { user }; -}; - -function useUser() { - const data = useLoaderData<{ user?: User }>(); - return data.user; -} export function Sidenav({ brandImg, @@ -33,7 +18,6 @@ export function Sidenav({ }) { const nav = useNavigate(); const [openSidenav, setOpenNav] = useState(false); - const user = useUser(); //TODO: change the followings to get value from context const sidenavType = nav == null ? "dark" : "white"; // this fake comparison is to avoid TS error only. @@ -101,13 +85,7 @@ export function Sidenav({ )} {pages.map(({ icon, name, path }) => (
  • - + {({ isActive }) => (