UNPKG

@saas-ui/supabase

Version:

Saas UI Supabase Auth integration

285 lines (257 loc) 7.03 kB
import type { AuthParams, AuthOptions, AuthStateChangeCallback, AuthProviderProps, } from '@saas-ui/auth-provider' import type { AuthChangeEvent, AuthResponse, OAuthResponse, Provider, Session, User, SupabaseClient, VerifyEmailOtpParams, VerifyMobileOtpParams, SignOut, } from '@supabase/supabase-js' interface RecoveryParams { access_token?: string refresh_token?: string expires_in?: string token_type?: string type?: string } interface OtpParams extends AuthParams { otp: string } interface SupabaseServiceAuthOptions { loginOptions?: { data?: object /** A URL to send the user to after they are confirmed. */ redirectTo?: string /** A space-separated list of scopes granted to the OAuth application. */ scopes?: string /** An object of query params */ queryParams?: { [key: string]: string } /** Verification token received when the user completes the captcha on the site. */ captchaToken?: string /** The redirect url embedded in the email link */ emailRedirectTo?: string /** If set to false, this method will not create a new user. Defaults to true. */ shouldCreateUser?: boolean } signupOptions?: { emailRedirectTo?: string /** * A custom data object to store the user's metadata. This maps to the `auth.users.user_metadata` column. * * The `data` should be a JSON object that includes user-specific info, such as their first and last name. */ data?: object /** Verification token received when the user completes the captcha on the site. */ captchaToken?: string } verifyOptions?: { /** A URL to send the user to after they are confirmed. */ redirectTo?: string /** Verification token received when the user completes the captcha on the site. */ captchaToken?: string } resetPasswordOptions?: { redirectTo?: string captchaToken?: string } } export const createAuthService = <Client extends SupabaseClient>( supabase: Client, serviceOptions?: SupabaseServiceAuthOptions ): AuthProviderProps<User> => { const onLogin = async ( params: AuthParams, authOptions?: AuthOptions<{ data?: object captchaToken?: string scopes?: string }> ) => { const options = { ...serviceOptions?.loginOptions, ...authOptions, emailRedirectTo: authOptions?.redirectTo, } function authenticate() { const { email, password, provider, phone } = params if (email && password) { return supabase.auth.signInWithPassword({ email, password, options, }) } else if (email) { return supabase.auth.signInWithOtp({ email, options }) } else if (provider) { return supabase.auth.signInWithOAuth({ provider: provider as Provider, options, }) } else if (phone && password) { return supabase.auth.signInWithPassword({ phone, password, options }) } else if (phone) { return supabase.auth.signInWithOtp({ phone, options }) } throw new Error('Could not find correct authentication method') } const resp = await authenticate() if (resp.error) { throw resp.error } if (isOauthResponse(resp)) { // do nothing, supabase will redirect return } return resp.data.user } const onSignup = async ( params: AuthParams, authOptions?: AuthOptions<{ captchaToken?: string emailRedirectTo?: string data?: object }> ) => { async function signup() { const { email, phone, password } = params const options = { ...serviceOptions?.signupOptions, ...authOptions, emailRedirectTo: authOptions?.redirectTo, } if (email && password) { return await supabase.auth.signUp({ email, password, options, }) } else if (phone && password) { return await supabase.auth.signUp({ phone, password, options, }) } else if (email) { return supabase.auth.signInWithOtp({ email, options }) } else if (phone) { return supabase.auth.signInWithOtp({ phone, options }) } } const resp = await signup() if (resp?.error) { throw resp.error } return resp?.data.user } const onVerifyOtp = async ( params: OtpParams, options?: AuthOptions<{ captchaToken?: string }> ) => { const { email, phone, otp, type } = params if (email) { const verify: VerifyEmailOtpParams = { email, token: otp, type: type || 'signup', options: { ...serviceOptions?.verifyOptions, ...options, }, } const resp = await supabase.auth.verifyOtp(verify) if (resp.error) { throw resp.error } return Boolean(resp.data.session) } if (phone) { const verify: VerifyMobileOtpParams = { phone, token: otp, type: type || 'sms', options: { ...serviceOptions?.verifyOptions, ...options, }, } const resp = await supabase.auth.verifyOtp(verify) if (resp.error) { throw resp.error } return Boolean(resp.data.session) } throw new Error('You need to provide either email or phone') } const onLogout = async (options?: AuthOptions<SignOut>) => { return await supabase.auth.signOut(options) } const onAuthStateChange = (callback: AuthStateChangeCallback<User>) => { const { data } = supabase.auth.onAuthStateChange( (event: AuthChangeEvent, session: Session | null) => { callback(session?.user) } ) return () => data?.subscription.unsubscribe() } const onLoadUser = async () => { const { data, error } = await supabase.auth.getUser() if (error) { throw error } return data.user } const onGetToken = async () => { const { data, error } = await supabase.auth.getSession() if (error) { throw error } return data.session?.access_token || null } const onResetPassword = async ( { email }: Required<Pick<AuthParams, 'email'>>, options?: AuthOptions ) => { const { error } = await supabase.auth.resetPasswordForEmail(email, { ...serviceOptions?.resetPasswordOptions, ...options, }) if (error) { throw error } } const onUpdatePassword = async ({ password, }: Required<Pick<AuthParams, 'password'>>) => { const { error } = await supabase.auth.updateUser({ password, }) if (error) { throw error } } return { onLogin, onSignup, onVerifyOtp, onLogout, onAuthStateChange, onLoadUser, onGetToken, onResetPassword, onUpdatePassword, } } function isOauthResponse( response: AuthResponse | OAuthResponse ): response is OAuthResponse { return Boolean((response as OAuthResponse).data?.provider) }