mirror of https://github.com/buster-so/buster.git
Better login handling
This commit is contained in:
parent
47a5405a08
commit
b77260cc67
|
@ -89,3 +89,4 @@ drizzle/meta/
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
/packages/aTest/.mastra
|
/packages/aTest/.mastra
|
||||||
**/*.private.*
|
**/*.private.*
|
||||||
|
apps/web-tss/.env.prod
|
||||||
|
|
|
@ -8,6 +8,7 @@ import Github from '@/components/ui/icons/customIcons/Github';
|
||||||
import Google from '@/components/ui/icons/customIcons/Google';
|
import Google from '@/components/ui/icons/customIcons/Google';
|
||||||
import Microsoft from '@/components/ui/icons/customIcons/Microsoft';
|
import Microsoft from '@/components/ui/icons/customIcons/Microsoft';
|
||||||
import { Input } from '@/components/ui/inputs';
|
import { Input } from '@/components/ui/inputs';
|
||||||
|
import { AppTooltip } from '@/components/ui/tooltip';
|
||||||
import { Text, Title } from '@/components/ui/typography';
|
import { Text, Title } from '@/components/ui/typography';
|
||||||
import { env } from '@/env';
|
import { env } from '@/env';
|
||||||
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
|
import { useMemoizedFn } from '@/hooks/useMemoizedFn';
|
||||||
|
@ -22,12 +23,20 @@ import { cn } from '@/lib/classMerge';
|
||||||
import { isValidEmail } from '@/lib/email';
|
import { isValidEmail } from '@/lib/email';
|
||||||
import { inputHasText } from '@/lib/text';
|
import { inputHasText } from '@/lib/text';
|
||||||
import { PolicyCheck } from './PolicyCheck';
|
import { PolicyCheck } from './PolicyCheck';
|
||||||
|
import { type LastUsedReturnType, useLastUsed } from './useLastUsed';
|
||||||
|
|
||||||
export const LoginForm: React.FC<{ redirectTo: string | null | undefined }> = ({ redirectTo }) => {
|
export const LoginForm: React.FC<{
|
||||||
|
redirectTo: string | null | undefined;
|
||||||
|
isAnonymousUser?: boolean;
|
||||||
|
}> = ({ redirectTo }) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const lastUsedProps = useLastUsed();
|
||||||
|
|
||||||
const [loading, setLoading] = useState<'google' | 'github' | 'azure' | 'email' | null>(null);
|
const [loading, setLoading] = useState<'google' | 'github' | 'azure' | 'email' | null>(null);
|
||||||
const [errorMessages, setErrorMessages] = useState<string[]>([]);
|
const [errorMessages, setErrorMessages] = useState<string[]>([]);
|
||||||
const [signUpFlow, setSignUpFlow] = useState(true);
|
const [signUpFlow, setSignUpFlow] = useState(
|
||||||
|
lastUsedProps.isAnonymousUser && !env.VITE_PUBLIC_USER
|
||||||
|
);
|
||||||
const [signUpSuccess, setSignUpSuccess] = useState(false);
|
const [signUpSuccess, setSignUpSuccess] = useState(false);
|
||||||
|
|
||||||
const onSignInWithUsernameAndPassword = useMemoizedFn(
|
const onSignInWithUsernameAndPassword = useMemoizedFn(
|
||||||
|
@ -162,6 +171,7 @@ export const LoginForm: React.FC<{ redirectTo: string | null | undefined }> = ({
|
||||||
onSignInWithGoogle={onSignInWithGoogle}
|
onSignInWithGoogle={onSignInWithGoogle}
|
||||||
onSignInWithGithub={onSignInWithGithub}
|
onSignInWithGithub={onSignInWithGithub}
|
||||||
onSignInWithAzure={onSignInWithAzure}
|
onSignInWithAzure={onSignInWithAzure}
|
||||||
|
lastUsedProps={lastUsedProps}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -179,6 +189,7 @@ const LoginOptions: React.FC<{
|
||||||
loading: 'google' | 'github' | 'azure' | 'email' | null;
|
loading: 'google' | 'github' | 'azure' | 'email' | null;
|
||||||
setErrorMessages: (value: string[]) => void;
|
setErrorMessages: (value: string[]) => void;
|
||||||
signUpFlow: boolean;
|
signUpFlow: boolean;
|
||||||
|
lastUsedProps: LastUsedReturnType;
|
||||||
}> = ({
|
}> = ({
|
||||||
onSubmitClick,
|
onSubmitClick,
|
||||||
onSignInWithGoogle,
|
onSignInWithGoogle,
|
||||||
|
@ -189,11 +200,15 @@ const LoginOptions: React.FC<{
|
||||||
loading,
|
loading,
|
||||||
setErrorMessages,
|
setErrorMessages,
|
||||||
signUpFlow,
|
signUpFlow,
|
||||||
|
lastUsedProps,
|
||||||
}) => {
|
}) => {
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState(
|
||||||
const [password, setPassword] = useState('');
|
lastUsedProps.supabaseUser?.email || env.VITE_PUBLIC_USER || ''
|
||||||
|
);
|
||||||
|
const [password, setPassword] = useState(env.VITE_PUBLIC_USER_PASSWORD || '');
|
||||||
const [password2, setPassword2] = useState('');
|
const [password2, setPassword2] = useState('');
|
||||||
const [passwordCheck, setPasswordCheck] = useState(false);
|
const [passwordCheck, setPasswordCheck] = useState(false);
|
||||||
|
const navigate = useNavigate();
|
||||||
const disableSubmitButton =
|
const disableSubmitButton =
|
||||||
!inputHasText(password) || !inputHasText(password2) || password !== password2 || !passwordCheck;
|
!inputHasText(password) || !inputHasText(password2) || password !== password2 || !passwordCheck;
|
||||||
|
|
||||||
|
@ -209,13 +224,13 @@ const LoginOptions: React.FC<{
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'meta+shift+b',
|
'meta+shift+b',
|
||||||
() => {
|
(e) => {
|
||||||
|
e.preventDefault();
|
||||||
setSignUpFlow(false);
|
setSignUpFlow(false);
|
||||||
const DEFAULT_CREDENTIALS = {
|
const DEFAULT_CREDENTIALS = {
|
||||||
email: env.VITE_PUBLIC_USER || '',
|
email: env.VITE_PUBLIC_USER || '',
|
||||||
password: env.VITE_PUBLIC_USER_PASSWORD || '',
|
password: env.VITE_PUBLIC_USER_PASSWORD || '',
|
||||||
};
|
};
|
||||||
console.log('DEFAULT_CREDENTIALS', DEFAULT_CREDENTIALS);
|
|
||||||
|
|
||||||
onSubmitClick({
|
onSubmitClick({
|
||||||
email: DEFAULT_CREDENTIALS.email,
|
email: DEFAULT_CREDENTIALS.email,
|
||||||
|
@ -228,7 +243,7 @@ const LoginOptions: React.FC<{
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col items-center text-center">
|
<div className="flex flex-col items-center text-center">
|
||||||
<WelcomeText signUpFlow={signUpFlow} />
|
<WelcomeText signUpFlow={signUpFlow} lastUsedProps={lastUsedProps} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 mb-4 flex flex-col space-y-3">
|
<div className="mt-6 mb-4 flex flex-col space-y-3">
|
||||||
|
@ -358,6 +373,14 @@ const LoginOptions: React.FC<{
|
||||||
>
|
>
|
||||||
{!signUpFlow ? 'Sign in' : 'Sign up'}
|
{!signUpFlow ? 'Sign in' : 'Sign up'}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{lastUsedProps.isCurrentlySignedIn && !signUpFlow && (
|
||||||
|
<Link to="/app/home">
|
||||||
|
<Button size={'tall'} block={true} type="button" className="truncate" variant="black">
|
||||||
|
Continue with {lastUsedProps.supabaseUser?.email}
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="mt-2 flex flex-col gap-y-2">
|
<div className="mt-2 flex flex-col gap-y-2">
|
||||||
|
@ -366,6 +389,7 @@ const LoginOptions: React.FC<{
|
||||||
setPassword2={setPassword2}
|
setPassword2={setPassword2}
|
||||||
setSignUpFlow={setSignUpFlow}
|
setSignUpFlow={setSignUpFlow}
|
||||||
signUpFlow={signUpFlow}
|
signUpFlow={signUpFlow}
|
||||||
|
lastUsedProps={lastUsedProps}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{!signUpFlow && <ResetPasswordLink email={email} tabIndex={-7} />}
|
{!signUpFlow && <ResetPasswordLink email={email} tabIndex={-7} />}
|
||||||
|
@ -401,13 +425,21 @@ const SignUpSuccess: React.FC<{
|
||||||
|
|
||||||
const WelcomeText: React.FC<{
|
const WelcomeText: React.FC<{
|
||||||
signUpFlow: boolean;
|
signUpFlow: boolean;
|
||||||
}> = ({ signUpFlow }) => {
|
lastUsedProps: LastUsedReturnType;
|
||||||
|
}> = ({ signUpFlow, lastUsedProps }) => {
|
||||||
const text = !signUpFlow ? 'Sign in' : 'Sign up for free';
|
const text = !signUpFlow ? 'Sign in' : 'Sign up for free';
|
||||||
|
const { isCurrentlySignedIn, supabaseUser } = lastUsedProps;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Title className="mb-0" as="h1">
|
<div className="flex items-center justify-center gap-2">
|
||||||
{text}
|
<AppTooltip
|
||||||
</Title>
|
title={isCurrentlySignedIn ? `Currently signed in as ${supabaseUser?.email}` : undefined}
|
||||||
|
>
|
||||||
|
<Title className="mb-0" as="h1">
|
||||||
|
{text}
|
||||||
|
</Title>
|
||||||
|
</AppTooltip>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -426,25 +458,30 @@ const AlreadyHaveAccount: React.FC<{
|
||||||
setPassword2: (value: string) => void;
|
setPassword2: (value: string) => void;
|
||||||
setSignUpFlow: (value: boolean) => void;
|
setSignUpFlow: (value: boolean) => void;
|
||||||
signUpFlow: boolean;
|
signUpFlow: boolean;
|
||||||
}> = React.memo(({ setErrorMessages, setPassword2, setSignUpFlow, signUpFlow }) => {
|
lastUsedProps: LastUsedReturnType;
|
||||||
return (
|
}> = React.memo(({ setErrorMessages, setPassword2, setSignUpFlow, signUpFlow, lastUsedProps }) => {
|
||||||
<div className="flex items-center justify-center gap-0.5">
|
const { isCurrentlySignedIn, supabaseUser } = lastUsedProps;
|
||||||
<Text className="" variant="secondary" size="xs">
|
|
||||||
{signUpFlow ? 'Already have an account? ' : "Don't already have an account?"}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<Text
|
return (
|
||||||
variant="primary"
|
<div className="flex flex-col items-center justify-center gap-1.5">
|
||||||
size="xs"
|
<div className="flex items-center justify-center gap-0.5">
|
||||||
className={cn('ml-1 cursor-pointer font-normal')}
|
<Text className="" variant="secondary" size="xs">
|
||||||
onClick={() => {
|
{signUpFlow ? 'Already have an account? ' : "Don't already have an account?"}
|
||||||
setErrorMessages([]);
|
</Text>
|
||||||
setPassword2('');
|
|
||||||
setSignUpFlow(!signUpFlow);
|
<Text
|
||||||
}}
|
variant="primary"
|
||||||
>
|
size="xs"
|
||||||
{!signUpFlow ? 'Sign up' : 'Sign in'}
|
className={cn('ml-1 cursor-pointer font-normal')}
|
||||||
</Text>
|
onClick={() => {
|
||||||
|
setErrorMessages([]);
|
||||||
|
setPassword2('');
|
||||||
|
setSignUpFlow(!signUpFlow);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!signUpFlow ? 'Sign up' : 'Sign in'}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useGetSupabaseUser } from '@/context/Supabase';
|
||||||
|
|
||||||
|
export type SignInType = 'google' | 'github' | 'azure' | 'email' | null;
|
||||||
|
|
||||||
|
export const useLastUsed = () => {
|
||||||
|
const supabaseUser = useGetSupabaseUser();
|
||||||
|
|
||||||
|
const provider = supabaseUser?.app_metadata?.provider;
|
||||||
|
const isAnonymousUser = !!supabaseUser?.is_anonymous;
|
||||||
|
const isCurrentlySignedIn = !isAnonymousUser && !!supabaseUser?.id;
|
||||||
|
|
||||||
|
const lastUsed: SignInType = useMemo(() => {
|
||||||
|
if (provider === 'google') {
|
||||||
|
return 'google';
|
||||||
|
} else if (provider === 'github') {
|
||||||
|
return 'github';
|
||||||
|
} else if (provider === 'azure') {
|
||||||
|
return 'azure';
|
||||||
|
} else if (provider === 'email' && !isAnonymousUser) {
|
||||||
|
return 'email';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [provider]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
lastUsed,
|
||||||
|
isAnonymousUser,
|
||||||
|
isCurrentlySignedIn,
|
||||||
|
supabaseUser,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LastUsedReturnType = ReturnType<typeof useLastUsed>;
|
|
@ -8,6 +8,9 @@ export type SupabaseContextType = {
|
||||||
user: {
|
user: {
|
||||||
id: string;
|
id: string;
|
||||||
is_anonymous: boolean;
|
is_anonymous: boolean;
|
||||||
|
email: string;
|
||||||
|
identities: User['identities'];
|
||||||
|
app_metadata: User['app_metadata'];
|
||||||
};
|
};
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
};
|
};
|
||||||
|
@ -20,7 +23,10 @@ const useSupabaseContextInternal = ({
|
||||||
accessToken: accessTokenProp,
|
accessToken: accessTokenProp,
|
||||||
}: SupabaseContextType) => {
|
}: SupabaseContextType) => {
|
||||||
const refreshTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
const refreshTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||||
const [supabaseUser, setSupabaseUser] = useState<Pick<User, 'id' | 'is_anonymous'> | null>(user);
|
const [supabaseUser, setSupabaseUser] = useState<Pick<
|
||||||
|
User,
|
||||||
|
'id' | 'is_anonymous' | 'email' | 'identities' | 'app_metadata'
|
||||||
|
> | null>(user);
|
||||||
const [accessToken, setAccessToken] = useState(accessTokenProp);
|
const [accessToken, setAccessToken] = useState(accessTokenProp);
|
||||||
|
|
||||||
const isAnonymousUser: boolean = !user?.id || user?.is_anonymous === true;
|
const isAnonymousUser: boolean = !user?.id || user?.is_anonymous === true;
|
||||||
|
|
|
@ -10,3 +10,8 @@ const stableIsAnonymousUserSelector = (state: SupabaseContextReturnType) => stat
|
||||||
export const useIsAnonymousSupabaseUser = () => {
|
export const useIsAnonymousSupabaseUser = () => {
|
||||||
return useContextSelector(SupabaseContext, stableIsAnonymousUserSelector);
|
return useContextSelector(SupabaseContext, stableIsAnonymousUserSelector);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const stableSupabaseUserSelector = (state: SupabaseContextReturnType) => state.supabaseUser;
|
||||||
|
export const useGetSupabaseUser = () => {
|
||||||
|
return useContextSelector(SupabaseContext, stableSupabaseUserSelector);
|
||||||
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { createFileRoute } from '@tanstack/react-router';
|
import { ClientOnly, createFileRoute } from '@tanstack/react-router';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { LoginForm } from '@/components/features/auth/LoginForm';
|
import { LoginForm } from '@/components/features/auth/LoginForm';
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ export const Route = createFileRoute('/auth/login')({
|
||||||
|
|
||||||
function LoginComp() {
|
function LoginComp() {
|
||||||
const { next } = Route.useSearch();
|
const { next } = Route.useSearch();
|
||||||
|
const cxt = Route.useRouteContext();
|
||||||
|
|
||||||
return <LoginForm redirectTo={next} />;
|
return <LoginForm redirectTo={next} />;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue