Skip to content

Commit

Permalink
Remove cookie and centralize token refresh (#40)
Browse files Browse the repository at this point in the history
Making refresh token logic a bit more centralized and simple.
Removes the storing of cookies as we are not having SSR anymore.
  • Loading branch information
fforres authored Jul 6, 2024
1 parent f28c804 commit 3a56e2d
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 117 deletions.
19 changes: 8 additions & 11 deletions app/api/ApolloWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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) {
Expand All @@ -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() {
Expand Down
3 changes: 2 additions & 1 deletion app/api/gql/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -633,7 +633,7 @@ export type TagSearchInput = {
export type Ticket = {
description: Maybe<Scalars["String"]["output"]>;
endDateTime: Maybe<Scalars["DateTime"]["output"]>;
eventId: Scalars["String"]["output"];
event: Event;
id: Scalars["ID"]["output"];
/** Whether or not the ticket is free */
isFree: Scalars["Boolean"]["output"];
Expand Down Expand Up @@ -773,6 +773,7 @@ export type UserTicket = {
approvalStatus: TicketApprovalStatus;
id: Scalars["ID"]["output"];
paymentStatus: TicketPaymentStatus;
purchaseOrder: Maybe<PurchaseOrder>;
redemptionStatus: TicketRedemptionStatus;
ticketTemplate: Ticket;
};
Expand Down
3 changes: 2 additions & 1 deletion app/api/gql/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -620,7 +620,7 @@ Representation of a ticket
type Ticket {
description: String
endDateTime: DateTime
eventId: String!
event: Event!
id: ID!

"""
Expand Down Expand Up @@ -787,6 +787,7 @@ type UserTicket {
approvalStatus: TicketApprovalStatus!
id: ID!
paymentStatus: TicketPaymentStatus!
purchaseOrder: PurchaseOrder
redemptionStatus: TicketRedemptionStatus!
ticketTemplate: Ticket!
}
Expand Down
45 changes: 24 additions & 21 deletions app/routes/_authenticated/graphiql/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -21,7 +21,7 @@ const comunidades = `query TodasLasComunidades {
}
}`;

const comunidadesYEventos = `query ComunidadesYEventos {
const communitiesAndEvents = `query CommunitiesAndEvents {
communities {
id
name
Expand All @@ -37,7 +37,7 @@ const comunidadesYEventos = `query ComunidadesYEventos {
}
}`;

const comunidadesUsuariosYEventos = `query comunidadesUsuariosYEventos {
const communitiesUsersAndEvents = `query communitiesUsersAndEvents {
communities {
id
name
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
Expand All @@ -166,7 +169,7 @@ export default function Pregunta() {
});

fetcherRef.current = fetcher;
}, [tokenRef]);
}, [refreshSession, tokenRef]);

if (!isAuthReady) {
<div className="p-4">...Loading</div>;
Expand Down Expand Up @@ -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: {
Expand All @@ -223,7 +226,7 @@ export default function Pregunta() {
),
},
{
query: mutacionCrearEvento,
query: createEventMutation,
variables: JSON.stringify(
{
input: {
Expand All @@ -239,7 +242,7 @@ export default function Pregunta() {
),
},
{
query: mutacionCrearTicket,
query: createTicketMutation,
variables: JSON.stringify(
{
input: {
Expand All @@ -262,7 +265,7 @@ export default function Pregunta() {
),
},
{
query: mutacionDeCreatePurchaseOrder,
query: createPurchaseOrderMutation,
variables: JSON.stringify(
{
input: {
Expand Down
89 changes: 44 additions & 45 deletions app/utils/supabase/AuthProvider/index.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -11,18 +10,15 @@ 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;
isLogged: boolean;
isReady: boolean;
tokenRef: MutableRefObject<string | null>;
setTokenRef: (token: string | null) => void;
refreshSession: () => Promise<void>;
};

export const AuthContext = createContext<AuthContextType>({
Expand All @@ -31,77 +27,79 @@ export const AuthContext = createContext<AuthContextType>({
isReady: false,
tokenRef: { current: null },
setTokenRef: () => {},
refreshSession: async () => {},
});

export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
const [supabaseSession, setSession] = useState<Session | null>(null);
const [user, setUser] = useState<User | null>(null);
const [isReady, setIsReady] = useState(false);
const tokenRef = useRef<string | null>(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 () => {
const {
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 <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
Expand All @@ -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;
Loading

0 comments on commit 3a56e2d

Please sign in to comment.