From 3a56e2d48a4e209ccd474db7c2af312af7defd0c Mon Sep 17 00:00:00 2001 From: "Felipe Torres (fforres)" Date: Fri, 5 Jul 2024 20:48:27 -0700 Subject: [PATCH] Remove cookie and centralize token refresh (#40) Making refresh token logic a bit more centralized and simple. Removes the storing of cookies as we are not having SSR anymore. --- app/api/ApolloWrapper.tsx | 19 ++--- app/api/gql/graphql.ts | 3 +- app/api/gql/schema.gql | 3 +- app/routes/_authenticated/graphiql/index.tsx | 45 +++++----- app/utils/supabase/AuthProvider/index.tsx | 89 ++++++++++---------- app/utils/supabase/client.ts | 38 --------- 6 files changed, 80 insertions(+), 117 deletions(-) diff --git a/app/api/ApolloWrapper.tsx b/app/api/ApolloWrapper.tsx index 56dc7b1..811663e 100644 --- a/app/api/ApolloWrapper.tsx +++ b/app/api/ApolloWrapper.tsx @@ -10,8 +10,7 @@ import { setContext } from "@apollo/client/link/context"; import { onError } from "@apollo/client/link/error"; import { RetryLink } from "@apollo/client/link/retry"; -import { useTokenRef } from "../utils/supabase/AuthProvider"; -import { useRefreshToken } from "../utils/supabase/client"; +import { useRefreshSession, useTokenRef } from "../utils/supabase/AuthProvider"; const retryLink = new RetryLink(); @@ -62,21 +61,20 @@ const useAuthLink = () => { // Este link se encarga de manejar errores de autenticación // Si el servidor responde con un code UNAUTHENTICATED, intenta refrescar el token. const useErrorLink = () => { - const refreshToken = useRefreshToken(); + const refreshSession = useRefreshSession(); return onError(({ graphQLErrors, networkError, operation, forward }) => { if (graphQLErrors) { for (const err of graphQLErrors) { if (err.extensions.type === "UNAUTHENTICATED") { - refreshToken( - () => { + refreshSession() + .then(() => { forward(operation); - }, - () => { + }) + .catch(() => { // eslint-disable-next-line no-console console.error("Error refreshing access token"); - }, - ); + }); } } } else if (networkError) { @@ -100,8 +98,7 @@ if (!import.meta.env.VITE_JSCL_API_URL) { const httpLink = new HttpLink({ // Tiene que ser una URL absoluta, ya que las URLs relativas no pueden ser usadas en SSR. uri: import.meta.env.VITE_JSCL_API_URL, - fetchOptions: { cache: "no-store", credentials: "include" }, - credentials: "include", + fetchOptions: { cache: "no-store" }, }); function useMakeClient() { diff --git a/app/api/gql/graphql.ts b/app/api/gql/graphql.ts index e9fbae5..bf2978d 100644 --- a/app/api/gql/graphql.ts +++ b/app/api/gql/graphql.ts @@ -633,7 +633,7 @@ export type TagSearchInput = { export type Ticket = { description: Maybe; endDateTime: Maybe; - eventId: Scalars["String"]["output"]; + event: Event; id: Scalars["ID"]["output"]; /** Whether or not the ticket is free */ isFree: Scalars["Boolean"]["output"]; @@ -773,6 +773,7 @@ export type UserTicket = { approvalStatus: TicketApprovalStatus; id: Scalars["ID"]["output"]; paymentStatus: TicketPaymentStatus; + purchaseOrder: Maybe; redemptionStatus: TicketRedemptionStatus; ticketTemplate: Ticket; }; diff --git a/app/api/gql/schema.gql b/app/api/gql/schema.gql index 256d8d8..1fa0c52 100644 --- a/app/api/gql/schema.gql +++ b/app/api/gql/schema.gql @@ -620,7 +620,7 @@ Representation of a ticket type Ticket { description: String endDateTime: DateTime - eventId: String! + event: Event! id: ID! """ @@ -787,6 +787,7 @@ type UserTicket { approvalStatus: TicketApprovalStatus! id: ID! paymentStatus: TicketPaymentStatus! + purchaseOrder: PurchaseOrder redemptionStatus: TicketRedemptionStatus! ticketTemplate: Ticket! } diff --git a/app/routes/_authenticated/graphiql/index.tsx b/app/routes/_authenticated/graphiql/index.tsx index 65378be..4fe7da1 100644 --- a/app/routes/_authenticated/graphiql/index.tsx +++ b/app/routes/_authenticated/graphiql/index.tsx @@ -7,12 +7,12 @@ import { Button } from "~/components/ui/button"; import { useIsAuthReady, useIsLoggedIn, + useRefreshSession, useTokenRef, } from "~/utils/supabase/AuthProvider"; -import { useRefreshToken } from "~/utils/supabase/client"; import { urls } from "~/utils/urls"; -const comunidades = `query TodasLasComunidades { +const communities = `query AllTheCommunities { communities { description id @@ -21,7 +21,7 @@ const comunidades = `query TodasLasComunidades { } }`; -const comunidadesYEventos = `query ComunidadesYEventos { +const communitiesAndEvents = `query CommunitiesAndEvents { communities { id name @@ -37,7 +37,7 @@ const comunidadesYEventos = `query ComunidadesYEventos { } }`; -const comunidadesUsuariosYEventos = `query comunidadesUsuariosYEventos { +const communitiesUsersAndEvents = `query communitiesUsersAndEvents { communities { id name @@ -60,11 +60,11 @@ const comunidadesUsuariosYEventos = `query comunidadesUsuariosYEventos { } }`; -const mutacionCrearComunidad = `# Esta mutación requiere un permiso especial. +const createCommunityMutation = `# Esta mutación requiere un permiso especial. # Manda un mensaje en el discord # https://https://discord.jschile.org # Para que te asignen el permiso -mutation CrearComunidad($input: CommunityCreateInput!) { +mutation CreateCommunity($input: CommunityCreateInput!) { createCommunity(input: $input) { id description @@ -73,10 +73,10 @@ mutation CrearComunidad($input: CommunityCreateInput!) { } }`; -const mutacionCrearEvento = `# Esta mutación requiere que seas Admin de una Comunidad. +const createEventMutation = `# Esta mutación requiere que seas Admin de una Comunidad. # Puedes permirle permisos al Admin de alguna comunidad # para que te haga admin -mutation CrearEvento($input: EventCreateInput!) { +mutation CreateEvent($input: EventCreateInput!) { createEvent(input: $input) { id description @@ -85,10 +85,10 @@ mutation CrearEvento($input: EventCreateInput!) { } }`; -const mutacionCrearTicket = `# Esta mutación requiere que seas Admin de una Comunidad. +const createTicketMutation = `# Esta mutación requiere que seas Admin de una Comunidad. # Puedes permirle permisos al Admin de alguna comunidad # para que te haga admin -mutation CrearTicket($input: TicketCreateInput!) { +mutation CreateTicket($input: TicketCreateInput!) { createTicket(input: $input) { id description @@ -97,7 +97,7 @@ mutation CrearTicket($input: TicketCreateInput!) { } }`; -const mutacionDeCreatePurchaseOrder = `mutation claimUserTicket($input: TicketClaimInput!) { +const createPurchaseOrderMutation = `mutation claimUserTicket($input: TicketClaimInput!) { claimUserTicket(input: $input) { __typename ... on PurchaseOrder { @@ -132,7 +132,7 @@ export default function Pregunta() { const tokenRef = useTokenRef(); const isLoggedIn = useIsLoggedIn(); const isAuthReady = useIsAuthReady(); - const refreshToken = useRefreshToken(); + const refreshSession = useRefreshSession(); useEffect(() => { if (!tokenRef.current) { @@ -157,7 +157,10 @@ export default function Pregunta() { (jsonResponse as { errors: { extensions: { type: string } }[] }) ?.errors?.[0]?.extensions?.type === "UNAUTHENTICATED" ) { - refreshToken(); + refreshSession().catch((error) => { + // eslint-disable-next-line no-console + console.error("Error refreshing access token", error); + }); } return cloned; @@ -166,7 +169,7 @@ export default function Pregunta() { }); fetcherRef.current = fetcher; - }, [tokenRef]); + }, [refreshSession, tokenRef]); if (!isAuthReady) {
...Loading
; @@ -200,16 +203,16 @@ export default function Pregunta() { defaultEditorToolsVisibility="variables" defaultTabs={[ { - query: comunidades, + query: communities, }, { - query: comunidadesUsuariosYEventos, + query: communitiesUsersAndEvents, }, { - query: comunidadesYEventos, + query: communitiesAndEvents, }, { - query: mutacionCrearComunidad, + query: createCommunityMutation, variables: JSON.stringify( { input: { @@ -223,7 +226,7 @@ export default function Pregunta() { ), }, { - query: mutacionCrearEvento, + query: createEventMutation, variables: JSON.stringify( { input: { @@ -239,7 +242,7 @@ export default function Pregunta() { ), }, { - query: mutacionCrearTicket, + query: createTicketMutation, variables: JSON.stringify( { input: { @@ -262,7 +265,7 @@ export default function Pregunta() { ), }, { - query: mutacionDeCreatePurchaseOrder, + query: createPurchaseOrderMutation, variables: JSON.stringify( { input: { diff --git a/app/utils/supabase/AuthProvider/index.tsx b/app/utils/supabase/AuthProvider/index.tsx index 8ea9b27..fbebadf 100644 --- a/app/utils/supabase/AuthProvider/index.tsx +++ b/app/utils/supabase/AuthProvider/index.tsx @@ -1,5 +1,4 @@ -import { Session, User } from "@supabase/supabase-js"; -import cookies from "js-cookie"; +import { AuthSession, Session, User } from "@supabase/supabase-js"; import { MutableRefObject, createContext, @@ -11,11 +10,7 @@ import { useState, } from "react"; -import { - COOKIE_NAME, - getCookieOptions, - supabaseClient, -} from "~/utils/supabase/client"; +import { supabaseClient } from "~/utils/supabase/client"; export type AuthContextType = { user: User | null; @@ -23,6 +18,7 @@ export type AuthContextType = { isReady: boolean; tokenRef: MutableRefObject; setTokenRef: (token: string | null) => void; + refreshSession: () => Promise; }; export const AuthContext = createContext({ @@ -31,23 +27,31 @@ export const AuthContext = createContext({ isReady: false, tokenRef: { current: null }, setTokenRef: () => {}, + refreshSession: async () => {}, }); export const AuthProvider = ({ children }: { children: React.ReactNode }) => { const [supabaseSession, setSession] = useState(null); const [user, setUser] = useState(null); - const [isReady, setIsReady] = useState(false); const tokenRef = useRef(null); - const setTokenRef = useCallback((token: string | null) => { - tokenRef.current = token; + const setter = useCallback( + (session: AuthSession | null) => { + const user = session?.user ?? null; + const token = session?.access_token ?? null; - if (!tokenRef.current) { - cookies.remove(COOKIE_NAME); - } else { - cookies.set(COOKIE_NAME, tokenRef.current, getCookieOptions()); - } - }, []); + setUser(user); + setSession(session); + tokenRef.current = token; + }, + [setSession, setUser], + ); + const setTokenRef = useCallback( + (token: string | null) => { + tokenRef.current = token; + }, + [tokenRef], + ); useEffect(() => { const initialize = async () => { @@ -55,53 +59,47 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { data: { session }, } = await supabaseClient.auth.getSession(); - setSession(session); + setter(session); + await supabaseClient.auth.startAutoRefresh(); }; const { data: { subscription }, } = supabaseClient.auth.onAuthStateChange(async (_event, session) => { - setSession(session); - const token = session?.access_token ?? null; - - tokenRef.current = token; + setter(session); await supabaseClient.auth.startAutoRefresh(); }); // eslint-disable-next-line no-console - initialize() - .catch(console.error) - .finally(() => setIsReady(true)); + initialize().catch(console.error); return () => subscription.unsubscribe(); - }, []); + }, [setter]); - useEffect(() => { - if (supabaseSession) { - const { user } = supabaseSession; + const refreshSession = useCallback(async () => { + const { data, error } = await supabaseClient.auth.refreshSession(); - setUser(user); + if (error) { + // eslint-disable-next-line no-console + console.error("Error refreshing access token", error); - if (!tokenRef.current) { - cookies.remove(COOKIE_NAME); - } else { - cookies.set(COOKIE_NAME, tokenRef.current, getCookieOptions()); - } - } else { - setUser(null); + return; } - }, [supabaseSession]); + setter(data?.session ?? null); + }, [setter]); const value = useMemo( - () => ({ - user, - isLogged: Boolean(user?.id), - isReady, - tokenRef, - setTokenRef, - }), - [user, isReady, setTokenRef], + () => + ({ + user, + isLogged: Boolean(user?.id), + isReady: Boolean(supabaseSession), + tokenRef, + setTokenRef, + refreshSession, + }) satisfies AuthContextType, + [user, supabaseSession, tokenRef, setTokenRef, refreshSession], ); return {children}; @@ -112,3 +110,4 @@ export const useTokenRef = () => useContext(AuthContext).tokenRef; export const useSetTokenRef = () => useContext(AuthContext).setTokenRef; export const useIsLoggedIn = () => useContext(AuthContext).isLogged; export const useIsAuthReady = () => useContext(AuthContext).isReady; +export const useRefreshSession = () => useContext(AuthContext).refreshSession; diff --git a/app/utils/supabase/client.ts b/app/utils/supabase/client.ts index 721af4e..d251639 100644 --- a/app/utils/supabase/client.ts +++ b/app/utils/supabase/client.ts @@ -1,12 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { createClient } from "@supabase/supabase-js"; -import cookies from "js-cookie"; import { CookieAttributes } from "node_modules/@types/js-cookie"; -import { useSetTokenRef } from "~/utils/supabase/AuthProvider"; - -export const COOKIE_NAME = "community-os-access-token"; - if (!import.meta.env.VITE_SUPABASE_URL) { throw new Error("Missing VITE_SUPABASE_URL"); } @@ -20,45 +15,12 @@ export const supabaseClient = createClient( import.meta.env.VITE_SUPABASE_ANON_KEY, ); -export const useRefreshToken = () => { - const setToken = useSetTokenRef(); - - return (onSuccess?: () => void, onError?: (error: unknown) => void) => { - supabaseClient.auth - .refreshSession() - .then(({ data, error }) => { - if (error) { - // eslint-disable-next-line no-console - console.error("Error refreshing access token", error); - - return; - } - - const newToken = data.session?.access_token; - - if (!newToken) { - // eslint-disable-next-line no-console - console.error("No access token found in session data"); - } else { - setToken(newToken); - } - - onSuccess?.(); - }) - .catch((error: unknown) => { - onError?.(error); - }); - }; -}; - const oneHour = 1000 * 60 * 60; const oneYear = oneHour * 24 * 365; export const logout = async (onDone?: () => void) => { const data = await supabaseClient.auth.signOut({ scope: "local" }); - cookies.remove(COOKIE_NAME); - if (data.error) { throw new Error("Error logging out"); }