diff --git a/src/GoTrueApi.ts b/src/GoTrueApi.ts index 6e487741..0bec4087 100644 --- a/src/GoTrueApi.ts +++ b/src/GoTrueApi.ts @@ -7,6 +7,7 @@ import { CookieOptions, User, OpenIDConnectCredentials, + VerifyOTPParams, } from './lib/types' import { COOKIE_OPTIONS } from './lib/constants' import { setCookies, getCookieString } from './lib/cookies' @@ -336,7 +337,7 @@ export default class GoTrueApi { } /** - * Send User supplied Mobile OTP to be verified + * @deprecated Use `verifyOTP` instead! * @param phone The user's phone number WITH international prefix * @param token token that user was sent to their mobile phone * @param redirectTo A URL or mobile address to send the user to after they are confirmed. @@ -364,6 +365,36 @@ export default class GoTrueApi { } } + /** + * Send User supplied Email / Mobile OTP to be verified + * @param email The user's email address + * @param phone The user's phone number WITH international prefix + * @param token token that user was sent to their mobile phone + * @param type verification type that the otp is generated for + * @param redirectTo A URL or mobile address to send the user to after they are confirmed. + */ + async verifyOTP( + { email, phone, token, type = 'sms' }: VerifyOTPParams, + options: { + redirectTo?: string + } = {} + ): Promise<{ data: Session | User | null; error: ApiError | null }> { + try { + const headers = { ...this.headers } + const data = await post( + this.fetch, + `${this.url}/verify`, + { email, phone, token, type, redirect_to: options.redirectTo }, + { headers } + ) + const session = { ...data } + if (session.expires_in) session.expires_at = expiresAt(data.expires_in) + return { data: session, error: null } + } catch (e) { + return { data: null, error: e as ApiError } + } + } + /** * Sends an invite link to an email address. * @param email The email address of the user. diff --git a/src/GoTrueClient.ts b/src/GoTrueClient.ts index fbd6e90b..3491a3c5 100644 --- a/src/GoTrueClient.ts +++ b/src/GoTrueClient.ts @@ -258,12 +258,14 @@ export default class GoTrueClient { /** * Log in a user given a User supplied OTP received via mobile. + * @param email The user's email address. * @param phone The user's phone number. * @param token The user's password. + * @param type The user's verification type. * @param redirectTo A URL or mobile address to send the user to after they are confirmed. */ async verifyOTP( - { phone, token }: VerifyOTPParams, + params: VerifyOTPParams, options: { redirectTo?: string } = {} @@ -275,7 +277,7 @@ export default class GoTrueClient { try { this._removeSession() - const { data, error } = await this.api.verifyMobileOTP(phone, token, options) + const { data, error } = await this.api.verifyOTP(params, options) if (error) { throw error diff --git a/src/lib/types.ts b/src/lib/types.ts index 28cffc70..19a94f4c 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -185,10 +185,21 @@ export interface UserCredentials { oidc?: OpenIDConnectCredentials } -export interface VerifyOTPParams { +export type VerifyOTPParams = VerifyMobileOTPParams | VerifyEmailOTPParams +export interface VerifyMobileOTPParams { + email?: undefined phone: string token: string + type?: MobileOTPType } +export interface VerifyEmailOTPParams { + email: string + phone?: undefined + token: string + type: EmailOTPType +} +export type MobileOTPType = 'sms' +export type EmailOTPType = 'signup' | 'invite' | 'magiclink' | 'recovery' | 'email_change' export interface OpenIDConnectCredentials { id_token: string diff --git a/test/GoTrueApi.test.ts b/test/GoTrueApi.test.ts index 6832dae3..c42f58df 100644 --- a/test/GoTrueApi.test.ts +++ b/test/GoTrueApi.test.ts @@ -4,6 +4,7 @@ import { clientApiAutoConfirmOffSignupsEnabledClient, serviceRoleApiClient, serviceRoleApiClientWithSms, + clientApiAutoConfirmDisabledClient, } from './lib/clients' import { @@ -11,6 +12,7 @@ import { mockUserCredentials, mockAppMetadata, mockUserMetadata, + mockVerificationOTP, } from './lib/utils' import type { Session, User } from '../src/lib/types' @@ -494,4 +496,63 @@ describe('GoTrueApi', () => { }) }) }) + + describe('Email/Phone OTP Verification', () => { + describe('GoTrueClient verifyOTP()', () => { + test('verifyOTP() with non-existent phone number', async () => { + const { phone } = mockUserCredentials() + const otp = mockVerificationOTP() + const { user, error } = await clientApiAutoConfirmDisabledClient.verifyOTP({ + phone: `${phone}`, + token: otp, + }) + + expect(user).toBeNull() + expect(error?.status).toEqual(404) + expect(error?.message).toEqual('User not found') + }) + + test('verifyOTP() with invalid phone number', async () => { + const { phone } = mockUserCredentials() + const otp = mockVerificationOTP() + const { user, error } = await clientApiAutoConfirmDisabledClient.verifyOTP({ + phone: `${phone}-invalid`, + token: otp, + }) + + expect(user).toBeNull() + expect(error?.status).toEqual(422) + expect(error?.message).toEqual('Invalid phone number format') + }) + }) + + describe('GoTrueApi verifyOTP()', () => { + test('verifyOTP() with invalid email', async () => { + const { email } = mockUserCredentials() + const otp = mockVerificationOTP() + const { data, error } = await serviceRoleApiClientWithSms.verifyOTP({ + email: `${email}-@invalid`, + token: otp, + type: 'signup', + }) + + expect(data).toBeNull() + expect(error?.status).toEqual(422) + expect(error?.message).toEqual('Invalid email format') + }) + test('verifyOTP() with invalid phone', async () => { + const { phone } = mockUserCredentials() + const otp = mockVerificationOTP() + const { data, error } = await serviceRoleApiClientWithSms.verifyOTP({ + phone: `${phone}-invalid`, + token: otp, + type: 'sms', + }) + + expect(data).toBeNull() + expect(error?.status).toEqual(422) + expect(error?.message).toEqual('Invalid phone number format') + }) + }) + }) }) diff --git a/test/lib/utils.ts b/test/lib/utils.ts index e6caf15f..65ae5017 100644 --- a/test/lib/utils.ts +++ b/test/lib/utils.ts @@ -33,6 +33,10 @@ export const mockUserCredentials = ( } } +export const mockVerificationOTP = (): string => { + return Math.floor(100000 + Math.random() * 900000).toString() +} + export const mockUserMetadata = () => { return { profile_image: faker.image.avatar(),