Skip to content

Commit

Permalink
feat: added support for multiple schemas and default redirect
Browse files Browse the repository at this point in the history
  • Loading branch information
jerrythomas committed Jan 25, 2024
1 parent 64ae9bd commit e775507
Show file tree
Hide file tree
Showing 6 changed files with 179 additions and 17 deletions.
3 changes: 3 additions & 0 deletions adapters/supabase/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,14 @@
"format": "prettier --write --plugin-search-dir=. .",
"test:ui": "vitest --ui",
"test": "vitest",
"coverage": "vitest --coverage",
"upgrade": "pnpm upgrade --latest && pnpm test:ci",
"release": "pnpm publish --access public"
},
"devDependencies": {
"@jerrythomas/prettier-config": "^1.0.2",
"@sveltejs/vite-plugin-svelte": "^3.0.1",
"@vitest/coverage-v8": "^1.2.1",
"@vitest/ui": "~1.2.1",
"eslint-config-shared": "workspace:*",
"jsdom": "^23.2.0",
Expand Down
115 changes: 115 additions & 0 deletions adapters/supabase/spec/adapter.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { getAdapter, transformResult, parseUrlError } from '../src/adapter.js'

import { describe, it, expect, vi } from 'vitest'
import { pick } from 'ramda'
// import { createClient } from '@supabase/supabase-js'
// import { getAdapter, transformResult, handleSignIn, parseUrlError } from './yourModule' // replace with the actual file name

// Mocks
vi.mock('@supabase/supabase-js', async (importOriginal) => ({
...(await importOriginal()),
createClient: vi.fn().mockReturnValue({
auth: {
signInWithOtp: vi.fn().mockImplementation((input) => {
return {
data: pick(['provider'], input),
credentials: pick(['email', 'provider'], input)
}
}),
signInWithPassword: vi.fn(),
signInWithOAuth: vi.fn(),
signUp: vi.fn(),
signOut: vi.fn(),
onAuthStateChange: vi.fn(() => ({
data: { subscription: { unsubscribe: vi.fn() } }
})),
setSession: vi.fn()
}
})
}))

// vi.mock('@kavach/core', () => ({
// urlHashToParams: vi.fn(),
// }))

describe('getAdapter', () => {
it('should create a client and define auth functions', () => {
const options = {
url: 'http://localhost',
anonKey: 'key',
schema: ['public']
}
const adapter = getAdapter(options)
expect(adapter).toHaveProperty('signIn')
expect(adapter).toHaveProperty('signUp')
expect(adapter).toHaveProperty('signOut')
expect(adapter).toHaveProperty('synchronize')
expect(adapter).toHaveProperty('onAuthChange')
expect(adapter).toHaveProperty('parseUrlError')
// expect(adapter).toHaveProperty('client')
expect(adapter).toHaveProperty('db')
expect(Object.keys(adapter)).toHaveLength(7)
})

it('should handle sign in', async () => {
const options = {
url: 'http://localhost',
anonKey: 'key',
schema: ['public']
}
const adapter = getAdapter(options)
const credentials = { provider: 'magic', email: 'a@b.com' }
const expectedResponse = { data: {}, message: '', type: 'info' }
const result = await adapter.signIn(credentials)
expect(result).toEqual(expectedResponse)
})
})

describe('transformResult', () => {
it('should transform result for successful response', () => {
const result = {
data: { provider: 'email' },
credentials: { email: 'test@example.com' }
}
const transformed = transformResult(result)
expect(transformed).toEqual({
type: 'info',
data: result.data,
message: ''
})
})

it('should transform result for error response', () => {
const result = { error: { message: 'Error', status: 400 }, credentials: {} }
const transformed = transformResult(result)
expect(transformed).toEqual({
type: 'error',
...result.error,
message: 'Server error. Try again later.',
data: result.data
})
})
})

describe('parseUrlError', () => {
it('should return no error if URL hash contains no error information', () => {
const url = { hash: '' }
const result = parseUrlError(url)
expect(result).toEqual({ isError: false })
})

it('should correctly parse the error from URL hash', () => {
const url = {
hash: '#error=invalid_request&error_code=400&error_description=The%20request%20is%20missing%20a%20required%20parameter.'
}
const result = parseUrlError(url)
expect(result).toEqual({
isError: true,
status: '400',
name: 'invalid_request',
message: 'The request is missing a required parameter.'
})
})
})

// ... Additional tests for each function
51 changes: 45 additions & 6 deletions adapters/supabase/src/adapter.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { createClient, AuthApiError } from '@supabase/supabase-js'
import { urlHashToParams } from '@kavach/core'

const defaultOrigin = window ? window.location.origin : ''

/**
* Handles sign in based on the credentials provided
*
Expand All @@ -9,29 +11,34 @@ import { urlHashToParams } from '@kavach/core'
* @returns {Promise<import('@kavach/core').AuthResponse>}
*/
async function handleSignIn(client, credentials) {
const { email, phone, password, provider, scopes, redirectTo } = credentials
const { email, phone, password, provider, scopes } = credentials
const redirectTo = credentials.redirectTo ?? defaultOrigin

let result
if (provider === 'magic') {
const { email } = credentials
result = await client.auth.signInWithOtp({ email })
result = { ...result, credentials: { provider, email } }
} else if (password) {
const options = { emailRedirectTo: redirectTo }
const creds = email
? { email, password, options: { emailRedirectTo: redirectTo } }
: { phone, password, options: { emailRedirectTo: redirectTo } }
? { email, password, options }
: { phone, password, options }

result = await client.auth.signInWithPassword(creds)
} else {
result = await client.auth.signInWithOAuth({
provider,
options: { scopes: scopes.join(' '), redirectTo }
options: {
scopes: scopes.join(' '),
redirectTo
}
})
}
return transformResult(result)
}

function parseUrlError(url) {
export function parseUrlError(url) {
let error = { isError: false }
let result = urlHashToParams(url.hash)
if (result.error) {
Expand All @@ -48,6 +55,11 @@ function parseUrlError(url) {
/** @type {import('./types').GetSupabaseAdapter} */
export function getAdapter(options) {
const client = createClient(options.url, options.anonKey)
const clients = createClientsForSchemas(
options.url,
options.anonKey,
options.schema
)

const signIn = async (credentials) => {
return handleSignIn(client, credentials)
Expand All @@ -70,29 +82,40 @@ export function getAdapter(options) {
const {
data: { subscription }
} = client.auth.onAuthStateChange(async (event, session) => {
await synchronizeClients(session)
await callback(event, session)
})
return () => {
subscription.unsubscribe()
}
}
const synchronize = async (session) => {
await synchronizeClients(session)
return client.auth.setSession(session)
}

const synchronizeClients = async (session) => {
const result = Object.keys(clients).map(async (schema) => {
clients[schema].auth.setSession(session)
})
return Promise.all(result)
}

return {
signIn,
signUp,
signOut,
synchronize,
onAuthChange,
parseUrlError,
client
client,
db: (schema = null) => (schema ? clients[schema] : client)
}
}

/**
* Transforms supabase result into a structure that can be used by kavach
*
* @param {*} result
* @returns
*/
Expand All @@ -117,3 +140,19 @@ export function transformResult({ data, error, credentials }) {
}
}
}

/**
* Generates clients for each schema. This is required to support multiple schemas with supabase
*
* @param {string} url The url of the supabase server
* @param {string} anonKey The anon key of the supabase server
* @param {Array<string>} schemas An array of schemas to create clients for
* @returns
*/
function createClientsForSchemas(url, anonKey, schemas = []) {
const clients = schemas.map((schema) => ({
[schema]: createClient(url, anonKey, { schema })
}))

return clients
}
3 changes: 1 addition & 2 deletions packages/svelte/src/AuthProvider.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@
} else {
await kavach.signIn({
provider: name,
scopes,
redirectTo: window.location.href
scopes
})
}
if (result) {
Expand Down
16 changes: 11 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions starter/supabase/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@
"@fontsource/open-sans": "^5.0.22",
"@fontsource/overpass": "^5.0.16",
"@fontsource/victor-mono": "^5.0.11",
"@kavach/adapter-supabase": "workspace:1.0.0-next.20",
"@kavach/core": "workspace:1.0.0-next.20",
"@kavach/logger": "workspace:1.0.0-next.20",
"@kavach/svelte": "workspace:1.0.0-next.20",
"@kavach/adapter-supabase": "workspace:latest",
"@kavach/core": "workspace:latest",
"@kavach/logger": "workspace:latest",
"@kavach/svelte": "workspace:latest",
"@rokkit/actions": "1.0.0-next.67",
"@rokkit/atoms": "1.0.0-next.67",
"@rokkit/core": "1.0.0-next.67",
Expand Down

0 comments on commit e775507

Please sign in to comment.