B2B-88: add starter kit structure and elements
This commit is contained in:
82
packages/supabase/src/hooks/use-auth-change-listener.ts
Normal file
82
packages/supabase/src/hooks/use-auth-change-listener.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import type { AuthChangeEvent, Session } from '@supabase/supabase-js';
|
||||
|
||||
import { useSupabase } from './use-supabase';
|
||||
|
||||
/**
|
||||
* @name PRIVATE_PATH_PREFIXES
|
||||
* @description A list of private path prefixes
|
||||
*/
|
||||
const PRIVATE_PATH_PREFIXES = ['/home', '/admin', '/join', '/update-password'];
|
||||
|
||||
/**
|
||||
* @name AUTH_PATHS
|
||||
* @description A list of auth paths
|
||||
*/
|
||||
const AUTH_PATHS = ['/auth'];
|
||||
|
||||
/**
|
||||
* @name useAuthChangeListener
|
||||
* @param privatePathPrefixes - A list of private path prefixes
|
||||
* @param appHomePath - The path to redirect to when the user is signed out
|
||||
* @param onEvent - Callback function to be called when an auth event occurs
|
||||
*/
|
||||
export function useAuthChangeListener({
|
||||
privatePathPrefixes = PRIVATE_PATH_PREFIXES,
|
||||
appHomePath,
|
||||
onEvent,
|
||||
}: {
|
||||
appHomePath: string;
|
||||
privatePathPrefixes?: string[];
|
||||
onEvent?: (event: AuthChangeEvent, user: Session | null) => void;
|
||||
}) {
|
||||
const client = useSupabase();
|
||||
const pathName = usePathname();
|
||||
|
||||
useEffect(() => {
|
||||
// keep this running for the whole session unless the component was unmounted
|
||||
const listener = client.auth.onAuthStateChange((event, user) => {
|
||||
if (onEvent) {
|
||||
onEvent(event, user);
|
||||
}
|
||||
|
||||
// log user out if user is falsy
|
||||
// and if the current path is a private route
|
||||
const shouldRedirectUser =
|
||||
!user && isPrivateRoute(pathName, privatePathPrefixes);
|
||||
|
||||
if (shouldRedirectUser) {
|
||||
// send user away when signed out
|
||||
window.location.assign('/');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// revalidate user session when user signs in or out
|
||||
if (event === 'SIGNED_OUT') {
|
||||
// sometimes Supabase sends SIGNED_OUT event
|
||||
// but in the auth path, so we ignore it
|
||||
if (AUTH_PATHS.some((path) => pathName.startsWith(path))) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
|
||||
// destroy listener on un-mounts
|
||||
return () => listener.data.subscription.unsubscribe();
|
||||
}, [client.auth, pathName, appHomePath, privatePathPrefixes, onEvent]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a given path is a private route.
|
||||
*/
|
||||
function isPrivateRoute(path: string, privatePathPrefixes: string[]) {
|
||||
return privatePathPrefixes.some((prefix) => path.startsWith(prefix));
|
||||
}
|
||||
25
packages/supabase/src/hooks/use-fetch-mfa-factors.ts
Normal file
25
packages/supabase/src/hooks/use-fetch-mfa-factors.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useSupabase } from './use-supabase';
|
||||
import { useFactorsMutationKey } from './use-user-factors-mutation-key';
|
||||
|
||||
export function useFetchAuthFactors(userId: string) {
|
||||
const client = useSupabase();
|
||||
const queryKey = useFactorsMutationKey(userId);
|
||||
|
||||
const queryFn = async () => {
|
||||
const { data, error } = await client.auth.mfa.listFactors();
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
return useQuery({
|
||||
queryKey,
|
||||
queryFn,
|
||||
staleTime: 0,
|
||||
});
|
||||
}
|
||||
42
packages/supabase/src/hooks/use-request-reset-password.ts
Normal file
42
packages/supabase/src/hooks/use-request-reset-password.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { useSupabase } from './use-supabase';
|
||||
|
||||
interface RequestPasswordResetMutationParams {
|
||||
email: string;
|
||||
redirectTo: string;
|
||||
captchaToken?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @name useRequestResetPassword
|
||||
* @description Requests a password reset for a user. This function will
|
||||
* trigger a password reset email to be sent to the user's email address.
|
||||
* After the user clicks the link in the email, they will be redirected to
|
||||
* /password-reset where their password can be updated.
|
||||
*/
|
||||
export function useRequestResetPassword() {
|
||||
const client = useSupabase();
|
||||
const mutationKey = ['auth', 'reset-password'];
|
||||
|
||||
const mutationFn = async (params: RequestPasswordResetMutationParams) => {
|
||||
const { error, data } = await client.auth.resetPasswordForEmail(
|
||||
params.email,
|
||||
{
|
||||
redirectTo: params.redirectTo,
|
||||
captchaToken: params.captchaToken,
|
||||
},
|
||||
);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
return useMutation({
|
||||
mutationFn,
|
||||
mutationKey,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { SignInWithPasswordCredentials } from '@supabase/supabase-js';
|
||||
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { useSupabase } from './use-supabase';
|
||||
|
||||
export function useSignInWithEmailPassword() {
|
||||
const client = useSupabase();
|
||||
const mutationKey = ['auth', 'sign-in-with-email-password'];
|
||||
|
||||
const mutationFn = async (credentials: SignInWithPasswordCredentials) => {
|
||||
const response = await client.auth.signInWithPassword(credentials);
|
||||
|
||||
if (response.error) {
|
||||
throw response.error.message;
|
||||
}
|
||||
|
||||
const user = response.data?.user;
|
||||
const identities = user?.identities ?? [];
|
||||
|
||||
// if the user has no identities, it means that the email is taken
|
||||
if (identities.length === 0) {
|
||||
throw new Error('User already registered');
|
||||
}
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
return useMutation({ mutationKey, mutationFn });
|
||||
}
|
||||
43
packages/supabase/src/hooks/use-sign-in-with-otp.ts
Normal file
43
packages/supabase/src/hooks/use-sign-in-with-otp.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { SignInWithPasswordlessCredentials } from '@supabase/supabase-js';
|
||||
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { useSupabase } from './use-supabase';
|
||||
|
||||
export function useSignInWithOtp() {
|
||||
const client = useSupabase();
|
||||
const mutationKey = ['auth', 'sign-in-with-otp'];
|
||||
|
||||
const mutationFn = async (credentials: SignInWithPasswordlessCredentials) => {
|
||||
const result = await client.auth.signInWithOtp(credentials);
|
||||
|
||||
if (result.error) {
|
||||
if (shouldIgnoreError(result.error.message)) {
|
||||
console.warn(
|
||||
`Ignoring error during development: ${result.error.message}`,
|
||||
);
|
||||
|
||||
return {} as never;
|
||||
}
|
||||
|
||||
throw result.error.message;
|
||||
}
|
||||
|
||||
return result.data;
|
||||
};
|
||||
|
||||
return useMutation({
|
||||
mutationFn,
|
||||
mutationKey,
|
||||
});
|
||||
}
|
||||
|
||||
export default useSignInWithOtp;
|
||||
|
||||
function shouldIgnoreError(error: string) {
|
||||
return isSmsProviderNotSetupError(error);
|
||||
}
|
||||
|
||||
function isSmsProviderNotSetupError(error: string) {
|
||||
return error.includes(`sms Provider could not be found`);
|
||||
}
|
||||
25
packages/supabase/src/hooks/use-sign-in-with-provider.ts
Normal file
25
packages/supabase/src/hooks/use-sign-in-with-provider.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { SignInWithOAuthCredentials } from '@supabase/supabase-js';
|
||||
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { useSupabase } from './use-supabase';
|
||||
|
||||
export function useSignInWithProvider() {
|
||||
const client = useSupabase();
|
||||
const mutationKey = ['auth', 'sign-in-with-provider'];
|
||||
|
||||
const mutationFn = async (credentials: SignInWithOAuthCredentials) => {
|
||||
const response = await client.auth.signInWithOAuth(credentials);
|
||||
|
||||
if (response.error) {
|
||||
throw response.error.message;
|
||||
}
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
return useMutation({
|
||||
mutationFn,
|
||||
mutationKey,
|
||||
});
|
||||
}
|
||||
13
packages/supabase/src/hooks/use-sign-out.ts
Normal file
13
packages/supabase/src/hooks/use-sign-out.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { useSupabase } from './use-supabase';
|
||||
|
||||
export function useSignOut() {
|
||||
const client = useSupabase();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () => {
|
||||
return client.auth.signOut();
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { useSupabase } from './use-supabase';
|
||||
|
||||
interface Credentials {
|
||||
email: string;
|
||||
password: string;
|
||||
emailRedirectTo: string;
|
||||
captchaToken?: string;
|
||||
}
|
||||
|
||||
export function useSignUpWithEmailAndPassword() {
|
||||
const client = useSupabase();
|
||||
const mutationKey = ['auth', 'sign-up-with-email-password'];
|
||||
|
||||
const mutationFn = async (params: Credentials) => {
|
||||
const { emailRedirectTo, captchaToken, ...credentials } = params;
|
||||
|
||||
const response = await client.auth.signUp({
|
||||
...credentials,
|
||||
options: {
|
||||
emailRedirectTo,
|
||||
captchaToken,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
throw response.error.message;
|
||||
}
|
||||
|
||||
const user = response.data?.user;
|
||||
const identities = user?.identities ?? [];
|
||||
|
||||
// if the user has no identities, it means that the email is taken
|
||||
if (identities.length === 0) {
|
||||
throw new Error('User already registered');
|
||||
}
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
return useMutation({
|
||||
mutationKey,
|
||||
mutationFn,
|
||||
});
|
||||
}
|
||||
8
packages/supabase/src/hooks/use-supabase.ts
Normal file
8
packages/supabase/src/hooks/use-supabase.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { getSupabaseBrowserClient } from '../clients/browser-client';
|
||||
import { Database } from '../database.types';
|
||||
|
||||
export function useSupabase<Db = Database>() {
|
||||
return useMemo(() => getSupabaseBrowserClient<Db>(), []);
|
||||
}
|
||||
31
packages/supabase/src/hooks/use-update-user-mutation.ts
Normal file
31
packages/supabase/src/hooks/use-update-user-mutation.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { UserAttributes } from '@supabase/supabase-js';
|
||||
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { useSupabase } from './use-supabase';
|
||||
|
||||
type Params = UserAttributes & { redirectTo: string };
|
||||
|
||||
export function useUpdateUser() {
|
||||
const client = useSupabase();
|
||||
const mutationKey = ['supabase:user'];
|
||||
|
||||
const mutationFn = async (attributes: Params) => {
|
||||
const { redirectTo, ...params } = attributes;
|
||||
|
||||
const response = await client.auth.updateUser(params, {
|
||||
emailRedirectTo: redirectTo,
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
throw response.error;
|
||||
}
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
return useMutation({
|
||||
mutationKey,
|
||||
mutationFn,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function useFactorsMutationKey(userId: string) {
|
||||
return ['mfa-factors', userId];
|
||||
}
|
||||
35
packages/supabase/src/hooks/use-user.ts
Normal file
35
packages/supabase/src/hooks/use-user.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { User } from '@supabase/supabase-js';
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useSupabase } from './use-supabase';
|
||||
|
||||
const queryKey = ['supabase:user'];
|
||||
|
||||
export function useUser(initialData?: User | null) {
|
||||
const client = useSupabase();
|
||||
|
||||
const queryFn = async () => {
|
||||
const response = await client.auth.getUser();
|
||||
|
||||
// this is most likely a session error or the user is not logged in
|
||||
if (response.error) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (response.data?.user) {
|
||||
return response.data.user;
|
||||
}
|
||||
|
||||
return Promise.reject(new Error('Unexpected result format'));
|
||||
};
|
||||
|
||||
return useQuery({
|
||||
queryFn,
|
||||
queryKey,
|
||||
initialData,
|
||||
refetchInterval: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
26
packages/supabase/src/hooks/use-verify-otp.ts
Normal file
26
packages/supabase/src/hooks/use-verify-otp.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { VerifyOtpParams } from '@supabase/supabase-js';
|
||||
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { useSupabase } from './use-supabase';
|
||||
|
||||
export function useVerifyOtp() {
|
||||
const client = useSupabase();
|
||||
|
||||
const mutationKey = ['verify-otp'];
|
||||
|
||||
const mutationFn = async (params: VerifyOtpParams) => {
|
||||
const { data, error } = await client.auth.verifyOtp(params);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
return useMutation({
|
||||
mutationFn,
|
||||
mutationKey,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user