284 lines
7.1 KiB
TypeScript
284 lines
7.1 KiB
TypeScript
import { API_VERSIONS, API_VERSION_HEADER_NAME } from './constants'
|
|
import { expiresAt, looksLikeFetchResponse, parseResponseAPIVersion } from './helpers'
|
|
import {
|
|
AuthResponse,
|
|
AuthResponsePassword,
|
|
SSOResponse,
|
|
GenerateLinkProperties,
|
|
GenerateLinkResponse,
|
|
User,
|
|
UserResponse,
|
|
} from './types'
|
|
import {
|
|
AuthApiError,
|
|
AuthRetryableFetchError,
|
|
AuthWeakPasswordError,
|
|
AuthUnknownError,
|
|
AuthSessionMissingError,
|
|
} from './errors'
|
|
|
|
export type Fetch = typeof fetch
|
|
|
|
export interface FetchOptions {
|
|
headers?: {
|
|
[key: string]: string
|
|
}
|
|
noResolveJson?: boolean
|
|
}
|
|
|
|
export interface FetchParameters {
|
|
signal?: AbortSignal
|
|
}
|
|
|
|
export type RequestMethodType = 'GET' | 'POST' | 'PUT' | 'DELETE'
|
|
|
|
const _getErrorMessage = (err: any): string =>
|
|
err.msg || err.message || err.error_description || err.error || JSON.stringify(err)
|
|
|
|
const NETWORK_ERROR_CODES = [502, 503, 504]
|
|
|
|
export async function handleError(error: unknown) {
|
|
if (!looksLikeFetchResponse(error)) {
|
|
throw new AuthRetryableFetchError(_getErrorMessage(error), 0)
|
|
}
|
|
|
|
if (NETWORK_ERROR_CODES.includes(error.status)) {
|
|
// status in 500...599 range - server had an error, request might be retryed.
|
|
throw new AuthRetryableFetchError(_getErrorMessage(error), error.status)
|
|
}
|
|
|
|
let data: any
|
|
try {
|
|
data = await error.json()
|
|
} catch (e: any) {
|
|
throw new AuthUnknownError(_getErrorMessage(e), e)
|
|
}
|
|
|
|
let errorCode: string | undefined = undefined
|
|
|
|
const responseAPIVersion = parseResponseAPIVersion(error)
|
|
if (
|
|
responseAPIVersion &&
|
|
responseAPIVersion.getTime() >= API_VERSIONS['2024-01-01'].timestamp &&
|
|
typeof data === 'object' &&
|
|
data &&
|
|
typeof data.code === 'string'
|
|
) {
|
|
errorCode = data.code
|
|
} else if (typeof data === 'object' && data && typeof data.error_code === 'string') {
|
|
errorCode = data.error_code
|
|
}
|
|
|
|
if (!errorCode) {
|
|
// Legacy support for weak password errors, when there were no error codes
|
|
if (
|
|
typeof data === 'object' &&
|
|
data &&
|
|
typeof data.weak_password === 'object' &&
|
|
data.weak_password &&
|
|
Array.isArray(data.weak_password.reasons) &&
|
|
data.weak_password.reasons.length &&
|
|
data.weak_password.reasons.reduce((a: boolean, i: any) => a && typeof i === 'string', true)
|
|
) {
|
|
throw new AuthWeakPasswordError(
|
|
_getErrorMessage(data),
|
|
error.status,
|
|
data.weak_password.reasons
|
|
)
|
|
}
|
|
} else if (errorCode === 'weak_password') {
|
|
throw new AuthWeakPasswordError(
|
|
_getErrorMessage(data),
|
|
error.status,
|
|
data.weak_password?.reasons || []
|
|
)
|
|
} else if (errorCode === 'session_not_found') {
|
|
// The `session_id` inside the JWT does not correspond to a row in the
|
|
// `sessions` table. This usually means the user has signed out, has been
|
|
// deleted, or their session has somehow been terminated.
|
|
throw new AuthSessionMissingError()
|
|
}
|
|
|
|
throw new AuthApiError(_getErrorMessage(data), error.status || 500, errorCode)
|
|
}
|
|
|
|
const _getRequestParams = (
|
|
method: RequestMethodType,
|
|
options?: FetchOptions,
|
|
parameters?: FetchParameters,
|
|
body?: object
|
|
) => {
|
|
const params: { [k: string]: any } = { method, headers: options?.headers || {} }
|
|
|
|
if (method === 'GET') {
|
|
return params
|
|
}
|
|
|
|
params.headers = { 'Content-Type': 'application/json;charset=UTF-8', ...options?.headers }
|
|
params.body = JSON.stringify(body)
|
|
return { ...params, ...parameters }
|
|
}
|
|
|
|
interface GotrueRequestOptions extends FetchOptions {
|
|
jwt?: string
|
|
redirectTo?: string
|
|
body?: object
|
|
query?: { [key: string]: string }
|
|
/**
|
|
* Function that transforms api response from gotrue into a desirable / standardised format
|
|
*/
|
|
xform?: (data: any) => any
|
|
}
|
|
|
|
export async function _request(
|
|
fetcher: Fetch,
|
|
method: RequestMethodType,
|
|
url: string,
|
|
options?: GotrueRequestOptions
|
|
) {
|
|
const headers = {
|
|
...options?.headers,
|
|
}
|
|
|
|
if (!headers[API_VERSION_HEADER_NAME]) {
|
|
headers[API_VERSION_HEADER_NAME] = API_VERSIONS['2024-01-01'].name
|
|
}
|
|
|
|
if (options?.jwt) {
|
|
headers['Authorization'] = `Bearer ${options.jwt}`
|
|
}
|
|
|
|
const qs = options?.query ?? {}
|
|
if (options?.redirectTo) {
|
|
qs['redirect_to'] = options.redirectTo
|
|
}
|
|
|
|
const queryString = Object.keys(qs).length ? '?' + new URLSearchParams(qs).toString() : ''
|
|
const data = await _handleRequest(
|
|
fetcher,
|
|
method,
|
|
url + queryString,
|
|
{
|
|
headers,
|
|
noResolveJson: options?.noResolveJson,
|
|
},
|
|
{},
|
|
options?.body
|
|
)
|
|
return options?.xform ? options?.xform(data) : { data: { ...data }, error: null }
|
|
}
|
|
|
|
async function _handleRequest(
|
|
fetcher: Fetch,
|
|
method: RequestMethodType,
|
|
url: string,
|
|
options?: FetchOptions,
|
|
parameters?: FetchParameters,
|
|
body?: object
|
|
): Promise<any> {
|
|
const requestParams = _getRequestParams(method, options, parameters, body)
|
|
|
|
let result: any
|
|
|
|
try {
|
|
result = await fetcher(url, {
|
|
...requestParams,
|
|
})
|
|
} catch (e) {
|
|
console.error(e)
|
|
|
|
// fetch failed, likely due to a network or CORS error
|
|
throw new AuthRetryableFetchError(_getErrorMessage(e), 0)
|
|
}
|
|
|
|
if (!result.ok) {
|
|
await handleError(result)
|
|
}
|
|
|
|
if (options?.noResolveJson) {
|
|
return result
|
|
}
|
|
|
|
try {
|
|
return await result.json()
|
|
} catch (e: any) {
|
|
await handleError(e)
|
|
}
|
|
}
|
|
|
|
export function _sessionResponse(data: any): AuthResponse {
|
|
let session = null
|
|
if (hasSession(data)) {
|
|
session = { ...data }
|
|
|
|
if (!data.expires_at) {
|
|
session.expires_at = expiresAt(data.expires_in)
|
|
}
|
|
}
|
|
|
|
const user: User = data.user ?? (data as User)
|
|
return { data: { session, user }, error: null }
|
|
}
|
|
|
|
export function _sessionResponsePassword(data: any): AuthResponsePassword {
|
|
const response = _sessionResponse(data) as AuthResponsePassword
|
|
|
|
if (
|
|
!response.error &&
|
|
data.weak_password &&
|
|
typeof data.weak_password === 'object' &&
|
|
Array.isArray(data.weak_password.reasons) &&
|
|
data.weak_password.reasons.length &&
|
|
data.weak_password.message &&
|
|
typeof data.weak_password.message === 'string' &&
|
|
data.weak_password.reasons.reduce((a: boolean, i: any) => a && typeof i === 'string', true)
|
|
) {
|
|
response.data.weak_password = data.weak_password
|
|
}
|
|
|
|
return response
|
|
}
|
|
|
|
export function _userResponse(data: any): UserResponse {
|
|
const user: User = data.user ?? (data as User)
|
|
return { data: { user }, error: null }
|
|
}
|
|
|
|
export function _ssoResponse(data: any): SSOResponse {
|
|
return { data, error: null }
|
|
}
|
|
|
|
export function _generateLinkResponse(data: any): GenerateLinkResponse {
|
|
const { action_link, email_otp, hashed_token, redirect_to, verification_type, ...rest } = data
|
|
|
|
const properties: GenerateLinkProperties = {
|
|
action_link,
|
|
email_otp,
|
|
hashed_token,
|
|
redirect_to,
|
|
verification_type,
|
|
}
|
|
|
|
const user: User = { ...rest }
|
|
return {
|
|
data: {
|
|
properties,
|
|
user,
|
|
},
|
|
error: null,
|
|
}
|
|
}
|
|
|
|
export function _noResolveJsonResponse(data: any): Response {
|
|
return data
|
|
}
|
|
|
|
/**
|
|
* hasSession checks if the response object contains a valid session
|
|
* @param data A response object
|
|
* @returns true if a session is in the response
|
|
*/
|
|
function hasSession(data: any): boolean {
|
|
return data.access_token && data.refresh_token && data.expires_in
|
|
}
|