Add OTP sign-in option + Account Linking (#276)
* feat(accounts): allow linking email password * feat(auth): add OTP sign-in * refactor(accounts): remove 'sonner' dependency and update toast imports * feat(supabase): enable analytics and configure database seeding * feat(auth): update email templates and add OTP template * feat(auth): add last sign in method hints * feat(config): add devIndicators position to bottom-right * feat(auth): implement comprehensive last authentication method tracking tests
This commit is contained in:
committed by
GitHub
parent
856e9612c4
commit
9033155fcd
@@ -0,0 +1,50 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { useSupabase } from './use-supabase';
|
||||
import { USER_IDENTITIES_QUERY_KEY } from './use-user-identities';
|
||||
|
||||
interface Credentials {
|
||||
email: string;
|
||||
password: string;
|
||||
redirectTo: string;
|
||||
}
|
||||
|
||||
export function useLinkIdentityWithEmailPassword() {
|
||||
const client = useSupabase();
|
||||
const queryClient = useQueryClient();
|
||||
const mutationKey = ['auth', 'link-email-password'];
|
||||
|
||||
const mutationFn = async (credentials: Credentials) => {
|
||||
const { email, password, redirectTo } = credentials;
|
||||
|
||||
const { error } = await client.auth.updateUser(
|
||||
{
|
||||
email,
|
||||
password,
|
||||
data: {
|
||||
// This is used to indicate that the user has a password set
|
||||
// because Supabase does not add the identity after setting a password
|
||||
// if the user was created with oAuth
|
||||
hasPassword: true,
|
||||
},
|
||||
},
|
||||
{ emailRedirectTo: redirectTo },
|
||||
);
|
||||
|
||||
if (error) {
|
||||
throw error.message ?? error;
|
||||
}
|
||||
|
||||
await client.auth.refreshSession();
|
||||
};
|
||||
|
||||
return useMutation({
|
||||
mutationKey,
|
||||
mutationFn,
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: USER_IDENTITIES_QUERY_KEY,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { Provider } from '@supabase/supabase-js';
|
||||
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { useSupabase } from './use-supabase';
|
||||
|
||||
export function useLinkIdentityWithProvider(
|
||||
props: {
|
||||
redirectToPath?: string;
|
||||
} = {},
|
||||
) {
|
||||
const client = useSupabase();
|
||||
const mutationKey = ['auth', 'link-identity'];
|
||||
|
||||
const mutationFn = async (provider: Provider) => {
|
||||
const origin = window.location.origin;
|
||||
const redirectToPath = props.redirectToPath ?? '/home/settings';
|
||||
|
||||
const url = new URL('/auth/callback', origin);
|
||||
url.searchParams.set('redirectTo', redirectToPath);
|
||||
|
||||
const { error: linkError } = await client.auth.linkIdentity({
|
||||
provider,
|
||||
options: {
|
||||
redirectTo: url.toString(),
|
||||
},
|
||||
});
|
||||
|
||||
if (linkError) {
|
||||
throw linkError.message ?? linkError;
|
||||
}
|
||||
|
||||
await client.auth.refreshSession();
|
||||
};
|
||||
|
||||
return useMutation({ mutationKey, mutationFn });
|
||||
}
|
||||
22
packages/supabase/src/hooks/use-unlink-identity.ts
Normal file
22
packages/supabase/src/hooks/use-unlink-identity.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { UserIdentity } from '@supabase/supabase-js';
|
||||
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
|
||||
import { useSupabase } from './use-supabase';
|
||||
|
||||
export function useUnlinkIdentity() {
|
||||
const client = useSupabase();
|
||||
const mutationKey = ['auth', 'unlink-identity'];
|
||||
|
||||
const mutationFn = async (identity: UserIdentity) => {
|
||||
const { error } = await client.auth.unlinkIdentity(identity);
|
||||
|
||||
if (error) {
|
||||
throw error.message ?? error;
|
||||
}
|
||||
|
||||
await client.auth.refreshSession();
|
||||
};
|
||||
|
||||
return useMutation({ mutationKey, mutationFn });
|
||||
}
|
||||
30
packages/supabase/src/hooks/use-unlink-user-identity.ts
Normal file
30
packages/supabase/src/hooks/use-unlink-user-identity.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { UserIdentity } from '@supabase/supabase-js';
|
||||
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { useSupabase } from './use-supabase';
|
||||
import { USER_IDENTITIES_QUERY_KEY } from './use-user-identities';
|
||||
|
||||
export function useUnlinkUserIdentity() {
|
||||
const supabase = useSupabase();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (identity: UserIdentity) => {
|
||||
// Unlink the identity
|
||||
const { error } = await supabase.auth.unlinkIdentity(identity);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return identity;
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Invalidate and refetch user identities
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: USER_IDENTITIES_QUERY_KEY,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
56
packages/supabase/src/hooks/use-user-identities.ts
Normal file
56
packages/supabase/src/hooks/use-user-identities.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import type { Provider } from '@supabase/supabase-js';
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useSupabase } from './use-supabase';
|
||||
|
||||
export const USER_IDENTITIES_QUERY_KEY = ['user-identities'];
|
||||
|
||||
export function useUserIdentities() {
|
||||
const supabase = useSupabase();
|
||||
|
||||
const {
|
||||
data: identities = [],
|
||||
isLoading,
|
||||
error,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: USER_IDENTITIES_QUERY_KEY,
|
||||
queryFn: async () => {
|
||||
const { data, error } = await supabase.auth.getUserIdentities();
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return data.identities;
|
||||
},
|
||||
});
|
||||
|
||||
const connectedProviders = useMemo(() => {
|
||||
return identities.map((identity) => identity.provider);
|
||||
}, [identities]);
|
||||
|
||||
const hasMultipleIdentities = identities.length > 1;
|
||||
|
||||
const getIdentityByProvider = (provider: Provider) => {
|
||||
return identities.find((identity) => identity.provider === provider);
|
||||
};
|
||||
|
||||
const isProviderConnected = (provider: Provider) => {
|
||||
return connectedProviders.includes(provider);
|
||||
};
|
||||
|
||||
return {
|
||||
identities,
|
||||
connectedProviders,
|
||||
hasMultipleIdentities,
|
||||
getIdentityByProvider,
|
||||
isProviderConnected,
|
||||
isLoading,
|
||||
error,
|
||||
refetch,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user