Skip to content

Commit

Permalink
Feature: Add ssr package to configure cookies for browser or server…
Browse files Browse the repository at this point in the history
… clients (#630)

* move client and server primitives to new ssr package

* remove unused isSecureEnvironment function

* Update with default cookie options

* Fix types for cookie options

* remove type narrowing for cookie options

* Move cookie method types to the types file

* Update cookie storage for browser client

* Update cookie properties for browser client

* Move cookie and ramda to dependencies

---------

Co-authored-by: Andrew Smith <[email protected]>
  • Loading branch information
dijonmusters and silentworks authored Sep 6, 2023
1 parent 59b8970 commit 49f4043
Show file tree
Hide file tree
Showing 16 changed files with 378 additions and 5 deletions.
8 changes: 5 additions & 3 deletions .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "next",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["@example/*"]
}
"ignore": [
"@example/*"
]
}
5 changes: 5 additions & 0 deletions .changeset/swift-drinks-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@supabase/ssr': patch
---

The successor to the auth-helpers packages with sane defaults
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"build:react": "turbo run build --filter=@supabase/auth-helpers-react",
"build:remix": "turbo run build --filter=@supabase/auth-helpers-remix",
"build:shared": "turbo run build --filter=@supabase/auth-helpers-shared",
"build:ssr": "turbo run build --filter=@supabase/ssr",
"dev": "turbo run dev --parallel",
"lint": "turbo run lint --filter=!@example/*",
"check": "prettier --check .",
Expand Down
3 changes: 3 additions & 0 deletions packages/ssr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# @supabase/ssr (BETA)

This submodule provides convenience helpers for implementing user authentication in your favorite framework.
49 changes: 49 additions & 0 deletions packages/ssr/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "@supabase/ssr",
"version": "0.0.0",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"publishConfig": {
"access": "public"
},
"files": [
"dist"
],
"scripts": {
"lint": "tsc",
"build": "tsup"
},
"repository": {
"type": "git",
"url": "git+https://github.com/supabase/auth-helpers.git"
},
"keywords": [
"Supabase",
"Auth",
"Next.js",
"Svelte Kit",
"Remix",
"Express"
],
"author": "Supabase",
"license": "MIT",
"bugs": {
"url": "https://github.com/supabase/auth-helpers/issues"
},
"homepage": "https://github.com/supabase/auth-helpers#readme",
"dependencies": {
"cookie": "^0.5.0",
"ramda": "^0.29.0"
},
"devDependencies": {
"@supabase/supabase-js": "2.33.1",
"@types/cookie": "^0.5.1",
"@types/ramda": "^0.29.3",
"tsconfig": "workspace:*",
"tsup": "^6.7.0"
},
"peerDependencies": {
"@supabase/supabase-js": "^2.33.1"
}
}
3 changes: 3 additions & 0 deletions packages/ssr/src/ambient.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// these variables are defined by tsup
declare const PACKAGE_NAME: string;
declare const PACKAGE_VERSION: string;
139 changes: 139 additions & 0 deletions packages/ssr/src/createBrowserClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { createClient } from '@supabase/supabase-js';
import { mergeDeepRight } from 'ramda';
import { DEFAULT_COOKIE_OPTIONS, isBrowser } from './utils';
import { parse, serialize } from 'cookie';

import type { SupabaseClient } from '@supabase/supabase-js';
import type {
GenericSchema,
SupabaseClientOptions
} from '@supabase/supabase-js/dist/module/lib/types';
import type { BrowserCookieMethods, CookieOptionsWithName } from './types';

let cachedBrowserClient: SupabaseClient<any, string> | undefined;

export function createBrowserClient<
Database = any,
SchemaName extends string & keyof Database = 'public' extends keyof Database
? 'public'
: string & keyof Database,
Schema extends GenericSchema = Database[SchemaName] extends GenericSchema
? Database[SchemaName]
: any
>(
supabaseUrl: string,
supabaseKey: string,
options?: SupabaseClientOptions<SchemaName> & {
cookies: BrowserCookieMethods;
cookieOptions?: CookieOptionsWithName;
isSingleton?: boolean;
}
) {
if (!supabaseUrl || !supabaseKey) {
throw new Error(
`Your project's URL and Key are required to create a Supabase client!\n\nCheck your Supabase project's API settings to find these values\n\nhttps://supabase.com/dashboard/project/_/settings/api`
);
}

let cookies: BrowserCookieMethods = {};
let isSingleton = true;
let cookieOptions: CookieOptionsWithName | undefined;
let userDefinedClientOptions;

if (options) {
({ cookies, isSingleton = true, cookieOptions, ...userDefinedClientOptions } = options);
}

const cookieClientOptions = {
global: {
headers: {
'X-Client-Info': `${PACKAGE_NAME}@${PACKAGE_VERSION}`
}
},
auth: {
flowType: 'pkce',
autoRefreshToken: isBrowser(),
detectSessionInUrl: isBrowser(),
persistSession: true,
storage: {
getItem: async (key: string) => {
if (typeof cookies.get === 'function') {
return (await cookies.get(key)) ?? null;
}

if (isBrowser()) {
const cookie = parse(document.cookie);
return cookie[key];
}
},
setItem: async (key: string, value: string) => {
if (typeof cookies.set === 'function') {
return await cookies.set(key, value, {
...DEFAULT_COOKIE_OPTIONS,
...cookieOptions
});
}

if (isBrowser()) {
document.cookie = serialize(key, value, {
...DEFAULT_COOKIE_OPTIONS,
...cookieOptions
});
}
},
removeItem: async (key: string) => {
if (typeof cookies.remove === 'function') {
return await cookies.remove(key, {
...DEFAULT_COOKIE_OPTIONS,
maxAge: 0,
...cookieOptions
});
}

if (isBrowser()) {
document.cookie = serialize(key, '', {
...DEFAULT_COOKIE_OPTIONS,
maxAge: 0,
...cookieOptions
});
}
}
}
}
};

// Overwrites default client config with any user defined options
const clientOptions = mergeDeepRight(
cookieClientOptions,
userDefinedClientOptions
) as SupabaseClientOptions<SchemaName>;

if (isSingleton) {
// The `Singleton` pattern is the default to simplify the instantiation
// of a Supabase client in the browser - there must only be one

const browser = isBrowser();

if (browser && cachedBrowserClient) {
return cachedBrowserClient as SupabaseClient<Database, SchemaName, Schema>;
}

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

if (browser) {
// The client should only be cached in the browser
cachedBrowserClient = client;
}

return client;
}

// This allows for multiple Supabase clients, which may be required when using
// multiple schemas. The user will be responsible for ensuring a single
// instance of Supabase is used for each schema in the browser.
return createClient<Database, SchemaName, Schema>(supabaseUrl, supabaseKey, clientOptions);
}
70 changes: 70 additions & 0 deletions packages/ssr/src/createServerClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { createClient } from '@supabase/supabase-js';
import { mergeDeepRight } from 'ramda';
import { DEFAULT_COOKIE_OPTIONS, isBrowser } from './utils';

