2025-01-07 02:29:29 +08:00
|
|
|
|
'use client';
|
|
|
|
|
|
2025-01-25 02:04:34 +08:00
|
|
|
|
import React, { useMemo, useState } from 'react';
|
2025-01-07 02:29:29 +08:00
|
|
|
|
import { Button, Divider, Input, Result } from 'antd';
|
|
|
|
|
import { User } from '@supabase/auth-js';
|
2025-02-22 03:07:30 +08:00
|
|
|
|
import { inputHasText, isValidEmail } from '@/lib';
|
2025-01-07 02:29:29 +08:00
|
|
|
|
import { useKeyPress, useMemoizedFn } from 'ahooks';
|
|
|
|
|
import Link from 'next/link';
|
|
|
|
|
import { BusterRoutes, createBusterRoute } from '@/routes/busterRoutes';
|
|
|
|
|
import { BsGithub, BsGoogle, BsMicrosoft } from 'react-icons/bs';
|
|
|
|
|
import { createStyles } from 'antd-style';
|
2025-03-02 14:19:50 +08:00
|
|
|
|
import { Title, Text } from '@/components/ui/typography';
|
2025-01-07 02:29:29 +08:00
|
|
|
|
import Cookies from 'js-cookie';
|
|
|
|
|
import { useBusterSupabaseAuthMethods } from '@/hooks/useBusterSupabaseAuthMethods';
|
|
|
|
|
import { PolicyCheck } from './PolicyCheck';
|
2025-01-23 06:58:06 +08:00
|
|
|
|
import { rustErrorHandler } from '@/api/buster_rest/errors';
|
2025-01-07 02:29:29 +08:00
|
|
|
|
|
|
|
|
|
const DEFAULT_CREDENTIALS = {
|
|
|
|
|
email: process.env.NEXT_PUBLIC_USER!,
|
|
|
|
|
password: process.env.NEXT_PUBLIC_USER_PASSWORD!
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const useStyles = createStyles(({ token, css }) => ({
|
|
|
|
|
link: {
|
|
|
|
|
color: token.colorPrimary
|
|
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
export const LoginForm: React.FC<{
|
|
|
|
|
user: null | User;
|
|
|
|
|
signInWithEmailAndPassword: ReturnType<
|
|
|
|
|
typeof useBusterSupabaseAuthMethods
|
|
|
|
|
>['signInWithEmailAndPassword'];
|
|
|
|
|
signInWithGoogle: ReturnType<typeof useBusterSupabaseAuthMethods>['signInWithGoogle'];
|
|
|
|
|
signUp: ReturnType<typeof useBusterSupabaseAuthMethods>['signUp'];
|
|
|
|
|
signInWithGithub: ReturnType<typeof useBusterSupabaseAuthMethods>['signInWithGithub'];
|
|
|
|
|
signInWithAzure: ReturnType<typeof useBusterSupabaseAuthMethods>['signInWithAzure'];
|
|
|
|
|
}> = ({
|
|
|
|
|
user,
|
|
|
|
|
signInWithEmailAndPassword,
|
|
|
|
|
signInWithGoogle,
|
|
|
|
|
signInWithGithub,
|
|
|
|
|
signInWithAzure,
|
|
|
|
|
signUp
|
|
|
|
|
}) => {
|
|
|
|
|
const hasSupabaseUser = !!user;
|
|
|
|
|
|
|
|
|
|
const [loading, setLoading] = useState<'google' | 'github' | 'azure' | 'email' | null>(null);
|
|
|
|
|
const [errorMessages, setErrorMessages] = useState<string[]>([]);
|
|
|
|
|
const [signUpFlow, setSignUpFlow] = useState(hasSupabaseUser);
|
|
|
|
|
const [signUpSuccess, setSignUpSuccess] = useState(false);
|
|
|
|
|
const hasUser = signUpFlow;
|
|
|
|
|
|
|
|
|
|
const errorFallback = (error: any) => {
|
|
|
|
|
const errorMessage = rustErrorHandler(error);
|
|
|
|
|
if (errorMessage?.message) {
|
|
|
|
|
setErrorMessages(['Invalid email or password']);
|
|
|
|
|
} else {
|
|
|
|
|
setErrorMessages(['An error occurred']);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const onSignInWithUsernameAndPassword = useMemoizedFn(
|
|
|
|
|
async ({ email, password }: { email: string; password: string }) => {
|
|
|
|
|
setLoading('email');
|
|
|
|
|
try {
|
|
|
|
|
const res = await signInWithEmailAndPassword({ email, password });
|
|
|
|
|
if (res?.error) throw res.error;
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
errorFallback(error);
|
|
|
|
|
}
|
|
|
|
|
setLoading(null);
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const onSignInWithGoogle = useMemoizedFn(async () => {
|
|
|
|
|
setLoading('google');
|
|
|
|
|
try {
|
|
|
|
|
const res = await signInWithGoogle();
|
|
|
|
|
if (res?.error) throw res.error;
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
errorFallback(error);
|
|
|
|
|
}
|
|
|
|
|
setLoading('google');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const onSignInWithGithub = useMemoizedFn(async () => {
|
|
|
|
|
setLoading('github');
|
|
|
|
|
try {
|
|
|
|
|
const res = await signInWithGithub();
|
|
|
|
|
if (res?.error) throw res.error;
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
errorFallback(error);
|
|
|
|
|
}
|
|
|
|
|
setLoading('github');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const onSignInWithAzure = useMemoizedFn(async () => {
|
|
|
|
|
setLoading('azure');
|
|
|
|
|
try {
|
|
|
|
|
const res = await signInWithAzure();
|
|
|
|
|
if (res?.error) throw res.error;
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
errorFallback(error);
|
|
|
|
|
}
|
|
|
|
|
setLoading('azure');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const onSignUp = useMemoizedFn(async (d: { email: string; password: string }) => {
|
|
|
|
|
setLoading('email');
|
|
|
|
|
try {
|
|
|
|
|
const res = await signUp(d);
|
|
|
|
|
if (res?.error) throw res.error;
|
|
|
|
|
|
|
|
|
|
setSignUpSuccess(true);
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
errorFallback(error);
|
|
|
|
|
}
|
|
|
|
|
setLoading(null);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const onSubmitClick = (d: { email: string; password: string }) => {
|
|
|
|
|
try {
|
|
|
|
|
setErrorMessages([]);
|
|
|
|
|
setLoading('email');
|
|
|
|
|
|
|
|
|
|
if (hasUser) onSignInWithUsernameAndPassword(d);
|
|
|
|
|
else onSignUp(d);
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
const errorMessage = rustErrorHandler(error);
|
|
|
|
|
if (errorMessage?.message == 'User already registered') {
|
|
|
|
|
onSignInWithUsernameAndPassword(d);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (errorMessage?.message) {
|
|
|
|
|
setErrorMessages([errorMessage.message]);
|
|
|
|
|
} else {
|
|
|
|
|
setErrorMessages(['An error occurred']);
|
|
|
|
|
}
|
|
|
|
|
setLoading(null);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex h-full flex-col items-center justify-center">
|
|
|
|
|
<div className="w-[330px]">
|
|
|
|
|
{signUpSuccess ? (
|
|
|
|
|
<SignUpSuccess setSignUpSuccess={setSignUpSuccess} setSignUpFlow={setSignUpFlow} />
|
|
|
|
|
) : (
|
|
|
|
|
<LoginOptions
|
|
|
|
|
hasUser={hasUser}
|
|
|
|
|
onSubmitClick={onSubmitClick}
|
|
|
|
|
setSignUpFlow={setSignUpFlow}
|
|
|
|
|
errorMessages={errorMessages}
|
|
|
|
|
loading={loading}
|
|
|
|
|
setErrorMessages={setErrorMessages}
|
|
|
|
|
signUpFlow={signUpFlow}
|
|
|
|
|
onSignInWithGoogle={onSignInWithGoogle}
|
|
|
|
|
onSignInWithGithub={onSignInWithGithub}
|
|
|
|
|
onSignInWithAzure={onSignInWithAzure}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const LoginOptions: React.FC<{
|
|
|
|
|
hasUser: boolean;
|
|
|
|
|
onSubmitClick: (d: { email: string; password: string }) => void;
|
|
|
|
|
onSignInWithGoogle: () => void;
|
|
|
|
|
onSignInWithGithub: () => void;
|
|
|
|
|
onSignInWithAzure: () => void;
|
|
|
|
|
setSignUpFlow: (value: boolean) => void;
|
|
|
|
|
errorMessages: string[];
|
|
|
|
|
loading: 'google' | 'github' | 'azure' | 'email' | null;
|
|
|
|
|
setErrorMessages: (value: string[]) => void;
|
|
|
|
|
signUpFlow: boolean;
|
|
|
|
|
}> = ({
|
|
|
|
|
hasUser,
|
|
|
|
|
onSubmitClick,
|
|
|
|
|
onSignInWithGoogle,
|
|
|
|
|
onSignInWithGithub,
|
|
|
|
|
onSignInWithAzure,
|
|
|
|
|
setSignUpFlow,
|
|
|
|
|
errorMessages,
|
|
|
|
|
loading,
|
|
|
|
|
setErrorMessages,
|
|
|
|
|
signUpFlow
|
|
|
|
|
}) => {
|
|
|
|
|
const { styles, cx } = useStyles();
|
|
|
|
|
|
|
|
|
|
const [email, setEmail] = useState('');
|
|
|
|
|
const [password, setPassword] = useState('');
|
|
|
|
|
const [password2, setPassword2] = useState('');
|
|
|
|
|
const [passwordCheck, setPasswordCheck] = useState(false);
|
|
|
|
|
const disableSubmitButton =
|
|
|
|
|
!inputHasText(password) || !inputHasText(password2) || password !== password2 || !passwordCheck;
|
|
|
|
|
|
|
|
|
|
const clearAllCookies = useMemoizedFn(() => {
|
|
|
|
|
Object.keys(Cookies.get()).forEach((cookieName) => {
|
|
|
|
|
Cookies.remove(cookieName);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2025-01-25 02:04:34 +08:00
|
|
|
|
const onSubmitClickPreflight = useMemoizedFn(async (d: { email: string; password: string }) => {
|
2025-01-07 02:29:29 +08:00
|
|
|
|
clearAllCookies();
|
|
|
|
|
onSubmitClick(d);
|
2025-01-25 02:04:34 +08:00
|
|
|
|
});
|
2025-01-07 02:29:29 +08:00
|
|
|
|
|
|
|
|
|
useKeyPress(['meta.shift.b', 'shift.ctrl.b'], async () => {
|
|
|
|
|
setSignUpFlow(false);
|
|
|
|
|
onSubmitClickPreflight({
|
|
|
|
|
email: DEFAULT_CREDENTIALS.email,
|
|
|
|
|
password: DEFAULT_CREDENTIALS.password
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<div className="flex flex-col items-center text-center">
|
|
|
|
|
<WelcomeText hasUser={hasUser} />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<form
|
|
|
|
|
className="my-6 space-y-3"
|
|
|
|
|
onSubmit={(v) => {
|
|
|
|
|
v.preventDefault();
|
|
|
|
|
onSubmitClickPreflight({
|
|
|
|
|
email,
|
|
|
|
|
password
|
|
|
|
|
});
|
|
|
|
|
}}>
|
|
|
|
|
<Button
|
|
|
|
|
type={'default'}
|
|
|
|
|
icon={<BsGoogle size={12} />}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
clearAllCookies();
|
|
|
|
|
onSignInWithGoogle();
|
|
|
|
|
}}
|
|
|
|
|
block={true}
|
|
|
|
|
loading={loading === 'google'}>
|
|
|
|
|
{hasUser ? `Continue with Google` : `Sign up with Google`}
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type={'default'}
|
|
|
|
|
icon={<BsGithub size={12} />}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
clearAllCookies();
|
|
|
|
|
onSignInWithGithub();
|
|
|
|
|
}}
|
|
|
|
|
block={true}
|
|
|
|
|
loading={loading === 'github'}>
|
|
|
|
|
{hasUser ? `Continue with Github` : `Sign up with Github`}
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
type={'default'}
|
|
|
|
|
icon={<BsMicrosoft size={12} />}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
clearAllCookies();
|
|
|
|
|
onSignInWithAzure();
|
|
|
|
|
}}
|
|
|
|
|
block={true}
|
|
|
|
|
loading={loading === 'azure'}>
|
|
|
|
|
{hasUser ? `Continue with Azure` : `Sign up with Azure`}
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
<Divider plain>or</Divider>
|
|
|
|
|
|
2025-02-22 06:05:21 +08:00
|
|
|
|
<div>
|
|
|
|
|
<Input
|
|
|
|
|
type="email"
|
|
|
|
|
placeholder="What is your email address?"
|
|
|
|
|
name="email"
|
|
|
|
|
id="email"
|
|
|
|
|
value={email}
|
|
|
|
|
onChange={(v) => {
|
|
|
|
|
setEmail(v.target.value);
|
|
|
|
|
}}
|
|
|
|
|
disabled={!!loading}
|
|
|
|
|
autoComplete="email"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2025-01-07 02:29:29 +08:00
|
|
|
|
<div className="relative">
|
|
|
|
|
<Input
|
|
|
|
|
value={password}
|
|
|
|
|
onChange={(v) => {
|
|
|
|
|
setPassword(v.target.value);
|
|
|
|
|
}}
|
|
|
|
|
disabled={!!loading}
|
|
|
|
|
id="password"
|
|
|
|
|
type="password"
|
|
|
|
|
name="password"
|
|
|
|
|
placeholder="Password"
|
|
|
|
|
autoComplete="new-password"
|
|
|
|
|
/>
|
|
|
|
|
<div className="absolute top-0 flex h-full items-center" style={{ right: 10 }}>
|
|
|
|
|
<PolicyCheck password={password} show={!hasUser} onCheckChange={setPasswordCheck} />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{!hasUser && (
|
|
|
|
|
<Input
|
|
|
|
|
value={password2}
|
|
|
|
|
onChange={(v) => {
|
|
|
|
|
setPassword2(v.target.value);
|
|
|
|
|
}}
|
|
|
|
|
disabled={!!loading}
|
|
|
|
|
id="password2"
|
|
|
|
|
type="password"
|
|
|
|
|
name="password2"
|
|
|
|
|
placeholder="Confirm password"
|
|
|
|
|
autoComplete="new-password"
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div className="flex flex-col space-y-0.5">
|
|
|
|
|
{errorMessages.map((message, index) => (
|
|
|
|
|
<LoginAlertMessage key={index} message={message} />
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<PolicyCheck
|
|
|
|
|
password={password}
|
|
|
|
|
show={!hasUser && disableSubmitButton && !!password}
|
|
|
|
|
placement="top">
|
|
|
|
|
<Button
|
|
|
|
|
block={true}
|
|
|
|
|
htmlType="submit"
|
|
|
|
|
loading={loading === 'email'}
|
|
|
|
|
type="primary"
|
|
|
|
|
disabled={hasUser ? false : disableSubmitButton}>
|
|
|
|
|
{hasUser ? `Sign in` : `Sign up`}
|
|
|
|
|
</Button>
|
|
|
|
|
</PolicyCheck>
|
|
|
|
|
</form>
|
|
|
|
|
|
|
|
|
|
<div className="pt-0">
|
2025-01-25 02:04:34 +08:00
|
|
|
|
<AlreadyHaveAccount
|
|
|
|
|
hasUser={hasUser}
|
|
|
|
|
setErrorMessages={setErrorMessages}
|
|
|
|
|
setPassword2={setPassword2}
|
|
|
|
|
setSignUpFlow={setSignUpFlow}
|
|
|
|
|
signUpFlow={signUpFlow}
|
|
|
|
|
/>
|
2025-01-07 02:29:29 +08:00
|
|
|
|
|
2025-01-25 02:04:34 +08:00
|
|
|
|
{hasUser && <ResetPasswordLink email={email} />}
|
2025-01-07 02:29:29 +08:00
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const SignUpSuccess: React.FC<{
|
|
|
|
|
setSignUpSuccess: (value: boolean) => void;
|
|
|
|
|
setSignUpFlow: (value: boolean) => void;
|
|
|
|
|
}> = ({ setSignUpSuccess, setSignUpFlow }) => {
|
|
|
|
|
return (
|
|
|
|
|
<Result
|
|
|
|
|
status="success"
|
|
|
|
|
title="Thanks for signing up"
|
|
|
|
|
subTitle="Please check your email to verify your account."
|
|
|
|
|
extra={[
|
|
|
|
|
<Button
|
|
|
|
|
key="login"
|
|
|
|
|
type="primary"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setSignUpSuccess(false);
|
|
|
|
|
setSignUpFlow(true);
|
|
|
|
|
}}>
|
|
|
|
|
Go to Login
|
|
|
|
|
</Button>
|
|
|
|
|
]}
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const WelcomeText: React.FC<{
|
|
|
|
|
hasUser: boolean;
|
|
|
|
|
}> = ({ hasUser }) => {
|
|
|
|
|
const text = hasUser ? `Sign in` : `Sign up for free`;
|
|
|
|
|
|
|
|
|
|
return (
|
2025-03-02 14:05:55 +08:00
|
|
|
|
<Title className="mb-0" as="h1">
|
2025-01-07 02:29:29 +08:00
|
|
|
|
{text}
|
|
|
|
|
</Title>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const LoginAlertMessage: React.FC<{
|
|
|
|
|
message: string;
|
|
|
|
|
}> = ({ message }) => {
|
|
|
|
|
return (
|
2025-03-02 14:19:50 +08:00
|
|
|
|
<Text size="2xs" variant="danger" className="">
|
2025-01-07 02:29:29 +08:00
|
|
|
|
{message}
|
|
|
|
|
</Text>
|
|
|
|
|
);
|
|
|
|
|
};
|
2025-01-25 02:04:34 +08:00
|
|
|
|
|
|
|
|
|
const AlreadyHaveAccount: React.FC<{
|
|
|
|
|
hasUser: boolean;
|
|
|
|
|
setErrorMessages: (value: string[]) => void;
|
|
|
|
|
setPassword2: (value: string) => void;
|
|
|
|
|
setSignUpFlow: (value: boolean) => void;
|
|
|
|
|
signUpFlow: boolean;
|
|
|
|
|
}> = React.memo(({ hasUser, setErrorMessages, setPassword2, setSignUpFlow, signUpFlow }) => {
|
|
|
|
|
const { styles, cx } = useStyles();
|
|
|
|
|
return (
|
|
|
|
|
<>
|
2025-03-02 14:19:50 +08:00
|
|
|
|
<Text
|
|
|
|
|
className="mb-1.5 flex w-full justify-center text-center"
|
|
|
|
|
variant="secondary"
|
|
|
|
|
size="2xs">
|
2025-01-25 02:04:34 +08:00
|
|
|
|
{!hasUser ? `Already have an account? ` : `Don’t already have an account? `}
|
|
|
|
|
<Text
|
2025-03-02 14:19:50 +08:00
|
|
|
|
variant="primary"
|
|
|
|
|
size="2xs"
|
2025-01-25 02:04:34 +08:00
|
|
|
|
className={cx('ml-1 cursor-pointer font-normal', styles.link)}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setErrorMessages([]);
|
|
|
|
|
setPassword2('');
|
|
|
|
|
setSignUpFlow(!signUpFlow);
|
|
|
|
|
}}>
|
|
|
|
|
{hasUser ? `Sign up` : `Sign in`}
|
|
|
|
|
</Text>
|
|
|
|
|
</Text>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
AlreadyHaveAccount.displayName = 'AlreadyHaveAccount';
|
|
|
|
|
|
|
|
|
|
const ResetPasswordLink: React.FC<{ email: string }> = ({ email }) => {
|
|
|
|
|
const { styles, cx } = useStyles();
|
|
|
|
|
|
|
|
|
|
const scrubbedEmail = useMemo(() => {
|
|
|
|
|
if (!email || !isValidEmail(email)) return '';
|
|
|
|
|
try {
|
|
|
|
|
return encodeURIComponent(email.trim());
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error encoding email:', error);
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
}, [email]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Link
|
|
|
|
|
className={cx(
|
|
|
|
|
'flex w-full cursor-pointer justify-center text-center font-normal',
|
|
|
|
|
styles.link
|
|
|
|
|
)}
|
|
|
|
|
href={
|
|
|
|
|
createBusterRoute({
|
|
|
|
|
route: BusterRoutes.AUTH_RESET_PASSWORD_EMAIL
|
|
|
|
|
}) + `?email=${scrubbedEmail}`
|
|
|
|
|
}>
|
2025-03-02 14:19:50 +08:00
|
|
|
|
<Text variant="primary" size="2xs">
|
2025-01-25 02:04:34 +08:00
|
|
|
|
Reset password
|
|
|
|
|
</Text>
|
|
|
|
|
</Link>
|
|
|
|
|
);
|
|
|
|
|
};
|