mirror of https://github.com/buster-so/buster.git
reset user flow
This commit is contained in:
parent
31ce0574cd
commit
6b2139fa5d
|
@ -83,9 +83,13 @@ enabled = true
|
|||
enabled = true
|
||||
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
|
||||
# in emails.
|
||||
site_url = "http://127.0.0.1:3000"
|
||||
site_url = "http://localhost:3000"
|
||||
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
|
||||
additional_redirect_urls = ["https://127.0.0.1:3000"]
|
||||
additional_redirect_urls = [
|
||||
"https://localhost:3000",
|
||||
"http://localhost:3000/auth/reset-password",
|
||||
"https://localhost:3000/auth/reset-password"
|
||||
]
|
||||
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
|
||||
jwt_expiry = 3600
|
||||
# If disabled, the refresh token will never expire.
|
||||
|
|
|
@ -122,7 +122,7 @@
|
|||
</p>
|
||||
|
||||
<div style="text-align: center;">
|
||||
<a href="{{ .RedirectTo }}" class="cta-button">Confirm Email Address</a>
|
||||
<a href="{{ .ConfirmationURL }}" class="cta-button">Confirm Email Address</a>
|
||||
</div>
|
||||
|
||||
<div class="security-note">
|
||||
|
@ -131,7 +131,7 @@
|
|||
|
||||
<p class="message">
|
||||
If the button doesn't work, you can copy and paste this link into your browser:<br>
|
||||
<a href="{{ .RedirectTo }}" style="color: #007bff; word-break: break-all;">{{ .RedirectTo }}</a>
|
||||
<a href="{{ .ConfirmationURL }}" style="color: #007bff; word-break: break-all;">{{ .ConfirmationURL }}</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -122,7 +122,7 @@
|
|||
</p>
|
||||
|
||||
<div style="text-align: center;">
|
||||
<a href="{{ .RedirectTo }}" class="cta-button">Accept Invitation</a>
|
||||
<a href="{{ .ConfirmationURL }}" class="cta-button">Accept Invitation</a>
|
||||
</div>
|
||||
|
||||
<div class="security-note">
|
||||
|
@ -131,7 +131,7 @@
|
|||
|
||||
<p class="message">
|
||||
If the button doesn't work, you can copy and paste this link into your browser:<br>
|
||||
<a href="{{ .RedirectTo }}" style="color: #007bff; word-break: break-all;">{{ .RedirectTo }}</a>
|
||||
<a href="{{ .ConfirmationURL }}" style="color: #007bff; word-break: break-all;">{{ .ConfirmationURL }}</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -122,7 +122,7 @@
|
|||
</p>
|
||||
|
||||
<div style="text-align: center;">
|
||||
<a href="{{ .RedirectTo }}" class="cta-button">Reset Password</a>
|
||||
<a href="{{ .ConfirmationURL }}" class="cta-button">Reset Password</a>
|
||||
</div>
|
||||
|
||||
<div class="security-note">
|
||||
|
@ -131,7 +131,7 @@
|
|||
|
||||
<p class="message">
|
||||
If the button doesn't work, you can copy and paste this link into your browser:<br>
|
||||
<a href="{{ .RedirectTo }}" style="color: #007bff; word-break: break-all;">{{ .RedirectTo }}</a>
|
||||
<a href="{{ .ConfirmationURL }}" style="color: #007bff; word-break: break-all;">{{ .ConfirmationURL }}</a>
|
||||
</p>
|
||||
|
||||
<p class="message">
|
||||
|
|
|
@ -77,7 +77,7 @@ export const PolicyCheck: React.FC<{
|
|||
},
|
||||
{
|
||||
text: 'Passwords match',
|
||||
check: password === password2 || password2 === undefined,
|
||||
check: (password && password === password2) || password2 === undefined,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import type { User } from '@supabase/supabase-js';
|
||||
import { useRouter } from '@tanstack/react-router';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { Link, useNavigate } from '@tanstack/react-router';
|
||||
import type React from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useGetMyUserInfo } from '@/api/buster_rest/users';
|
||||
import { Button } from '@/components/ui/buttons';
|
||||
import { SuccessCard } from '@/components/ui/card/SuccessCard';
|
||||
import { Input } from '@/components/ui/inputs';
|
||||
import { Title } from '@/components/ui/typography';
|
||||
import { useBusterNotifications } from '@/context/BusterNotifications';
|
||||
import { Text, Title } from '@/components/ui/typography';
|
||||
import { openErrorNotification } from '@/context/BusterNotifications';
|
||||
import { resetPassword } from '@/integrations/supabase/resetPassword';
|
||||
import { PolicyCheck } from './PolicyCheck';
|
||||
|
||||
|
@ -15,15 +16,18 @@ export const ResetPasswordForm: React.FC<{
|
|||
supabaseUser: Pick<User, 'email'>;
|
||||
}> = ({ supabaseUser }) => {
|
||||
const { data: busterUser } = useGetMyUserInfo();
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const [resetSuccess, setResetSuccess] = useState(false);
|
||||
const email = busterUser?.user?.email || supabaseUser?.email;
|
||||
const [password, setPassword] = useState('');
|
||||
const [password2, setPassword2] = useState('');
|
||||
const [goodPassword, setGoodPassword] = useState(false);
|
||||
const { openErrorMessage, openSuccessMessage } = useBusterNotifications();
|
||||
const [countdown, setCountdown] = useState(5);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
||||
const { mutateAsync: resetPasswordMutation, isPending: loading } = useMutation({
|
||||
mutationFn: resetPassword,
|
||||
});
|
||||
|
||||
const disabled = !goodPassword || loading || !password || !password2 || password !== password2;
|
||||
|
||||
|
@ -33,7 +37,7 @@ export const ResetPasswordForm: React.FC<{
|
|||
setCountdown((prev) => {
|
||||
if (prev === 0) {
|
||||
clearInterval(interval);
|
||||
router.navigate({ to: '/' });
|
||||
navigate({ to: '/app/home', replace: true, reloadDocument: true });
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
|
@ -43,21 +47,37 @@ export const ResetPasswordForm: React.FC<{
|
|||
}, []);
|
||||
|
||||
const handleResetPassword = useCallback(async () => {
|
||||
setLoading(true);
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password || !password2 || password !== password2 || !goodPassword) {
|
||||
openErrorNotification('Password and confirm password do not match');
|
||||
return;
|
||||
}
|
||||
|
||||
setResetSuccess(false);
|
||||
setErrorMessage('');
|
||||
|
||||
try {
|
||||
const res = await resetPassword({ data: { password } });
|
||||
setLoading(false);
|
||||
if (res?.error) {
|
||||
throw res;
|
||||
}
|
||||
await resetPasswordMutation({ data: { password } });
|
||||
setResetSuccess(true);
|
||||
openSuccessMessage('Password reset successfully');
|
||||
startCountdown();
|
||||
} catch (error) {
|
||||
openErrorMessage(error as string);
|
||||
openErrorNotification(error as string);
|
||||
setErrorMessage(error as string);
|
||||
}
|
||||
}, [resetPassword, password, openErrorMessage, openSuccessMessage]);
|
||||
}, [
|
||||
resetPassword,
|
||||
password,
|
||||
password2,
|
||||
goodPassword,
|
||||
loading,
|
||||
setResetSuccess,
|
||||
startCountdown,
|
||||
resetPasswordMutation,
|
||||
openErrorNotification,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-4">
|
||||
|
@ -100,6 +120,12 @@ export const ResetPasswordForm: React.FC<{
|
|||
autoComplete="new-password"
|
||||
/>
|
||||
|
||||
{errorMessage && (
|
||||
<Text size="xs" variant="danger">
|
||||
{errorMessage}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<PolicyCheck
|
||||
password={password}
|
||||
password2={password2}
|
||||
|
@ -127,6 +153,13 @@ export const ResetPasswordForm: React.FC<{
|
|||
<SuccessCard
|
||||
title="Password reset successfully"
|
||||
message={`Navigating to app in ${countdown} seconds`}
|
||||
extra={
|
||||
<Link to="/app/home" reloadDocument replace>
|
||||
<Button block variant="black">
|
||||
Go to app now
|
||||
</Button>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -32,5 +32,12 @@ export const getSupabaseUser = async () => {
|
|||
const { data: userData } = isServer
|
||||
? await getSupabaseUserServerFn()
|
||||
: await supabase.auth.getUser();
|
||||
return userData.user;
|
||||
|
||||
const simplifiedUser = {
|
||||
id: userData.user?.id ?? '',
|
||||
email: userData.user?.email ?? '',
|
||||
is_anonymous: userData.user?.is_anonymous ?? true,
|
||||
};
|
||||
|
||||
return simplifiedUser;
|
||||
};
|
||||
|
|
|
@ -1,21 +1,18 @@
|
|||
import { createServerFn } from '@tanstack/react-start';
|
||||
import { z } from 'zod';
|
||||
import { env } from '@/env';
|
||||
import { ServerRoute as AuthCallbackRoute } from '../../routes/auth.callback';
|
||||
import { Route as AuthResetPasswordRoute } from '../../routes/auth.reset-password';
|
||||
import { getSupabaseUser } from './getSupabaseUserClient';
|
||||
import { getSupabaseServerClient } from './server';
|
||||
|
||||
export const resetPasswordEmailSend = createServerFn({ method: 'POST' })
|
||||
.validator(z.object({ email: z.string().email() }))
|
||||
.handler(async ({ data: { email } }) => {
|
||||
const supabase = await getSupabaseServerClient();
|
||||
|
||||
const url = env.VITE_PUBLIC_URL;
|
||||
|
||||
const authURLFull = `${url}${AuthResetPasswordRoute.to}`;
|
||||
|
||||
console.log('email', email);
|
||||
console.log('authURLFull', authURLFull);
|
||||
|
||||
const { error } = await supabase.auth.resetPasswordForEmail(email, {
|
||||
redirectTo: authURLFull,
|
||||
});
|
||||
|
@ -32,16 +29,21 @@ export const resetPassword = createServerFn({ method: 'POST' })
|
|||
.handler(async ({ data: { password } }) => {
|
||||
const supabase = await getSupabaseServerClient();
|
||||
|
||||
const { data: user } = await supabase.auth.getUser();
|
||||
const supabaseUser = await getSupabaseUser();
|
||||
|
||||
if (!user?.user) {
|
||||
throw new Error('User not found');
|
||||
if (supabaseUser.is_anonymous) {
|
||||
console.error('User is anonymous', supabaseUser);
|
||||
throw new Error('User is anonymous');
|
||||
}
|
||||
|
||||
if (!supabaseUser.email) {
|
||||
console.error('User email not found', supabaseUser);
|
||||
}
|
||||
|
||||
const { error } = await supabase.auth.updateUser({ password });
|
||||
|
||||
if (error) {
|
||||
return { error: error.message };
|
||||
throw new Error(error.message);
|
||||
}
|
||||
|
||||
return;
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
import type { User } from '@supabase/supabase-js';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { useState } from 'react';
|
||||
import { z } from 'zod';
|
||||
import { ResetEmailForm } from '@/components/features/auth/ResetEmailForm';
|
||||
import { ResetPasswordForm } from '@/components/features/auth/ResetPasswordForm';
|
||||
import { useGetSupabaseUser } from '@/context/Supabase';
|
||||
import { CircleSpinnerLoaderContainer } from '@/components/ui/loaders';
|
||||
import { useMount } from '@/hooks/useMount';
|
||||
import { getBrowserClient } from '@/integrations/supabase/client';
|
||||
import { getSupabaseUser } from '@/integrations/supabase/getSupabaseUserClient';
|
||||
|
||||
export const Route = createFileRoute('/auth/reset-password')({
|
||||
ssr: true,
|
||||
head: () => ({
|
||||
meta: [
|
||||
{ title: 'Reset Password' },
|
||||
|
@ -13,17 +19,59 @@ export const Route = createFileRoute('/auth/reset-password')({
|
|||
{ name: 'og:description', content: 'Reset your Buster account password' },
|
||||
],
|
||||
}),
|
||||
beforeLoad: async () => {
|
||||
const supabaseUser = await getSupabaseUser();
|
||||
return { supabaseUser };
|
||||
},
|
||||
loader: async ({ context }) => {
|
||||
const { supabaseUser } = context;
|
||||
return { supabaseUser };
|
||||
},
|
||||
|
||||
component: RouteComponent,
|
||||
validateSearch: z.object({
|
||||
email: z.string().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
const supabase = getBrowserClient();
|
||||
|
||||
function RouteComponent() {
|
||||
const { email } = Route.useSearch();
|
||||
const supabaseUser = useGetSupabaseUser();
|
||||
const supabaseUserLoader = Route.useLoaderData();
|
||||
const [mounting, setMounting] = useState(true);
|
||||
|
||||
if (email || supabaseUser?.is_anonymous || !supabaseUser) {
|
||||
const [supabaseUser, setSupabaseUser] = useState<null | Pick<User, 'email' | 'is_anonymous'>>(
|
||||
supabaseUserLoader?.supabaseUser ?? null
|
||||
);
|
||||
|
||||
useMount(() => {
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
setMounting(false);
|
||||
if (session?.user) {
|
||||
setSupabaseUser({
|
||||
email: session.user.email ?? '',
|
||||
is_anonymous: session.user.is_anonymous ?? true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
setMounting(false);
|
||||
}, 500);
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
});
|
||||
|
||||
if (mounting) {
|
||||
return <CircleSpinnerLoaderContainer />;
|
||||
}
|
||||
|
||||
if (email || !supabaseUser || supabaseUser.is_anonymous) {
|
||||
return <ResetEmailForm queryEmail={email || ''} />;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue