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

Remove cookie and centralize token refresh #40

Merged
merged 2 commits into from
Jul 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member

Choose a reason for hiding this comment

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

Love all these changes.

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
Loading