Skip to content

Commit

Permalink
feat: fix ssr cookie handling in all edge cases
Browse files Browse the repository at this point in the history
  • Loading branch information
hf committed May 15, 2024
1 parent 627c57c commit db513c2
Show file tree
Hide file tree
Showing 3 changed files with 198 additions and 63 deletions.
4 changes: 2 additions & 2 deletions packages/ssr/src/createBrowserClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export function createBrowserClient<
await deleteChunks(
key,
async (chunkName) => {
if (typeof cookies.get === 'function') {
if ('get' in cookies && typeof cookies.get === 'function') {
return await cookies.get(chunkName);
}
if (isBrowser()) {
Expand All @@ -71,7 +71,7 @@ export function createBrowserClient<
}
},
async (chunkName) => {
if (typeof cookies.remove === 'function') {
if ('remove' in cookies.remove && typeof cookies.remove === 'function') {
await cookies.remove(chunkName, {
...DEFAULT_COOKIE_OPTIONS,
...cookieOptions,
Expand Down
234 changes: 178 additions & 56 deletions packages/ssr/src/createServerClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,25 +46,109 @@ export function createServerClient<
};
}

const deleteAllChunks = async (key: string) => {
await deleteChunks(
key,
async (chunkName) => {
if (typeof cookies.get === 'function') {
return await cookies.get(chunkName);
let storage: any;

if ('get' in cookies) {
const deleteAllChunks = async (key: string) => {
await deleteChunks(
key,
async (chunkName) => {
if (typeof cookies.get === 'function') {
return await cookies.get(chunkName);
}
},
async (chunkName) => {
if (typeof cookies.remove === 'function') {
return await cookies.remove(chunkName, {
...DEFAULT_COOKIE_OPTIONS,
...cookieOptions,
maxAge: 0
});
}
}
);
};

storage = {
// to signal to the libraries that these cookies are coming from a server environment and their value should not be trusted
isServer: true,
getItem: async (key: string) => {
const chunkedCookie = await combineChunks(key, async (chunkName: string) => {
if (typeof cookies.get === 'function') {
return await cookies.get(chunkName);
}
});
return chunkedCookie;
},
async (chunkName) => {
if (typeof cookies.remove === 'function') {
return await cookies.remove(chunkName, {
...DEFAULT_COOKIE_OPTIONS,
...cookieOptions,
maxAge: 0
});
setItem: async (key: string, value: string) => {
if (typeof cookies.set === 'function') {
// first delete all chunks so that there would be no overlap
await deleteAllChunks(key);

const chunks = createChunks(key, value);

for (let i = 0; i < chunks.length; i += 1) {
const chunk = chunks[i];

await cookies.set(chunk.name, chunk.value, {
...DEFAULT_COOKIE_OPTIONS,
...cookieOptions,
maxAge: DEFAULT_COOKIE_OPTIONS.maxAge
});
}
}
},
removeItem: async (key: string) => {
if (typeof cookies.remove === 'function' && typeof cookies.get !== 'function') {
console.log(
'Removing chunked cookie without a `get` method is not supported.\n\n\tWhen you call the `createServerClient` function from the `@supabase/ssr` package, make sure you declare both a `get` and `remove` method on the `cookies` object.\n\nhttps://supabase.com/docs/guides/auth/server-side/creating-a-client'
);
return;
}

await deleteAllChunks(key);
}
};
}

const setItems: { [key: string]: string } = {};
const removedItems: { [key: string]: boolean } = {};
if ('getAll' in cookies) {
storage = {
// to signal to the libraries that these cookies are coming from a server environment and their value should not be trusted
isServer: true,
getItem: async (key: string) => {
if (typeof setItems[key] === 'string') {
return setItems[key];
}

if (removedItems[key]) {
return null;
}

const allCookies = await cookies.getAll();
const chunkedCookie = await combineChunks(key, async (chunkName: string) => {
const cookie = allCookies?.find(({ name }) => name === chunkName) || null;

if (!cookie) {
return null;
}

return cookie.value;
});

return chunkedCookie;
},
setItem: async (key: string, value: string) => {
setItems[key] = value;
delete removedItems[key];
},
removeItem: async (key: string) => {
delete setItems[key];
removedItems[key] = true;
}
);
};
};
}

const cookieClientOptions = {
global: {
Expand All @@ -77,46 +161,7 @@ export function createServerClient<
autoRefreshToken: isBrowser(),
detectSessionInUrl: isBrowser(),
persistSession: true,
storage: {
// to signal to the libraries that these cookies are coming from a server environment and their value should not be trusted
isServer: true,
getItem: async (key: string) => {
const chunkedCookie = await combineChunks(key, async (chunkName: string) => {
if (typeof cookies.get === 'function') {
return await cookies.get(chunkName);
}
});
return chunkedCookie;
},
setItem: async (key: string, value: string) => {
if (typeof cookies.set === 'function') {
// first delete all chunks so that there would be no overlap
await deleteAllChunks(key);

const chunks = createChunks(key, value);

for (let i = 0; i < chunks.length; i += 1) {
const chunk = chunks[i];

await cookies.set(chunk.name, chunk.value, {
...DEFAULT_COOKIE_OPTIONS,
...cookieOptions,
maxAge: DEFAULT_COOKIE_OPTIONS.maxAge
});
}
}
},
removeItem: async (key: string) => {
if (typeof cookies.remove === 'function' && typeof cookies.get !== 'function') {
console.log(
'Removing chunked cookie without a `get` method is not supported.\n\n\tWhen you call the `createServerClient` function from the `@supabase/ssr` package, make sure you declare both a `get` and `remove` method on the `cookies` object.\n\nhttps://supabase.com/docs/guides/auth/server-side/creating-a-client'
);
return;
}

await deleteAllChunks(key);
}
}
storage
}
};

Expand All @@ -126,5 +171,82 @@ export function createServerClient<
userDefinedClientOptions
) as SupabaseClientOptions<SchemaName>;

return createClient<Database, SchemaName, Schema>(supabaseUrl, supabaseKey, clientOptions);
const client = createClient<Database, SchemaName, Schema>(
supabaseUrl,
supabaseKey,
clientOptions
);

if ('getAll' in cookies) {
client.auth.onAuthStateChange(async (event) => {
if (event === 'TOKEN_REFRESHED' || event === 'USER_UPDATED' || event === 'SIGNED_OUT') {
if (typeof cookies.setAll !== 'function') {
console.log('You are holding it wrong!!!!!');
}

const allCookies = await cookies.getAll();
const cookieNames = allCookies?.map(({ name }) => name) || [];

const removeCookies = cookieNames.filter((name) => {
if (removedItems[name]) {
return true;
}

const chunkLike = name.match(/^(.*)[.](0|[1-9][0-9]*)$/);
if (chunkLike && removedItems[chunkLike[1]]) {
return true;
}

return false;
});

const setCookies = Object.keys(setItems).flatMap((itemName) => {
const removeExistingCookiesForItem = new Set(
cookieNames.filter((name) => {
if (name === itemName) {
return true;
}

const chunkLike = name.match(/^(.*)[.](0|[1-9][0-9]*)$/);
if (chunkLike && chunkLike[1] === itemName) {
return true;
}

return false;
})
);

const chunks = createChunks(itemName, setItems[itemName]);

chunks.forEach((chunk) => {
removeExistingCookiesForItem.delete(chunk.name);
});

removeCookies.push(...removeExistingCookiesForItem);

return chunks;
});

const removeCookieOptions = {
...DEFAULT_COOKIE_OPTIONS,
...cookieOptions,
maxAge: 0
};
const setCookieOptions = {
...DEFAULT_COOKIE_OPTIONS,
...cookieOptions,
maxAge: DEFAULT_COOKIE_OPTIONS.maxAge
};

await cookies.setAll(
[].concat(
removeCookies.map((name) => ({ name, value: '', options: removeCookieOptions })),
setCookies.map(({ name, value }) => ({ name, value, options: setCookieOptions }))
)
);
}
});
}

return client;
}
23 changes: 18 additions & 5 deletions packages/ssr/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,21 @@ import type { CookieSerializeOptions } from 'cookie';

export type CookieOptions = Partial<CookieSerializeOptions>;
export type CookieOptionsWithName = { name?: string } & CookieOptions;
export type CookieMethods = {
get?: (key: string) => Promise<string | null | undefined> | string | null | undefined;
set?: (key: string, value: string, options: CookieOptions) => Promise<void> | void;
remove?: (key: string, options: CookieOptions) => Promise<void> | void;
};
export type CookieMethods =
| {
/** @deprecated Move to using `getAll` instead. */
get: (key: string) => Promise<string | null | undefined> | string | null | undefined;
/** @deprecated Move to using `setAll` instead. */
set?: (key: string, value: string, options: CookieOptions) => Promise<void> | void;
/** @deprecated Move to using `setAll` instead. */
remove?: (key: string, options: CookieOptions) => Promise<void> | void;
}
| {
getAll: () =>
| Promise<{ name: string; value: string }[] | null>
| { name: string; value: string }[]
| null;
setAll?: (
cookies: { key: string; value: string; options: CookieOptions }[]
) => Promise<void> | void;
};

0 comments on commit db513c2

Please sign in to comment.