import type {
GenericSchema,
SupabaseClientOptions
} from '@supabase/supabase-js/dist/module/lib/types';
import type { CookieOptionsWithName, ServerCookieMethods } from './types';

export function createServerClient<
Database = any,
SchemaName extends string & keyof Database = 'public' extends keyof Database
? 'public'
: string & keyof Database,
Schema extends GenericSchema = Database[SchemaName] extends GenericSchema
? Database[SchemaName]
: any
>(
supabaseUrl: string,
supabaseKey: string,
options: SupabaseClientOptions<SchemaName> & {
cookies: ServerCookieMethods;
cookieOptions?: CookieOptionsWithName;
}
) {
if (!supabaseUrl || !supabaseKey) {
throw new Error(
`Your project's URL and Key are required to create a Supabase client!\n\nCheck your Supabase project's API settings to find these values\n\nhttps://supabase.com/dashboard/project/_/settings/api`
);
}

const { cookies, cookieOptions, ...userDefinedClientOptions } = options;

if (!cookies.get || !cookies.set || !cookies.remove) {
// todo: point to helpful docs in error message, once they have been written! 😏
throw new Error(
'The Supabase client requires functions to get, set, and remove cookies in your specific framework!'
);
}

const cookieClientOptions = {
global: {
headers: {
'X-Client-Info': `${PACKAGE_NAME}@${PACKAGE_VERSION}`
}
},
auth: {
flowType: 'pkce',
autoRefreshToken: isBrowser(),
detectSessionInUrl: isBrowser(),
persistSession: true,
storage: {
getItem: async (key: string) => (await cookies.get(key)) ?? null,
setItem: async (key: string, value: string) =>
await cookies.set(key, value, { ...DEFAULT_COOKIE_OPTIONS, ...cookieOptions }),
removeItem: async (key: string) =>
await cookies.remove(key, { ...DEFAULT_COOKIE_OPTIONS, maxAge: 0, ...cookieOptions })
}
}
};

// Overwrites default client config with any user defined options
const clientOptions = mergeDeepRight(
cookieClientOptions,
userDefinedClientOptions
) as SupabaseClientOptions<SchemaName>;

return createClient<Database, SchemaName, Schema>(supabaseUrl, supabaseKey, clientOptions);
}
4 changes: 4 additions & 0 deletions packages/ssr/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './createBrowserClient';
export * from './createServerClient';
export * from './types';
export * from './utils';
14 changes: 14 additions & 0 deletions packages/ssr/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { CookieSerializeOptions } from 'cookie';

export type CookieOptions = Partial<CookieSerializeOptions>;
export type CookieOptionsWithName = { name?: string } & CookieOptions;
export type ServerCookieMethods = {
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 BrowserCookieMethods = {
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;
};
7 changes: 7 additions & 0 deletions packages/ssr/src/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { CookieOptions } from '../types';

export const DEFAULT_COOKIE_OPTIONS: CookieOptions = {
path: '/',
maxAge: 60 * 60 * 24 * 365 * 1000,
httpOnly: false
};
5 changes: 5 additions & 0 deletions packages/ssr/src/utils/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export { parse, serialize } from 'cookie';

export function isBrowser() {
return typeof window !== 'undefined' && typeof window.document !== 'undefined';
}
2 changes: 2 additions & 0 deletions packages/ssr/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './helpers';
export * from './constants';
5 changes: 5 additions & 0 deletions packages/ssr/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extends": "tsconfig/base.json",
"include": ["src"],
"exclude": ["node_modules"]
}
20 changes: 20 additions & 0 deletions packages/ssr/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Options } from 'tsup';
import pkg from './package.json';

export const tsup: Options = {
dts: true,
entryPoints: ['src/index.ts'],
external: ['react', 'next', /^@supabase\//],
format: ['cjs', 'esm'],
// inject: ['src/react-shim.js'],
// ! .cjs/.mjs doesn't work with Angular's webpack4 config by default!
legacyOutput: false,
sourcemap: true,
splitting: false,
bundle: true,
clean: true,
define: {
PACKAGE_NAME: JSON.stringify(pkg.name),
PACKAGE_VERSION: JSON.stringify(pkg.version)
}
};
Loading

0 comments on commit 49f4043

Please sign in to comment.