Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions src/GoTrueAdminApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
_request,
_userResponse,
} from './lib/fetch'
import { resolveFetch } from './lib/helpers'
import { resolveFetch, validateUUID } from './lib/helpers'
import {
AdminUserAttributes,
GenerateLinkParams,
Expand All @@ -19,6 +19,8 @@ import {
AuthMFAAdminListFactorsParams,
AuthMFAAdminListFactorsResponse,
PageParams,
SIGN_OUT_SCOPES,
SignOutScope,
} from './lib/types'
import { AuthError, isAuthError } from './lib/errors'

Expand Down Expand Up @@ -59,8 +61,14 @@ export default class GoTrueAdminApi {
*/
async signOut(
jwt: string,
scope: 'global' | 'local' | 'others' = 'global'
scope: SignOutScope = SIGN_OUT_SCOPES[0]
): Promise<{ data: null; error: AuthError | null }> {
if (SIGN_OUT_SCOPES.indexOf(scope) < 0) {
throw new Error(
`@supabase/auth-js: Parameter scope must be one of ${SIGN_OUT_SCOPES.join(', ')}`
)
}

try {
await _request(this.fetch, 'POST', `${this.url}/logout?scope=${scope}`, {
headers: this.headers,
Expand Down Expand Up @@ -219,6 +227,8 @@ export default class GoTrueAdminApi {
* This function should only be called on a server. Never expose your `service_role` key in the browser.
*/
async getUserById(uid: string): Promise<UserResponse> {
validateUUID(uid)

try {
return await _request(this.fetch, 'GET', `${this.url}/admin/users/${uid}`, {
headers: this.headers,
Expand All @@ -241,6 +251,8 @@ export default class GoTrueAdminApi {
* This function should only be called on a server. Never expose your `service_role` key in the browser.
*/
async updateUserById(uid: string, attributes: AdminUserAttributes): Promise<UserResponse> {
validateUUID(uid)

try {
return await _request(this.fetch, 'PUT', `${this.url}/admin/users/${uid}`, {
body: attributes,
Expand All @@ -266,6 +278,8 @@ export default class GoTrueAdminApi {
* This function should only be called on a server. Never expose your `service_role` key in the browser.
*/
async deleteUser(id: string, shouldSoftDelete = false): Promise<UserResponse> {
validateUUID(id)

try {
return await _request(this.fetch, 'DELETE', `${this.url}/admin/users/${id}`, {
headers: this.headers,
Expand All @@ -286,6 +300,8 @@ export default class GoTrueAdminApi {
private async _listFactors(
params: AuthMFAAdminListFactorsParams
): Promise<AuthMFAAdminListFactorsResponse> {
validateUUID(params.userId)

try {
const { data, error } = await _request(
this.fetch,
Expand All @@ -311,6 +327,9 @@ export default class GoTrueAdminApi {
private async _deleteFactor(
params: AuthMFAAdminDeleteFactorParams
): Promise<AuthMFAAdminDeleteFactorResponse> {
validateUUID(params.userId)
validateUUID(params.id)

try {
const data = await _request(
this.fetch,
Expand Down
10 changes: 9 additions & 1 deletion src/lib/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { API_VERSION_HEADER_NAME, BASE64URL_REGEX } from './constants'
import { AuthInvalidJwtError } from './errors'
import { base64UrlToUint8Array, stringFromBase64URL, stringToBase64URL } from './base64url'
import { base64UrlToUint8Array, stringFromBase64URL } from './base64url'
import { JwtHeader, JwtPayload, SupportedStorage } from './types'

export function expiresAt(expiresIn: number) {
Expand Down Expand Up @@ -357,3 +357,11 @@ export function getAlgorithm(alg: 'RS256' | 'ES256'): RsaHashedImportParams | Ec
throw new Error('Invalid alg claim')
}
}

const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/

export function validateUUID(str: string) {
if (!UUID_REGEX.test(str)) {
throw new Error('@supabase/auth-js: Expected parameter to be UUID but is not')
}
}
3 changes: 3 additions & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1279,3 +1279,6 @@ export interface JWK {
kid?: string
[key: string]: any
}

export const SIGN_OUT_SCOPES = ['global', 'local', 'others'] as const
export type SignOutScope = typeof SIGN_OUT_SCOPES[number]
38 changes: 22 additions & 16 deletions test/GoTrueApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
import type { GenerateLinkProperties, User } from '../src/lib/types'

const INVALID_EMAIL = 'xx:;[email protected]'
const INVALID_USER_ID = 'invalid-uuid'
const NON_EXISTANT_USER_ID = '83fd9e20-7a80-46e4-bf29-a86e3d6bbf66'

describe('GoTrueAdminApi', () => {
describe('User creation', () => {
Expand Down Expand Up @@ -152,7 +152,7 @@ describe('GoTrueAdminApi', () => {
})

test('getUserById() returns AuthError when user id is invalid', async () => {
const { error, data } = await serviceRoleApiClient.getUserById(INVALID_USER_ID)
const { error, data } = await serviceRoleApiClient.getUserById(NON_EXISTANT_USER_ID)

expect(error).not.toBeNull()
expect(data.user).toBeNull()
Expand Down Expand Up @@ -283,7 +283,7 @@ describe('GoTrueAdminApi', () => {
})

test('deleteUser() returns AuthError when user id is invalid', async () => {
const { error, data } = await serviceRoleApiClient.deleteUser(INVALID_USER_ID)
const { error, data } = await serviceRoleApiClient.deleteUser(NON_EXISTANT_USER_ID)

expect(error).not.toBeNull()
expect(data.user).toBeNull()
Expand Down Expand Up @@ -479,7 +479,7 @@ describe('GoTrueAdminApi', () => {
test('listUsers() returns AuthError when page is invalid', async () => {
const { error, data } = await serviceRoleApiClient.listUsers({
page: -1,
perPage: 10
perPage: 10,
})

expect(error).not.toBeNull()
Expand All @@ -489,8 +489,8 @@ describe('GoTrueAdminApi', () => {

describe('Update User', () => {
test('updateUserById() returns AuthError when user id is invalid', async () => {
const { error, data } = await serviceRoleApiClient.updateUserById(INVALID_USER_ID, {
email: '[email protected]'
const { error, data } = await serviceRoleApiClient.updateUserById(NON_EXISTANT_USER_ID, {
email: '[email protected]',
})

expect(error).not.toBeNull()
Expand All @@ -513,7 +513,7 @@ describe('GoTrueAdminApi', () => {
expect(uid).toBeTruthy()

const { error: enrollError } = await authClientWithSession.mfa.enroll({
factorType: 'totp'
factorType: 'totp',
})
expect(enrollError).toBeNull()

Expand All @@ -526,35 +526,41 @@ describe('GoTrueAdminApi', () => {

const factorId = data?.factors[0].id
expect(factorId).toBeDefined()
const { data: deletedData, error: deletedError } = await serviceRoleApiClient.mfa.deleteFactor({
userId: uid,
id: factorId!
})
const { data: deletedData, error: deletedError } =
await serviceRoleApiClient.mfa.deleteFactor({
userId: uid,
id: factorId!,
})
expect(deletedError).toBeNull()
expect(deletedData).not.toBeNull()
const deletedId = (deletedData as any)?.data?.id
console.log('deletedId:', deletedId)
expect(deletedId).toEqual(factorId)

const { data: latestData, error: latestError } = await serviceRoleApiClient.mfa.listFactors({ userId: uid })
const { data: latestData, error: latestError } = await serviceRoleApiClient.mfa.listFactors({
userId: uid,
})
expect(latestError).toBeNull()
expect(latestData).not.toBeNull()
expect(Array.isArray(latestData?.factors)).toBe(true)
expect(latestData?.factors.length).toEqual(0)
})


test('mfa.listFactors returns AuthError for invalid user', async () => {
const { data, error } = await serviceRoleApiClient.mfa.listFactors({ userId: INVALID_USER_ID })
const { data, error } = await serviceRoleApiClient.mfa.listFactors({
userId: NON_EXISTANT_USER_ID,
})
expect(data).toBeNull()
expect(error).not.toBeNull()
})

test('mfa.deleteFactors returns AuthError for invalid user', async () => {
const { data, error } = await serviceRoleApiClient.mfa.deleteFactor({ userId: INVALID_USER_ID , id: '1' })
const { data, error } = await serviceRoleApiClient.mfa.deleteFactor({
userId: NON_EXISTANT_USER_ID,
id: NON_EXISTANT_USER_ID,
})
expect(data).toBeNull()
expect(error).not.toBeNull()
})

})
})