init commit,
This commit is contained in:
28
03_source/frontend/src/auth/components/form-divider.tsx
Normal file
28
03_source/frontend/src/auth/components/form-divider.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { Theme, SxProps } from '@mui/material/styles';
|
||||
|
||||
import Divider from '@mui/material/Divider';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type FormDividerProps = {
|
||||
sx?: SxProps<Theme>;
|
||||
label?: React.ReactNode;
|
||||
};
|
||||
|
||||
export function FormDivider({ sx, label = 'OR' }: FormDividerProps) {
|
||||
return (
|
||||
<Divider
|
||||
sx={[
|
||||
() => ({
|
||||
my: 3,
|
||||
typography: 'overline',
|
||||
color: 'text.disabled',
|
||||
'&::before, :after': { borderTopStyle: 'dashed' },
|
||||
}),
|
||||
...(Array.isArray(sx) ? sx : [sx]),
|
||||
]}
|
||||
>
|
||||
{label}
|
||||
</Divider>
|
||||
);
|
||||
}
|
47
03_source/frontend/src/auth/components/form-head.tsx
Normal file
47
03_source/frontend/src/auth/components/form-head.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { BoxProps } from '@mui/material/Box';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type FormHeadProps = BoxProps & {
|
||||
icon?: React.ReactNode;
|
||||
title: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
};
|
||||
|
||||
export function FormHead({ sx, icon, title, description, ...other }: FormHeadProps) {
|
||||
return (
|
||||
<>
|
||||
{icon && (
|
||||
<Box component="span" sx={{ mb: 3, mx: 'auto', display: 'inline-flex' }}>
|
||||
{icon}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box
|
||||
sx={[
|
||||
() => ({
|
||||
mb: 5,
|
||||
gap: 1.5,
|
||||
display: 'flex',
|
||||
textAlign: 'center',
|
||||
whiteSpace: 'pre-line',
|
||||
flexDirection: 'column',
|
||||
}),
|
||||
...(Array.isArray(sx) ? sx : [sx]),
|
||||
]}
|
||||
{...other}
|
||||
>
|
||||
<Typography variant="h5">{title}</Typography>
|
||||
|
||||
{description && (
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
||||
{description}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
46
03_source/frontend/src/auth/components/form-resend-code.tsx
Normal file
46
03_source/frontend/src/auth/components/form-resend-code.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { BoxProps } from '@mui/material/Box';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Link from '@mui/material/Link';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type FormResendCodeProps = BoxProps & {
|
||||
value?: number;
|
||||
disabled?: boolean;
|
||||
onResendCode?: () => void;
|
||||
};
|
||||
|
||||
export function FormResendCode({
|
||||
value,
|
||||
disabled,
|
||||
onResendCode,
|
||||
sx,
|
||||
...other
|
||||
}: FormResendCodeProps) {
|
||||
return (
|
||||
<Box
|
||||
sx={[
|
||||
() => ({
|
||||
mt: 3,
|
||||
typography: 'body2',
|
||||
alignSelf: 'center',
|
||||
}),
|
||||
...(Array.isArray(sx) ? sx : [sx]),
|
||||
]}
|
||||
{...other}
|
||||
>
|
||||
{`Don’t have a code? `}
|
||||
<Link
|
||||
variant="subtitle2"
|
||||
onClick={onResendCode}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
...(disabled && { color: 'text.disabled', pointerEvents: 'none' }),
|
||||
}}
|
||||
>
|
||||
Resend {disabled && value && value > 0 && `(${value}s)`}
|
||||
</Link>
|
||||
</Box>
|
||||
);
|
||||
}
|
41
03_source/frontend/src/auth/components/form-return-link.tsx
Normal file
41
03_source/frontend/src/auth/components/form-return-link.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { LinkProps } from '@mui/material/Link';
|
||||
|
||||
import Link from '@mui/material/Link';
|
||||
|
||||
import { RouterLink } from 'src/routes/components';
|
||||
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type FormReturnLinkProps = LinkProps & {
|
||||
href: string;
|
||||
icon?: React.ReactNode;
|
||||
label?: React.ReactNode;
|
||||
};
|
||||
|
||||
export function FormReturnLink({ sx, href, label, icon, children, ...other }: FormReturnLinkProps) {
|
||||
return (
|
||||
<Link
|
||||
component={RouterLink}
|
||||
href={href}
|
||||
color="inherit"
|
||||
variant="subtitle2"
|
||||
sx={[
|
||||
{
|
||||
mt: 3,
|
||||
gap: 0.5,
|
||||
mx: 'auto',
|
||||
alignItems: 'center',
|
||||
display: 'inline-flex',
|
||||
},
|
||||
...(Array.isArray(sx) ? sx : [sx]),
|
||||
]}
|
||||
{...other}
|
||||
>
|
||||
{icon || <Iconify width={16} icon="eva:arrow-ios-back-fill" />}
|
||||
{label || 'Return to sign in'}
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
46
03_source/frontend/src/auth/components/form-socials.tsx
Normal file
46
03_source/frontend/src/auth/components/form-socials.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { BoxProps } from '@mui/material/Box';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type FormSocialsProps = BoxProps & {
|
||||
signInWithGoogle?: () => void;
|
||||
singInWithGithub?: () => void;
|
||||
signInWithTwitter?: () => void;
|
||||
};
|
||||
|
||||
export function FormSocials({
|
||||
sx,
|
||||
signInWithGoogle,
|
||||
singInWithGithub,
|
||||
signInWithTwitter,
|
||||
...other
|
||||
}: FormSocialsProps) {
|
||||
return (
|
||||
<Box
|
||||
sx={[
|
||||
{
|
||||
gap: 1.5,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
...(Array.isArray(sx) ? sx : [sx]),
|
||||
]}
|
||||
{...other}
|
||||
>
|
||||
<IconButton color="inherit" onClick={signInWithGoogle}>
|
||||
<Iconify width={22} icon="socials:google" />
|
||||
</IconButton>
|
||||
<IconButton color="inherit" onClick={singInWithGithub}>
|
||||
<Iconify width={22} icon="socials:github" />
|
||||
</IconButton>
|
||||
<IconButton color="inherit" onClick={signInWithTwitter}>
|
||||
<Iconify width={22} icon="socials:twitter" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
);
|
||||
}
|
35
03_source/frontend/src/auth/components/sign-up-terms.tsx
Normal file
35
03_source/frontend/src/auth/components/sign-up-terms.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { BoxProps } from '@mui/material/Box';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Link from '@mui/material/Link';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function SignUpTerms({ sx, ...other }: BoxProps) {
|
||||
return (
|
||||
<Box
|
||||
component="span"
|
||||
sx={[
|
||||
() => ({
|
||||
mt: 3,
|
||||
display: 'block',
|
||||
textAlign: 'center',
|
||||
typography: 'caption',
|
||||
color: 'text.secondary',
|
||||
}),
|
||||
...(Array.isArray(sx) ? sx : [sx]),
|
||||
]}
|
||||
{...other}
|
||||
>
|
||||
{'By signing up, I agree to '}
|
||||
<Link underline="always" color="text.primary">
|
||||
Terms of service
|
||||
</Link>
|
||||
{' and '}
|
||||
<Link underline="always" color="text.primary">
|
||||
Privacy policy
|
||||
</Link>
|
||||
.
|
||||
</Box>
|
||||
);
|
||||
}
|
97
03_source/frontend/src/auth/context/amplify/action.tsx
Normal file
97
03_source/frontend/src/auth/context/amplify/action.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import type {
|
||||
SignUpInput,
|
||||
SignInInput,
|
||||
ConfirmSignUpInput,
|
||||
ResetPasswordInput,
|
||||
ResendSignUpCodeInput,
|
||||
ConfirmResetPasswordInput,
|
||||
} from 'aws-amplify/auth';
|
||||
|
||||
import {
|
||||
signIn as _signIn,
|
||||
signUp as _signUp,
|
||||
signOut as _signOut,
|
||||
confirmSignUp as _confirmSignUp,
|
||||
resetPassword as _resetPassword,
|
||||
resendSignUpCode as _resendSignUpCode,
|
||||
confirmResetPassword as _confirmResetPassword,
|
||||
} from 'aws-amplify/auth';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export type SignInParams = SignInInput;
|
||||
|
||||
export type SignUpParams = SignUpInput & { firstName: string; lastName: string };
|
||||
|
||||
export type ResendSignUpCodeParams = ResendSignUpCodeInput;
|
||||
|
||||
export type ConfirmSignUpParams = ConfirmSignUpInput;
|
||||
|
||||
export type ResetPasswordParams = ResetPasswordInput;
|
||||
|
||||
export type ConfirmResetPasswordParams = ConfirmResetPasswordInput;
|
||||
|
||||
/** **************************************
|
||||
* Sign in
|
||||
*************************************** */
|
||||
export const signInWithPassword = async ({ username, password }: SignInParams): Promise<void> => {
|
||||
await _signIn({ username, password });
|
||||
};
|
||||
|
||||
/** **************************************
|
||||
* Sign up
|
||||
*************************************** */
|
||||
export const signUp = async ({
|
||||
username,
|
||||
password,
|
||||
firstName,
|
||||
lastName,
|
||||
}: SignUpParams): Promise<void> => {
|
||||
await _signUp({
|
||||
username,
|
||||
password,
|
||||
options: { userAttributes: { email: username, given_name: firstName, family_name: lastName } },
|
||||
});
|
||||
};
|
||||
|
||||
/** **************************************
|
||||
* Confirm sign up
|
||||
*************************************** */
|
||||
export const confirmSignUp = async ({
|
||||
username,
|
||||
confirmationCode,
|
||||
}: ConfirmSignUpParams): Promise<void> => {
|
||||
await _confirmSignUp({ username, confirmationCode });
|
||||
};
|
||||
|
||||
/** **************************************
|
||||
* Resend code sign up
|
||||
*************************************** */
|
||||
export const resendSignUpCode = async ({ username }: ResendSignUpCodeParams): Promise<void> => {
|
||||
await _resendSignUpCode({ username });
|
||||
};
|
||||
|
||||
/** **************************************
|
||||
* Sign out
|
||||
*************************************** */
|
||||
export const signOut = async (): Promise<void> => {
|
||||
await _signOut();
|
||||
};
|
||||
|
||||
/** **************************************
|
||||
* Reset password
|
||||
*************************************** */
|
||||
export const resetPassword = async ({ username }: ResetPasswordParams): Promise<void> => {
|
||||
await _resetPassword({ username });
|
||||
};
|
||||
|
||||
/** **************************************
|
||||
* Update password
|
||||
*************************************** */
|
||||
export const updatePassword = async ({
|
||||
username,
|
||||
confirmationCode,
|
||||
newPassword,
|
||||
}: ConfirmResetPasswordParams): Promise<void> => {
|
||||
await _confirmResetPassword({ username, confirmationCode, newPassword });
|
||||
};
|
@@ -0,0 +1,99 @@
|
||||
import { Amplify } from 'aws-amplify';
|
||||
import { useSetState } from 'minimal-shared/hooks';
|
||||
import { useMemo, useEffect, useCallback } from 'react';
|
||||
import { fetchAuthSession, fetchUserAttributes } from 'aws-amplify/auth';
|
||||
|
||||
import axios from 'src/lib/axios';
|
||||
import { CONFIG } from 'src/global-config';
|
||||
|
||||
import { AuthContext } from '../auth-context';
|
||||
|
||||
import type { AuthState } from '../../types';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* NOTE:
|
||||
* We only build demo at basic level.
|
||||
* Customer will need to do some extra handling yourself if you want to extend the logic and other features...
|
||||
*/
|
||||
|
||||
/**
|
||||
* Docs:
|
||||
* https://docs.amplify.aws/react/build-a-backend/auth/manage-user-session/
|
||||
*/
|
||||
|
||||
Amplify.configure({
|
||||
Auth: {
|
||||
Cognito: {
|
||||
userPoolId: CONFIG.amplify.userPoolId,
|
||||
userPoolClientId: CONFIG.amplify.userPoolWebClientId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export function AuthProvider({ children }: Props) {
|
||||
const { state, setState } = useSetState<AuthState>({ user: null, loading: true });
|
||||
|
||||
const checkUserSession = useCallback(async () => {
|
||||
try {
|
||||
const authSession = (await fetchAuthSession({ forceRefresh: true })).tokens;
|
||||
|
||||
if (authSession) {
|
||||
const userAttributes = await fetchUserAttributes();
|
||||
|
||||
const accessToken = authSession.accessToken.toString();
|
||||
|
||||
setState({ user: { ...authSession, ...userAttributes }, loading: false });
|
||||
axios.defaults.headers.common.Authorization = `Bearer ${accessToken}`;
|
||||
} else {
|
||||
setState({ user: null, loading: false });
|
||||
delete axios.defaults.headers.common.Authorization;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setState({ user: null, loading: false });
|
||||
}
|
||||
}, [setState]);
|
||||
|
||||
useEffect(() => {
|
||||
checkUserSession();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const checkAuthenticated = state.user ? 'authenticated' : 'unauthenticated';
|
||||
|
||||
const status = state.loading ? 'loading' : checkAuthenticated;
|
||||
|
||||
const memoizedValue = useMemo(
|
||||
() => ({
|
||||
user: state.user
|
||||
? {
|
||||
...state.user,
|
||||
id: state.user?.sub,
|
||||
accessToken: state.user?.accessToken?.toString(),
|
||||
displayName:
|
||||
state.user?.given_name &&
|
||||
state.user?.family_name &&
|
||||
`${state.user?.given_name} ${state.user?.family_name}`,
|
||||
role: state.user?.role ?? 'admin',
|
||||
}
|
||||
: null,
|
||||
checkUserSession,
|
||||
loading: status === 'loading',
|
||||
authenticated: status === 'authenticated',
|
||||
unauthenticated: status === 'unauthenticated',
|
||||
}),
|
||||
[checkUserSession, state.user, status]
|
||||
);
|
||||
|
||||
return <AuthContext value={memoizedValue}>{children}</AuthContext>;
|
||||
}
|
3
03_source/frontend/src/auth/context/amplify/index.ts
Normal file
3
03_source/frontend/src/auth/context/amplify/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './action';
|
||||
|
||||
export * from './auth-provider';
|
7
03_source/frontend/src/auth/context/auth-context.tsx
Normal file
7
03_source/frontend/src/auth/context/auth-context.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
import type { AuthContextValue } from '../types';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export const AuthContext = createContext<AuthContextValue | undefined>(undefined);
|
94
03_source/frontend/src/auth/context/auth0/auth-provider.tsx
Normal file
94
03_source/frontend/src/auth/context/auth0/auth-provider.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import type { AppState } from '@auth0/auth0-react';
|
||||
|
||||
import { useAuth0, Auth0Provider } from '@auth0/auth0-react';
|
||||
import { useMemo, useState, useEffect, useCallback } from 'react';
|
||||
|
||||
import axios from 'src/lib/axios';
|
||||
import { CONFIG } from 'src/global-config';
|
||||
|
||||
import { AuthContext } from '../auth-context';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export function AuthProvider({ children }: Props) {
|
||||
const { domain, clientId, callbackUrl } = CONFIG.auth0;
|
||||
|
||||
const onRedirectCallback = useCallback((appState?: AppState) => {
|
||||
window.location.replace(appState?.returnTo || window.location.pathname);
|
||||
}, []);
|
||||
|
||||
if (!(domain && clientId && callbackUrl)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Auth0Provider
|
||||
domain={domain}
|
||||
clientId={clientId}
|
||||
authorizationParams={{ redirect_uri: callbackUrl }}
|
||||
onRedirectCallback={onRedirectCallback}
|
||||
cacheLocation="localstorage"
|
||||
>
|
||||
<AuthProviderContainer>{children}</AuthProviderContainer>
|
||||
</Auth0Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
function AuthProviderContainer({ children }: Props) {
|
||||
const { user, isLoading, isAuthenticated, getAccessTokenSilently } = useAuth0();
|
||||
|
||||
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||
|
||||
const getAccessToken = useCallback(async () => {
|
||||
try {
|
||||
if (isAuthenticated) {
|
||||
const token = await getAccessTokenSilently();
|
||||
|
||||
setAccessToken(token);
|
||||
axios.defaults.headers.common.Authorization = `Bearer ${token}`;
|
||||
} else {
|
||||
setAccessToken(null);
|
||||
delete axios.defaults.headers.common.Authorization;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, [getAccessTokenSilently, isAuthenticated]);
|
||||
|
||||
useEffect(() => {
|
||||
getAccessToken();
|
||||
}, [getAccessToken]);
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const checkAuthenticated = isAuthenticated ? 'authenticated' : 'unauthenticated';
|
||||
|
||||
const status = isLoading ? 'loading' : checkAuthenticated;
|
||||
|
||||
const memoizedValue = useMemo(
|
||||
() => ({
|
||||
user: user
|
||||
? {
|
||||
...user,
|
||||
id: user?.sub,
|
||||
accessToken,
|
||||
displayName: user?.name,
|
||||
photoURL: user?.picture,
|
||||
role: user?.role ?? 'admin',
|
||||
}
|
||||
: null,
|
||||
loading: status === 'loading',
|
||||
authenticated: status === 'authenticated',
|
||||
unauthenticated: status === 'unauthenticated',
|
||||
}),
|
||||
[accessToken, status, user]
|
||||
);
|
||||
|
||||
return <AuthContext value={memoizedValue}>{children}</AuthContext>;
|
||||
}
|
1
03_source/frontend/src/auth/context/auth0/index.ts
Normal file
1
03_source/frontend/src/auth/context/auth0/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './auth-provider';
|
110
03_source/frontend/src/auth/context/firebase/action.ts
Normal file
110
03_source/frontend/src/auth/context/firebase/action.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { doc, setDoc, collection } from 'firebase/firestore';
|
||||
import {
|
||||
signOut as _signOut,
|
||||
signInWithPopup as _signInWithPopup,
|
||||
GoogleAuthProvider as _GoogleAuthProvider,
|
||||
GithubAuthProvider as _GithubAuthProvider,
|
||||
TwitterAuthProvider as _TwitterAuthProvider,
|
||||
sendEmailVerification as _sendEmailVerification,
|
||||
sendPasswordResetEmail as _sendPasswordResetEmail,
|
||||
signInWithEmailAndPassword as _signInWithEmailAndPassword,
|
||||
createUserWithEmailAndPassword as _createUserWithEmailAndPassword,
|
||||
} from 'firebase/auth';
|
||||
|
||||
import { AUTH, FIRESTORE } from 'src/lib/firebase';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export type SignInParams = {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type SignUpParams = {
|
||||
email: string;
|
||||
password: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
};
|
||||
|
||||
export type ForgotPasswordParams = {
|
||||
email: string;
|
||||
};
|
||||
|
||||
/** **************************************
|
||||
* Sign in
|
||||
*************************************** */
|
||||
export const signInWithPassword = async ({ email, password }: SignInParams): Promise<void> => {
|
||||
try {
|
||||
await _signInWithEmailAndPassword(AUTH, email, password);
|
||||
|
||||
const user = AUTH.currentUser;
|
||||
|
||||
if (!user?.emailVerified) {
|
||||
throw new Error('Email not verified!');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during sign in with password:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const signInWithGoogle = async (): Promise<void> => {
|
||||
const provider = new _GoogleAuthProvider();
|
||||
await _signInWithPopup(AUTH, provider);
|
||||
};
|
||||
|
||||
export const signInWithGithub = async (): Promise<void> => {
|
||||
const provider = new _GithubAuthProvider();
|
||||
await _signInWithPopup(AUTH, provider);
|
||||
};
|
||||
|
||||
export const signInWithTwitter = async (): Promise<void> => {
|
||||
const provider = new _TwitterAuthProvider();
|
||||
await _signInWithPopup(AUTH, provider);
|
||||
};
|
||||
|
||||
/** **************************************
|
||||
* Sign up
|
||||
*************************************** */
|
||||
export const signUp = async ({
|
||||
email,
|
||||
password,
|
||||
firstName,
|
||||
lastName,
|
||||
}: SignUpParams): Promise<void> => {
|
||||
try {
|
||||
const newUser = await _createUserWithEmailAndPassword(AUTH, email, password);
|
||||
|
||||
/*
|
||||
* (1) If skip emailVerified
|
||||
* Remove : await _sendEmailVerification(newUser.user);
|
||||
*/
|
||||
await _sendEmailVerification(newUser.user);
|
||||
|
||||
const userProfile = doc(collection(FIRESTORE, 'users'), newUser.user?.uid);
|
||||
|
||||
await setDoc(userProfile, {
|
||||
uid: newUser.user?.uid,
|
||||
email,
|
||||
displayName: `${firstName} ${lastName}`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error during sign up:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/** **************************************
|
||||
* Sign out
|
||||
*************************************** */
|
||||
export const signOut = async (): Promise<void> => {
|
||||
await _signOut(AUTH);
|
||||
};
|
||||
|
||||
/** **************************************
|
||||
* Reset password
|
||||
*************************************** */
|
||||
export const sendPasswordResetEmail = async ({ email }: ForgotPasswordParams): Promise<void> => {
|
||||
await _sendPasswordResetEmail(AUTH, email);
|
||||
};
|
@@ -0,0 +1,89 @@
|
||||
import { doc, getDoc } from 'firebase/firestore';
|
||||
import { onAuthStateChanged } from 'firebase/auth';
|
||||
import { useSetState } from 'minimal-shared/hooks';
|
||||
import { useMemo, useEffect, useCallback } from 'react';
|
||||
|
||||
import axios from 'src/lib/axios';
|
||||
import { AUTH, FIRESTORE } from 'src/lib/firebase';
|
||||
|
||||
import { AuthContext } from '../auth-context';
|
||||
|
||||
import type { AuthState } from '../../types';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* NOTE:
|
||||
* We only build demo at basic level.
|
||||
* Customer will need to do some extra handling yourself if you want to extend the logic and other features...
|
||||
*/
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export function AuthProvider({ children }: Props) {
|
||||
const { state, setState } = useSetState<AuthState>({ user: null, loading: true });
|
||||
|
||||
const checkUserSession = useCallback(async () => {
|
||||
try {
|
||||
onAuthStateChanged(AUTH, async (user: AuthState['user']) => {
|
||||
if (user && user.emailVerified) {
|
||||
/*
|
||||
* (1) If skip emailVerified
|
||||
* Remove the condition (if/else) : user.emailVerified
|
||||
*/
|
||||
const userProfile = doc(FIRESTORE, 'users', user.uid);
|
||||
|
||||
const docSnap = await getDoc(userProfile);
|
||||
|
||||
const profileData = docSnap.data();
|
||||
|
||||
const { accessToken } = user;
|
||||
|
||||
setState({ user: { ...user, ...profileData }, loading: false });
|
||||
axios.defaults.headers.common.Authorization = `Bearer ${accessToken}`;
|
||||
} else {
|
||||
setState({ user: null, loading: false });
|
||||
delete axios.defaults.headers.common.Authorization;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setState({ user: null, loading: false });
|
||||
}
|
||||
}, [setState]);
|
||||
|
||||
useEffect(() => {
|
||||
checkUserSession();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const checkAuthenticated = state.user ? 'authenticated' : 'unauthenticated';
|
||||
|
||||
const status = state.loading ? 'loading' : checkAuthenticated;
|
||||
|
||||
const memoizedValue = useMemo(
|
||||
() => ({
|
||||
user: state.user
|
||||
? {
|
||||
...state.user,
|
||||
id: state.user?.uid,
|
||||
accessToken: state.user?.accessToken,
|
||||
displayName: state.user?.displayName,
|
||||
photoURL: state.user?.photoURL,
|
||||
role: state.user?.role ?? 'admin',
|
||||
}
|
||||
: null,
|
||||
checkUserSession,
|
||||
loading: status === 'loading',
|
||||
authenticated: status === 'authenticated',
|
||||
unauthenticated: status === 'unauthenticated',
|
||||
}),
|
||||
[checkUserSession, state.user, status]
|
||||
);
|
||||
|
||||
return <AuthContext value={memoizedValue}>{children}</AuthContext>;
|
||||
}
|
3
03_source/frontend/src/auth/context/firebase/index.ts
Normal file
3
03_source/frontend/src/auth/context/firebase/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './action';
|
||||
|
||||
export * from './auth-provider';
|
84
03_source/frontend/src/auth/context/jwt/action.ts
Normal file
84
03_source/frontend/src/auth/context/jwt/action.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import axios, { endpoints } from 'src/lib/axios';
|
||||
|
||||
import { setSession } from './utils';
|
||||
import { JWT_STORAGE_KEY } from './constant';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export type SignInParams = {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type SignUpParams = {
|
||||
email: string;
|
||||
password: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
};
|
||||
|
||||
/** **************************************
|
||||
* Sign in
|
||||
*************************************** */
|
||||
export const signInWithPassword = async ({ email, password }: SignInParams): Promise<void> => {
|
||||
try {
|
||||
const params = { email, password };
|
||||
|
||||
const res = await axios.post(endpoints.auth.signIn, params);
|
||||
|
||||
const { accessToken } = res.data;
|
||||
|
||||
if (!accessToken) {
|
||||
throw new Error('Access token not found in response');
|
||||
}
|
||||
|
||||
setSession(accessToken);
|
||||
} catch (error) {
|
||||
console.error('Error during sign in:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/** **************************************
|
||||
* Sign up
|
||||
*************************************** */
|
||||
export const signUp = async ({
|
||||
email,
|
||||
password,
|
||||
firstName,
|
||||
lastName,
|
||||
}: SignUpParams): Promise<void> => {
|
||||
const params = {
|
||||
email,
|
||||
password,
|
||||
firstName,
|
||||
lastName,
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await axios.post(endpoints.auth.signUp, params);
|
||||
|
||||
const { accessToken } = res.data;
|
||||
|
||||
if (!accessToken) {
|
||||
throw new Error('Access token not found in response');
|
||||
}
|
||||
|
||||
sessionStorage.setItem(JWT_STORAGE_KEY, accessToken);
|
||||
} catch (error) {
|
||||
console.error('Error during sign up:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/** **************************************
|
||||
* Sign out
|
||||
*************************************** */
|
||||
export const signOut = async (): Promise<void> => {
|
||||
try {
|
||||
await setSession(null);
|
||||
} catch (error) {
|
||||
console.error('Error during sign out:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
71
03_source/frontend/src/auth/context/jwt/auth-provider.tsx
Normal file
71
03_source/frontend/src/auth/context/jwt/auth-provider.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { useSetState } from 'minimal-shared/hooks';
|
||||
import { useMemo, useEffect, useCallback } from 'react';
|
||||
|
||||
import axios, { endpoints } from 'src/lib/axios';
|
||||
|
||||
import { JWT_STORAGE_KEY } from './constant';
|
||||
import { AuthContext } from '../auth-context';
|
||||
import { setSession, isValidToken } from './utils';
|
||||
|
||||
import type { AuthState } from '../../types';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* NOTE:
|
||||
* We only build demo at basic level.
|
||||
* Customer will need to do some extra handling yourself if you want to extend the logic and other features...
|
||||
*/
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export function AuthProvider({ children }: Props) {
|
||||
const { state, setState } = useSetState<AuthState>({ user: null, loading: true });
|
||||
|
||||
const checkUserSession = useCallback(async () => {
|
||||
try {
|
||||
const accessToken = sessionStorage.getItem(JWT_STORAGE_KEY);
|
||||
|
||||
if (accessToken && isValidToken(accessToken)) {
|
||||
setSession(accessToken);
|
||||
|
||||
const res = await axios.get(endpoints.auth.me);
|
||||
|
||||
const { user } = res.data;
|
||||
|
||||
setState({ user: { ...user, accessToken }, loading: false });
|
||||
} else {
|
||||
setState({ user: null, loading: false });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setState({ user: null, loading: false });
|
||||
}
|
||||
}, [setState]);
|
||||
|
||||
useEffect(() => {
|
||||
checkUserSession();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const checkAuthenticated = state.user ? 'authenticated' : 'unauthenticated';
|
||||
|
||||
const status = state.loading ? 'loading' : checkAuthenticated;
|
||||
|
||||
const memoizedValue = useMemo(
|
||||
() => ({
|
||||
user: state.user ? { ...state.user, role: state.user?.role ?? 'admin' } : null,
|
||||
checkUserSession,
|
||||
loading: status === 'loading',
|
||||
authenticated: status === 'authenticated',
|
||||
unauthenticated: status === 'unauthenticated',
|
||||
}),
|
||||
[checkUserSession, state.user, status]
|
||||
);
|
||||
|
||||
return <AuthContext value={memoizedValue}>{children}</AuthContext>;
|
||||
}
|
1
03_source/frontend/src/auth/context/jwt/constant.ts
Normal file
1
03_source/frontend/src/auth/context/jwt/constant.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const JWT_STORAGE_KEY = 'jwt_access_token';
|
7
03_source/frontend/src/auth/context/jwt/index.ts
Normal file
7
03_source/frontend/src/auth/context/jwt/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from './utils';
|
||||
|
||||
export * from './action';
|
||||
|
||||
export * from './constant';
|
||||
|
||||
export * from './auth-provider';
|
94
03_source/frontend/src/auth/context/jwt/utils.ts
Normal file
94
03_source/frontend/src/auth/context/jwt/utils.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { paths } from 'src/routes/paths';
|
||||
|
||||
import axios from 'src/lib/axios';
|
||||
|
||||
import { JWT_STORAGE_KEY } from './constant';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function jwtDecode(token: string) {
|
||||
try {
|
||||
if (!token) return null;
|
||||
|
||||
const parts = token.split('.');
|
||||
if (parts.length < 2) {
|
||||
throw new Error('Invalid token!');
|
||||
}
|
||||
|
||||
const base64Url = parts[1];
|
||||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||
const decoded = JSON.parse(atob(base64));
|
||||
|
||||
return decoded;
|
||||
} catch (error) {
|
||||
console.error('Error decoding token:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function isValidToken(accessToken: string) {
|
||||
if (!accessToken) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwtDecode(accessToken);
|
||||
|
||||
if (!decoded || !('exp' in decoded)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentTime = Date.now() / 1000;
|
||||
|
||||
return decoded.exp > currentTime;
|
||||
} catch (error) {
|
||||
console.error('Error during token validation:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function tokenExpired(exp: number) {
|
||||
const currentTime = Date.now();
|
||||
const timeLeft = exp * 1000 - currentTime;
|
||||
|
||||
setTimeout(() => {
|
||||
try {
|
||||
alert('Token expired!');
|
||||
sessionStorage.removeItem(JWT_STORAGE_KEY);
|
||||
window.location.href = paths.auth.jwt.signIn;
|
||||
} catch (error) {
|
||||
console.error('Error during token expiration:', error);
|
||||
throw error;
|
||||
}
|
||||
}, timeLeft);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export async function setSession(accessToken: string | null) {
|
||||
try {
|
||||
if (accessToken) {
|
||||
sessionStorage.setItem(JWT_STORAGE_KEY, accessToken);
|
||||
|
||||
axios.defaults.headers.common.Authorization = `Bearer ${accessToken}`;
|
||||
|
||||
const decodedToken = jwtDecode(accessToken); // ~3 days by minimals server
|
||||
|
||||
if (decodedToken && 'exp' in decodedToken) {
|
||||
tokenExpired(decodedToken.exp);
|
||||
} else {
|
||||
throw new Error('Invalid access token!');
|
||||
}
|
||||
} else {
|
||||
sessionStorage.removeItem(JWT_STORAGE_KEY);
|
||||
delete axios.defaults.headers.common.Authorization;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during set session:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
138
03_source/frontend/src/auth/context/supabase/action.tsx
Normal file
138
03_source/frontend/src/auth/context/supabase/action.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import type {
|
||||
AuthError,
|
||||
AuthResponse,
|
||||
UserResponse,
|
||||
AuthTokenResponsePassword,
|
||||
SignInWithPasswordCredentials,
|
||||
SignUpWithPasswordCredentials,
|
||||
} from '@supabase/supabase-js';
|
||||
|
||||
import { paths } from 'src/routes/paths';
|
||||
|
||||
import { supabase } from 'src/lib/supabase';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export type SignInParams = {
|
||||
email: string;
|
||||
password: string;
|
||||
options?: SignInWithPasswordCredentials['options'];
|
||||
};
|
||||
|
||||
export type SignUpParams = {
|
||||
email: string;
|
||||
password: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
options?: SignUpWithPasswordCredentials['options'];
|
||||
};
|
||||
|
||||
export type ResetPasswordParams = {
|
||||
email: string;
|
||||
options?: {
|
||||
redirectTo?: string;
|
||||
captchaToken?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type UpdatePasswordParams = {
|
||||
password: string;
|
||||
options?: {
|
||||
emailRedirectTo?: string | undefined;
|
||||
};
|
||||
};
|
||||
|
||||
/** **************************************
|
||||
* Sign in
|
||||
*************************************** */
|
||||
export const signInWithPassword = async ({
|
||||
email,
|
||||
password,
|
||||
}: SignInParams): Promise<AuthTokenResponsePassword> => {
|
||||
const { data, error } = await supabase.auth.signInWithPassword({ email, password });
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return { data, error };
|
||||
};
|
||||
|
||||
/** **************************************
|
||||
* Sign up
|
||||
*************************************** */
|
||||
export const signUp = async ({
|
||||
email,
|
||||
password,
|
||||
firstName,
|
||||
lastName,
|
||||
}: SignUpParams): Promise<AuthResponse> => {
|
||||
const { data, error } = await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
options: {
|
||||
emailRedirectTo: `${window.location.origin}${paths.dashboard.root}`,
|
||||
data: { display_name: `${firstName} ${lastName}` },
|
||||
},
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!data?.user?.identities?.length) {
|
||||
throw new Error('This user already exists');
|
||||
}
|
||||
|
||||
return { data, error };
|
||||
};
|
||||
|
||||
/** **************************************
|
||||
* Sign out
|
||||
*************************************** */
|
||||
export const signOut = async (): Promise<{
|
||||
error: AuthError | null;
|
||||
}> => {
|
||||
const { error } = await supabase.auth.signOut();
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return { error };
|
||||
};
|
||||
|
||||
/** **************************************
|
||||
* Reset password
|
||||
*************************************** */
|
||||
export const resetPassword = async ({
|
||||
email,
|
||||
}: ResetPasswordParams): Promise<{ data: {}; error: null } | { data: null; error: AuthError }> => {
|
||||
const { data, error } = await supabase.auth.resetPasswordForEmail(email, {
|
||||
redirectTo: `${window.location.origin}${paths.auth.supabase.updatePassword}`,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return { data, error };
|
||||
};
|
||||
|
||||
/** **************************************
|
||||
* Update password
|
||||
*************************************** */
|
||||
export const updatePassword = async ({ password }: UpdatePasswordParams): Promise<UserResponse> => {
|
||||
const { data, error } = await supabase.auth.updateUser({ password });
|
||||
|
||||
if (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return { data, error };
|
||||
};
|
@@ -0,0 +1,85 @@
|
||||
import { useSetState } from 'minimal-shared/hooks';
|
||||
import { useMemo, useEffect, useCallback } from 'react';
|
||||
|
||||
import axios from 'src/lib/axios';
|
||||
import { supabase } from 'src/lib/supabase';
|
||||
|
||||
import { AuthContext } from '../auth-context';
|
||||
|
||||
import type { AuthState } from '../../types';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* NOTE:
|
||||
* We only build demo at basic level.
|
||||
* Customer will need to do some extra handling yourself if you want to extend the logic and other features...
|
||||
*/
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export function AuthProvider({ children }: Props) {
|
||||
const { state, setState } = useSetState<AuthState>({ user: null, loading: true });
|
||||
|
||||
const checkUserSession = useCallback(async () => {
|
||||
try {
|
||||
const {
|
||||
data: { session },
|
||||
error,
|
||||
} = await supabase.auth.getSession();
|
||||
|
||||
if (error) {
|
||||
setState({ user: null, loading: false });
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (session) {
|
||||
const accessToken = session?.access_token;
|
||||
|
||||
setState({ user: { ...session, ...session?.user }, loading: false });
|
||||
axios.defaults.headers.common.Authorization = `Bearer ${accessToken}`;
|
||||
} else {
|
||||
setState({ user: null, loading: false });
|
||||
delete axios.defaults.headers.common.Authorization;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setState({ user: null, loading: false });
|
||||
}
|
||||
}, [setState]);
|
||||
|
||||
useEffect(() => {
|
||||
checkUserSession();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
const checkAuthenticated = state.user ? 'authenticated' : 'unauthenticated';
|
||||
|
||||
const status = state.loading ? 'loading' : checkAuthenticated;
|
||||
|
||||
const memoizedValue = useMemo(
|
||||
() => ({
|
||||
user: state.user
|
||||
? {
|
||||
...state.user,
|
||||
id: state.user?.id,
|
||||
accessToken: state.user?.access_token,
|
||||
displayName: state.user?.user_metadata.display_name,
|
||||
role: state.user?.role ?? 'admin',
|
||||
}
|
||||
: null,
|
||||
checkUserSession,
|
||||
loading: status === 'loading',
|
||||
authenticated: status === 'authenticated',
|
||||
unauthenticated: status === 'unauthenticated',
|
||||
}),
|
||||
[checkUserSession, state.user, status]
|
||||
);
|
||||
|
||||
return <AuthContext value={memoizedValue}>{children}</AuthContext>;
|
||||
}
|
3
03_source/frontend/src/auth/context/supabase/index.ts
Normal file
3
03_source/frontend/src/auth/context/supabase/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './action';
|
||||
|
||||
export * from './auth-provider';
|
68
03_source/frontend/src/auth/guard/auth-guard.tsx
Normal file
68
03_source/frontend/src/auth/guard/auth-guard.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
import { paths } from 'src/routes/paths';
|
||||
import { useRouter, usePathname } from 'src/routes/hooks';
|
||||
|
||||
import { CONFIG } from 'src/global-config';
|
||||
|
||||
import { SplashScreen } from 'src/components/loading-screen';
|
||||
|
||||
import { useAuthContext } from '../hooks';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type AuthGuardProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
const signInPaths = {
|
||||
jwt: paths.auth.jwt.signIn,
|
||||
auth0: paths.auth.auth0.signIn,
|
||||
amplify: paths.auth.amplify.signIn,
|
||||
firebase: paths.auth.firebase.signIn,
|
||||
supabase: paths.auth.supabase.signIn,
|
||||
};
|
||||
|
||||
export function AuthGuard({ children }: AuthGuardProps) {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const { authenticated, loading } = useAuthContext();
|
||||
|
||||
const [isChecking, setIsChecking] = useState(true);
|
||||
|
||||
const createRedirectPath = (currentPath: string) => {
|
||||
const queryString = new URLSearchParams({ returnTo: pathname }).toString();
|
||||
return `${currentPath}?${queryString}`;
|
||||
};
|
||||
|
||||
const checkPermissions = async (): Promise<void> => {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!authenticated) {
|
||||
const { method } = CONFIG.auth;
|
||||
|
||||
const signInPath = signInPaths[method];
|
||||
const redirectPath = createRedirectPath(signInPath);
|
||||
|
||||
router.replace(redirectPath);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setIsChecking(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
checkPermissions();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [authenticated, loading]);
|
||||
|
||||
if (isChecking) {
|
||||
return <SplashScreen />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
51
03_source/frontend/src/auth/guard/guest-guard.tsx
Normal file
51
03_source/frontend/src/auth/guard/guest-guard.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
import { useSearchParams } from 'src/routes/hooks';
|
||||
|
||||
import { CONFIG } from 'src/global-config';
|
||||
|
||||
import { SplashScreen } from 'src/components/loading-screen';
|
||||
|
||||
import { useAuthContext } from '../hooks';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
type GuestGuardProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export function GuestGuard({ children }: GuestGuardProps) {
|
||||
const { loading, authenticated } = useAuthContext();
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
const returnTo = searchParams.get('returnTo') || CONFIG.auth.redirectPath;
|
||||
|
||||
const [isChecking, setIsChecking] = useState(true);
|
||||
|
||||
const checkPermissions = async (): Promise<void> => {
|
||||
if (loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (authenticated) {
|
||||
// Redirect authenticated users to the returnTo path
|
||||
// Using `window.location.href` instead of `router.replace` to avoid unnecessary re-rendering
|
||||
// that might be caused by the AuthGuard component
|
||||
window.location.href = returnTo;
|
||||
return;
|
||||
}
|
||||
|
||||
setIsChecking(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
checkPermissions();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [authenticated, loading]);
|
||||
|
||||
if (isChecking) {
|
||||
return <SplashScreen />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
5
03_source/frontend/src/auth/guard/index.ts
Normal file
5
03_source/frontend/src/auth/guard/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './auth-guard';
|
||||
|
||||
export * from './guest-guard';
|
||||
|
||||
export * from './role-based-guard';
|
61
03_source/frontend/src/auth/guard/role-based-guard.tsx
Normal file
61
03_source/frontend/src/auth/guard/role-based-guard.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { Theme, SxProps } from '@mui/material/styles';
|
||||
|
||||
import { m } from 'framer-motion';
|
||||
|
||||
import Container from '@mui/material/Container';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
import { ForbiddenIllustration } from 'src/assets/illustrations';
|
||||
|
||||
import { varBounce, MotionContainer } from 'src/components/animate';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* NOTE:
|
||||
* This component is for reference only.
|
||||
* You can customize the logic and conditions to better suit your application's requirements.
|
||||
*/
|
||||
|
||||
export type RoleBasedGuardProp = {
|
||||
sx?: SxProps<Theme>;
|
||||
currentRole: string;
|
||||
hasContent?: boolean;
|
||||
allowedRoles: string | string[];
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export function RoleBasedGuard({
|
||||
sx,
|
||||
children,
|
||||
hasContent,
|
||||
currentRole,
|
||||
allowedRoles,
|
||||
}: RoleBasedGuardProp) {
|
||||
if (currentRole && allowedRoles && !allowedRoles.includes(currentRole)) {
|
||||
return hasContent ? (
|
||||
<Container
|
||||
component={MotionContainer}
|
||||
sx={[{ textAlign: 'center' }, ...(Array.isArray(sx) ? sx : [sx])]}
|
||||
>
|
||||
<m.div variants={varBounce('in')}>
|
||||
<Typography variant="h3" sx={{ mb: 2 }}>
|
||||
Permission denied
|
||||
</Typography>
|
||||
</m.div>
|
||||
|
||||
<m.div variants={varBounce('in')}>
|
||||
<Typography sx={{ color: 'text.secondary' }}>
|
||||
You do not have permission to access this page.
|
||||
</Typography>
|
||||
</m.div>
|
||||
|
||||
<m.div variants={varBounce('in')}>
|
||||
<ForbiddenIllustration sx={{ my: { xs: 5, sm: 10 } }} />
|
||||
</m.div>
|
||||
</Container>
|
||||
) : null;
|
||||
}
|
||||
|
||||
return <> {children} </>;
|
||||
}
|
3
03_source/frontend/src/auth/hooks/index.ts
Normal file
3
03_source/frontend/src/auth/hooks/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './use-mocked-user';
|
||||
|
||||
export * from './use-auth-context';
|
15
03_source/frontend/src/auth/hooks/use-auth-context.ts
Normal file
15
03_source/frontend/src/auth/hooks/use-auth-context.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { use } from 'react';
|
||||
|
||||
import { AuthContext } from '../context/auth-context';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function useAuthContext() {
|
||||
const context = use(AuthContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useAuthContext: Context must be used inside AuthProvider');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
33
03_source/frontend/src/auth/hooks/use-mocked-user.ts
Normal file
33
03_source/frontend/src/auth/hooks/use-mocked-user.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { _mock } from 'src/_mock';
|
||||
|
||||
// To get the user from the <AuthContext/>, you can use
|
||||
|
||||
// Change:
|
||||
// import { useMockedUser } from 'src/auth/hooks';
|
||||
// const { user } = useMockedUser();
|
||||
|
||||
// To:
|
||||
// import { useAuthContext } from 'src/auth/hooks';
|
||||
// const { user } = useAuthContext();
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function useMockedUser() {
|
||||
const user = {
|
||||
id: '8864c717-587d-472a-929a-8e5f298024da-0',
|
||||
displayName: 'Jaydon Frankie',
|
||||
email: 'demo@minimals.cc',
|
||||
photoURL: _mock.image.avatar(24),
|
||||
phoneNumber: _mock.phoneNumber(1),
|
||||
country: _mock.countryNames(1),
|
||||
address: '90210 Broadway Blvd',
|
||||
state: 'California',
|
||||
city: 'San Francisco',
|
||||
zipCode: '94116',
|
||||
about: 'Praesent turpis. Phasellus viverra nulla ut metus varius laoreet. Phasellus tempus.',
|
||||
role: 'admin',
|
||||
isPublic: true,
|
||||
};
|
||||
|
||||
return { user };
|
||||
}
|
14
03_source/frontend/src/auth/types.ts
Normal file
14
03_source/frontend/src/auth/types.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export type UserType = Record<string, any> | null;
|
||||
|
||||
export type AuthState = {
|
||||
user: UserType;
|
||||
loading: boolean;
|
||||
};
|
||||
|
||||
export type AuthContextValue = {
|
||||
user: UserType;
|
||||
loading: boolean;
|
||||
authenticated: boolean;
|
||||
unauthenticated: boolean;
|
||||
checkUserSession?: () => Promise<void>;
|
||||
};
|
20
03_source/frontend/src/auth/utils/error-message.ts
Normal file
20
03_source/frontend/src/auth/utils/error-message.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function getErrorMessage(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
return error.message || error.name || 'An error occurred';
|
||||
}
|
||||
|
||||
if (typeof error === 'string') {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (typeof error === 'object' && error !== null) {
|
||||
const errorMessage = (error as { message?: string }).message;
|
||||
if (typeof errorMessage === 'string') {
|
||||
return errorMessage;
|
||||
}
|
||||
}
|
||||
|
||||
return `Unknown error: ${error}`;
|
||||
}
|
1
03_source/frontend/src/auth/utils/index.ts
Normal file
1
03_source/frontend/src/auth/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './error-message';
|
@@ -0,0 +1,104 @@
|
||||
import { z as zod } from 'zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
|
||||
import { paths } from 'src/routes/paths';
|
||||
import { useRouter } from 'src/routes/hooks';
|
||||
|
||||
import { PasswordIcon } from 'src/assets/icons';
|
||||
|
||||
import { Form, Field } from 'src/components/hook-form';
|
||||
|
||||
import { resetPassword } from '../../context/amplify';
|
||||
import { FormHead } from '../../components/form-head';
|
||||
import { FormReturnLink } from '../../components/form-return-link';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export type ResetPasswordSchemaType = zod.infer<typeof ResetPasswordSchema>;
|
||||
|
||||
export const ResetPasswordSchema = zod.object({
|
||||
email: zod
|
||||
.string()
|
||||
.min(1, { message: 'Email is required!' })
|
||||
.email({ message: 'Email must be a valid email address!' }),
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function AmplifyResetPasswordView() {
|
||||
const router = useRouter();
|
||||
|
||||
const defaultValues: ResetPasswordSchemaType = {
|
||||
email: '',
|
||||
};
|
||||
|
||||
const methods = useForm<ResetPasswordSchemaType>({
|
||||
resolver: zodResolver(ResetPasswordSchema),
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
} = methods;
|
||||
|
||||
const createRedirectPath = (query: string) => {
|
||||
const queryString = new URLSearchParams({ email: query }).toString();
|
||||
return `${paths.auth.amplify.updatePassword}?${queryString}`;
|
||||
};
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
try {
|
||||
await resetPassword({ username: data.email });
|
||||
|
||||
const redirectPath = createRedirectPath(data.email);
|
||||
|
||||
router.push(redirectPath);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
const renderForm = () => (
|
||||
<Box sx={{ gap: 3, display: 'flex', flexDirection: 'column' }}>
|
||||
<Field.Text
|
||||
autoFocus
|
||||
name="email"
|
||||
label="Email address"
|
||||
placeholder="example@gmail.com"
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
/>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
size="large"
|
||||
type="submit"
|
||||
variant="contained"
|
||||
loading={isSubmitting}
|
||||
loadingIndicator="Send request..."
|
||||
>
|
||||
Send request
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormHead
|
||||
icon={<PasswordIcon />}
|
||||
title="Forgot your password?"
|
||||
description={`Please enter the email address associated with your account and we'll email you a link to reset your password.`}
|
||||
/>
|
||||
|
||||
<Form methods={methods} onSubmit={onSubmit}>
|
||||
{renderForm()}
|
||||
</Form>
|
||||
|
||||
<FormReturnLink href={paths.auth.amplify.signIn} />
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,157 @@
|
||||
import { z as zod } from 'zod';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useBoolean } from 'minimal-shared/hooks';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Link from '@mui/material/Link';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Button from '@mui/material/Button';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
|
||||
import { paths } from 'src/routes/paths';
|
||||
import { useRouter } from 'src/routes/hooks';
|
||||
import { RouterLink } from 'src/routes/components';
|
||||
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import { Form, Field } from 'src/components/hook-form';
|
||||
|
||||
import { useAuthContext } from '../../hooks';
|
||||
import { getErrorMessage } from '../../utils';
|
||||
import { FormHead } from '../../components/form-head';
|
||||
import { signInWithPassword } from '../../context/amplify';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export type SignInSchemaType = zod.infer<typeof SignInSchema>;
|
||||
|
||||
export const SignInSchema = zod.object({
|
||||
email: zod
|
||||
.string()
|
||||
.min(1, { message: 'Email is required!' })
|
||||
.email({ message: 'Email must be a valid email address!' }),
|
||||
password: zod
|
||||
.string()
|
||||
.min(1, { message: 'Password is required!' })
|
||||
.min(6, { message: 'Password must be at least 6 characters!' }),
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function AmplifySignInView() {
|
||||
const router = useRouter();
|
||||
|
||||
const showPassword = useBoolean();
|
||||
|
||||
const { checkUserSession } = useAuthContext();
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const defaultValues: SignInSchemaType = {
|
||||
email: '',
|
||||
password: '',
|
||||
};
|
||||
|
||||
const methods = useForm<SignInSchemaType>({
|
||||
resolver: zodResolver(SignInSchema),
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
} = methods;
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
try {
|
||||
await signInWithPassword({ username: data.email, password: data.password });
|
||||
await checkUserSession?.();
|
||||
|
||||
router.refresh();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const feedbackMessage = getErrorMessage(error);
|
||||
setErrorMessage(feedbackMessage);
|
||||
}
|
||||
});
|
||||
|
||||
const renderForm = () => (
|
||||
<Box sx={{ gap: 3, display: 'flex', flexDirection: 'column' }}>
|
||||
<Field.Text name="email" label="Email address" slotProps={{ inputLabel: { shrink: true } }} />
|
||||
|
||||
<Box sx={{ gap: 1.5, display: 'flex', flexDirection: 'column' }}>
|
||||
<Link
|
||||
component={RouterLink}
|
||||
href={paths.auth.amplify.resetPassword}
|
||||
variant="body2"
|
||||
color="inherit"
|
||||
sx={{ alignSelf: 'flex-end' }}
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
|
||||
<Field.Text
|
||||
name="password"
|
||||
label="Password"
|
||||
placeholder="6+ characters"
|
||||
type={showPassword.value ? 'text' : 'password'}
|
||||
slotProps={{
|
||||
inputLabel: { shrink: true },
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={showPassword.onToggle} edge="end">
|
||||
<Iconify
|
||||
icon={showPassword.value ? 'solar:eye-bold' : 'solar:eye-closed-bold'}
|
||||
/>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
color="inherit"
|
||||
size="large"
|
||||
type="submit"
|
||||
variant="contained"
|
||||
loading={isSubmitting}
|
||||
loadingIndicator="Sign in..."
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormHead
|
||||
title="Sign in to your account"
|
||||
description={
|
||||
<>
|
||||
{`Don’t have an account? `}
|
||||
<Link component={RouterLink} href={paths.auth.amplify.signUp} variant="subtitle2">
|
||||
Get started
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
sx={{ textAlign: { xs: 'center', md: 'left' } }}
|
||||
/>
|
||||
|
||||
{!!errorMessage && (
|
||||
<Alert severity="error" sx={{ mb: 3 }}>
|
||||
{errorMessage}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Form methods={methods} onSubmit={onSubmit}>
|
||||
{renderForm()}
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,173 @@
|
||||
import { z as zod } from 'zod';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useBoolean } from 'minimal-shared/hooks';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Link from '@mui/material/Link';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Button from '@mui/material/Button';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
|
||||
import { paths } from 'src/routes/paths';
|
||||
import { useRouter } from 'src/routes/hooks';
|
||||
import { RouterLink } from 'src/routes/components';
|
||||
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import { Form, Field } from 'src/components/hook-form';
|
||||
|
||||
import { getErrorMessage } from '../../utils';
|
||||
import { signUp } from '../../context/amplify';
|
||||
import { FormHead } from '../../components/form-head';
|
||||
import { SignUpTerms } from '../../components/sign-up-terms';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export type SignUpSchemaType = zod.infer<typeof SignUpSchema>;
|
||||
|
||||
export const SignUpSchema = zod.object({
|
||||
firstName: zod.string().min(1, { message: 'First name is required!' }),
|
||||
lastName: zod.string().min(1, { message: 'Last name is required!' }),
|
||||
email: zod
|
||||
.string()
|
||||
.min(1, { message: 'Email is required!' })
|
||||
.email({ message: 'Email must be a valid email address!' }),
|
||||
password: zod
|
||||
.string()
|
||||
.min(1, { message: 'Password is required!' })
|
||||
.min(6, { message: 'Password must be at least 6 characters!' }),
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function AmplifySignUpView() {
|
||||
const router = useRouter();
|
||||
|
||||
const showPassword = useBoolean();
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const defaultValues: SignUpSchemaType = {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
password: '',
|
||||
};
|
||||
|
||||
const methods = useForm<SignUpSchemaType>({
|
||||
resolver: zodResolver(SignUpSchema),
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
} = methods;
|
||||
|
||||
const createRedirectPath = (query: string) => {
|
||||
const queryString = new URLSearchParams({ email: query }).toString();
|
||||
return `${paths.auth.amplify.verify}?${queryString}`;
|
||||
};
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
try {
|
||||
await signUp({
|
||||
username: data.email,
|
||||
password: data.password,
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
});
|
||||
|
||||
const redirectPath = createRedirectPath(data.email);
|
||||
|
||||
router.push(redirectPath);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const feedbackMessage = getErrorMessage(error);
|
||||
setErrorMessage(feedbackMessage);
|
||||
}
|
||||
});
|
||||
|
||||
const renderForm = () => (
|
||||
<Box sx={{ gap: 3, display: 'flex', flexDirection: 'column' }}>
|
||||
<Box
|
||||
sx={{ display: 'flex', gap: { xs: 3, sm: 2 }, flexDirection: { xs: 'column', sm: 'row' } }}
|
||||
>
|
||||
<Field.Text
|
||||
name="firstName"
|
||||
label="First name"
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
/>
|
||||
<Field.Text
|
||||
name="lastName"
|
||||
label="Last name"
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Field.Text name="email" label="Email address" slotProps={{ inputLabel: { shrink: true } }} />
|
||||
|
||||
<Field.Text
|
||||
name="password"
|
||||
label="Password"
|
||||
placeholder="6+ characters"
|
||||
type={showPassword.value ? 'text' : 'password'}
|
||||
slotProps={{
|
||||
inputLabel: { shrink: true },
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={showPassword.onToggle} edge="end">
|
||||
<Iconify icon={showPassword.value ? 'solar:eye-bold' : 'solar:eye-closed-bold'} />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
color="inherit"
|
||||
size="large"
|
||||
type="submit"
|
||||
variant="contained"
|
||||
loading={isSubmitting}
|
||||
loadingIndicator="Create account..."
|
||||
>
|
||||
Create account
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormHead
|
||||
title="Get started absolutely free"
|
||||
description={
|
||||
<>
|
||||
{`Already have an account? `}
|
||||
<Link component={RouterLink} href={paths.auth.amplify.signIn} variant="subtitle2">
|
||||
Get started
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
sx={{ textAlign: { xs: 'center', md: 'left' } }}
|
||||
/>
|
||||
|
||||
{!!errorMessage && (
|
||||
<Alert severity="error" sx={{ mb: 3 }}>
|
||||
{errorMessage}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Form methods={methods} onSubmit={onSubmit}>
|
||||
{renderForm()}
|
||||
</Form>
|
||||
|
||||
<SignUpTerms />
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,193 @@
|
||||
import { z as zod } from 'zod';
|
||||
import { useCallback } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useBoolean, useCountdownSeconds } from 'minimal-shared/hooks';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
|
||||
import { paths } from 'src/routes/paths';
|
||||
import { useRouter, useSearchParams } from 'src/routes/hooks';
|
||||
|
||||
import { SentIcon } from 'src/assets/icons';
|
||||
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import { Form, Field } from 'src/components/hook-form';
|
||||
|
||||
import { FormHead } from '../../components/form-head';
|
||||
import { FormReturnLink } from '../../components/form-return-link';
|
||||
import { FormResendCode } from '../../components/form-resend-code';
|
||||
import { resetPassword, updatePassword } from '../../context/amplify';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export type UpdatePasswordSchemaType = zod.infer<typeof UpdatePasswordSchema>;
|
||||
|
||||
export const UpdatePasswordSchema = zod
|
||||
.object({
|
||||
code: zod
|
||||
.string()
|
||||
.min(1, { message: 'Code is required!' })
|
||||
.min(6, { message: 'Code must be at least 6 characters!' }),
|
||||
email: zod
|
||||
.string()
|
||||
.min(1, { message: 'Email is required!' })
|
||||
.email({ message: 'Email must be a valid email address!' }),
|
||||
password: zod
|
||||
.string()
|
||||
.min(1, { message: 'Password is required!' })
|
||||
.min(6, { message: 'Password must be at least 6 characters!' }),
|
||||
confirmPassword: zod.string().min(1, { message: 'Confirm password is required!' }),
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
message: 'Passwords do not match!',
|
||||
path: ['confirmPassword'],
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function AmplifyUpdatePasswordView() {
|
||||
const router = useRouter();
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const email = searchParams.get('email');
|
||||
|
||||
const showPassword = useBoolean();
|
||||
|
||||
const countdown = useCountdownSeconds(5);
|
||||
|
||||
const defaultValues: UpdatePasswordSchemaType = {
|
||||
code: '',
|
||||
email: email || '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
};
|
||||
|
||||
const methods = useForm<UpdatePasswordSchemaType>({
|
||||
resolver: zodResolver(UpdatePasswordSchema),
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const {
|
||||
watch,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
} = methods;
|
||||
|
||||
const values = watch();
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
try {
|
||||
await updatePassword({
|
||||
username: data.email,
|
||||
confirmationCode: data.code,
|
||||
newPassword: data.password,
|
||||
});
|
||||
|
||||
router.push(paths.auth.amplify.signIn);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
const handleResendCode = useCallback(async () => {
|
||||
if (!countdown.isCounting) {
|
||||
try {
|
||||
countdown.reset();
|
||||
countdown.start();
|
||||
|
||||
await resetPassword({ username: values.email });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}, [countdown, values.email]);
|
||||
|
||||
const renderForm = () => (
|
||||
<Box sx={{ gap: 3, display: 'flex', flexDirection: 'column' }}>
|
||||
<Field.Text
|
||||
name="email"
|
||||
label="Email address"
|
||||
placeholder="example@gmail.com"
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
disabled
|
||||
/>
|
||||
|
||||
<Field.Code name="code" />
|
||||
|
||||
<Field.Text
|
||||
name="password"
|
||||
label="Password"
|
||||
placeholder="6+ characters"
|
||||
type={showPassword.value ? 'text' : 'password'}
|
||||
slotProps={{
|
||||
inputLabel: { shrink: true },
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={showPassword.onToggle} edge="end">
|
||||
<Iconify icon={showPassword.value ? 'solar:eye-bold' : 'solar:eye-closed-bold'} />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Field.Text
|
||||
name="confirmPassword"
|
||||
label="Confirm new password"
|
||||
type={showPassword.value ? 'text' : 'password'}
|
||||
slotProps={{
|
||||
inputLabel: { shrink: true },
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={showPassword.onToggle} edge="end">
|
||||
<Iconify icon={showPassword.value ? 'solar:eye-bold' : 'solar:eye-closed-bold'} />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
size="large"
|
||||
type="submit"
|
||||
variant="contained"
|
||||
loading={isSubmitting}
|
||||
loadingIndicator="Update password..."
|
||||
>
|
||||
Update password
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormHead
|
||||
icon={<SentIcon />}
|
||||
title="Request sent successfully!"
|
||||
description={`We've sent a 6-digit confirmation email to your email. \nPlease enter the code in below box to verify your email.`}
|
||||
/>
|
||||
|
||||
<Form methods={methods} onSubmit={onSubmit}>
|
||||
{renderForm()}
|
||||
</Form>
|
||||
|
||||
<FormResendCode
|
||||
onResendCode={handleResendCode}
|
||||
value={countdown.value}
|
||||
disabled={countdown.isCounting}
|
||||
/>
|
||||
|
||||
<FormReturnLink href={paths.auth.amplify.signIn} />
|
||||
</>
|
||||
);
|
||||
}
|
134
03_source/frontend/src/auth/view/amplify/amplify-verify-view.tsx
Normal file
134
03_source/frontend/src/auth/view/amplify/amplify-verify-view.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import { z as zod } from 'zod';
|
||||
import { useCallback } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useCountdownSeconds } from 'minimal-shared/hooks';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
|
||||
import { paths } from 'src/routes/paths';
|
||||
import { useRouter, useSearchParams } from 'src/routes/hooks';
|
||||
|
||||
import { EmailInboxIcon } from 'src/assets/icons';
|
||||
|
||||
import { Form, Field } from 'src/components/hook-form';
|
||||
|
||||
import { FormHead } from '../../components/form-head';
|
||||
import { FormReturnLink } from '../../components/form-return-link';
|
||||
import { FormResendCode } from '../../components/form-resend-code';
|
||||
import { confirmSignUp, resendSignUpCode } from '../../context/amplify';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export type VerifySchemaType = zod.infer<typeof VerifySchema>;
|
||||
|
||||
export const VerifySchema = zod.object({
|
||||
code: zod
|
||||
.string()
|
||||
.min(1, { message: 'Code is required!' })
|
||||
.min(6, { message: 'Code must be at least 6 characters!' }),
|
||||
email: zod
|
||||
.string()
|
||||
.min(1, { message: 'Email is required!' })
|
||||
.email({ message: 'Email must be a valid email address!' }),
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function AmplifyVerifyView() {
|
||||
const router = useRouter();
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const email = searchParams.get('email');
|
||||
|
||||
const countdown = useCountdownSeconds(5);
|
||||
|
||||
const defaultValues: VerifySchemaType = {
|
||||
code: '',
|
||||
email: email || '',
|
||||
};
|
||||
|
||||
const methods = useForm<VerifySchemaType>({
|
||||
resolver: zodResolver(VerifySchema),
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const {
|
||||
watch,
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
} = methods;
|
||||
|
||||
const values = watch();
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
try {
|
||||
await confirmSignUp({ username: data.email, confirmationCode: data.code });
|
||||
router.push(paths.auth.amplify.signIn);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
const handleResendCode = useCallback(async () => {
|
||||
if (!countdown.isCounting) {
|
||||
try {
|
||||
countdown.reset();
|
||||
countdown.start();
|
||||
|
||||
await resendSignUpCode?.({ username: values.email });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}, [countdown, values.email]);
|
||||
|
||||
const renderForm = () => (
|
||||
<Box sx={{ gap: 3, display: 'flex', flexDirection: 'column' }}>
|
||||
<Field.Text
|
||||
name="email"
|
||||
label="Email address"
|
||||
placeholder="example@gmail.com"
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
disabled
|
||||
/>
|
||||
|
||||
<Field.Code name="code" />
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
size="large"
|
||||
type="submit"
|
||||
variant="contained"
|
||||
loading={isSubmitting}
|
||||
loadingIndicator="Verify..."
|
||||
>
|
||||
Verify
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormHead
|
||||
icon={<EmailInboxIcon />}
|
||||
title="Please check your email!"
|
||||
description={`We've emailed a 6-digit confirmation code. \nPlease enter the code in the box below to verify your email.`}
|
||||
/>
|
||||
|
||||
<Form methods={methods} onSubmit={onSubmit}>
|
||||
{renderForm()}
|
||||
</Form>
|
||||
|
||||
<FormResendCode
|
||||
onResendCode={handleResendCode}
|
||||
value={countdown.value}
|
||||
disabled={countdown.isCounting}
|
||||
/>
|
||||
|
||||
<FormReturnLink href={paths.auth.amplify.signIn} />
|
||||
</>
|
||||
);
|
||||
}
|
9
03_source/frontend/src/auth/view/amplify/index.ts
Normal file
9
03_source/frontend/src/auth/view/amplify/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export * from './amplify-verify-view';
|
||||
|
||||
export * from './amplify-sign-in-view';
|
||||
|
||||
export * from './amplify-sign-up-view';
|
||||
|
||||
export * from './amplify-reset-password-view';
|
||||
|
||||
export * from './amplify-update-password-view';
|
@@ -0,0 +1,92 @@
|
||||
import { z as zod } from 'zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
|
||||
import { paths } from 'src/routes/paths';
|
||||
|
||||
import { PasswordIcon } from 'src/assets/icons';
|
||||
|
||||
import { Form, Field } from 'src/components/hook-form';
|
||||
|
||||
import { FormHead } from '../../../components/form-head';
|
||||
import { FormReturnLink } from '../../../components/form-return-link';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export type ResetPasswordSchemaType = zod.infer<typeof ResetPasswordSchema>;
|
||||
|
||||
export const ResetPasswordSchema = zod.object({
|
||||
email: zod
|
||||
.string()
|
||||
.min(1, { message: 'Email is required!' })
|
||||
.email({ message: 'Email must be a valid email address!' }),
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function CenteredResetPasswordView() {
|
||||
const defaultValues: ResetPasswordSchemaType = {
|
||||
email: '',
|
||||
};
|
||||
|
||||
const methods = useForm<ResetPasswordSchemaType>({
|
||||
resolver: zodResolver(ResetPasswordSchema),
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
} = methods;
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
console.info('DATA', data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
const renderForm = () => (
|
||||
<Box sx={{ gap: 3, display: 'flex', flexDirection: 'column' }}>
|
||||
<Field.Text
|
||||
name="email"
|
||||
label="Email address"
|
||||
placeholder="example@gmail.com"
|
||||
autoFocus
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
/>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
size="large"
|
||||
type="submit"
|
||||
variant="contained"
|
||||
loading={isSubmitting}
|
||||
loadingIndicator="Send request..."
|
||||
>
|
||||
Send request
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormHead
|
||||
icon={<PasswordIcon />}
|
||||
title="Forgot your password?"
|
||||
description={`Please enter the email address associated with your account and we'll email you a link to reset your password.`}
|
||||
/>
|
||||
|
||||
<Form methods={methods} onSubmit={onSubmit}>
|
||||
{renderForm()}
|
||||
</Form>
|
||||
|
||||
<FormReturnLink href={paths.authDemo.centered.signIn} />
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,147 @@
|
||||
import { z as zod } from 'zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useBoolean } from 'minimal-shared/hooks';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Link from '@mui/material/Link';
|
||||
import Button from '@mui/material/Button';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
|
||||
import { paths } from 'src/routes/paths';
|
||||
import { RouterLink } from 'src/routes/components';
|
||||
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import { Form, Field } from 'src/components/hook-form';
|
||||
import { AnimateLogoRotate } from 'src/components/animate';
|
||||
|
||||
import { FormHead } from '../../../components/form-head';
|
||||
import { FormSocials } from '../../../components/form-socials';
|
||||
import { FormDivider } from '../../../components/form-divider';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export type SignInSchemaType = zod.infer<typeof SignInSchema>;
|
||||
|
||||
export const SignInSchema = zod.object({
|
||||
email: zod
|
||||
.string()
|
||||
.min(1, { message: 'Email is required!' })
|
||||
.email({ message: 'Email must be a valid email address!' }),
|
||||
password: zod
|
||||
.string()
|
||||
.min(1, { message: 'Password is required!' })
|
||||
.min(6, { message: 'Password must be at least 6 characters!' }),
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function CenteredSignInView() {
|
||||
const showPassword = useBoolean();
|
||||
|
||||
const defaultValues: SignInSchemaType = {
|
||||
email: '',
|
||||
password: '',
|
||||
};
|
||||
|
||||
const methods = useForm<SignInSchemaType>({
|
||||
resolver: zodResolver(SignInSchema),
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
} = methods;
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
console.info('DATA', data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
const renderForm = () => (
|
||||
<Box sx={{ gap: 3, display: 'flex', flexDirection: 'column' }}>
|
||||
<Field.Text name="email" label="Email address" slotProps={{ inputLabel: { shrink: true } }} />
|
||||
|
||||
<Box sx={{ gap: 1.5, display: 'flex', flexDirection: 'column' }}>
|
||||
<Link
|
||||
component={RouterLink}
|
||||
href={paths.authDemo.centered.resetPassword}
|
||||
variant="body2"
|
||||
color="inherit"
|
||||
sx={{ alignSelf: 'flex-end' }}
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
|
||||
<Field.Text
|
||||
name="password"
|
||||
label="Password"
|
||||
placeholder="6+ characters"
|
||||
type={showPassword.value ? 'text' : 'password'}
|
||||
slotProps={{
|
||||
inputLabel: { shrink: true },
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={showPassword.onToggle} edge="end">
|
||||
<Iconify
|
||||
icon={showPassword.value ? 'solar:eye-bold' : 'solar:eye-closed-bold'}
|
||||
/>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
color="inherit"
|
||||
size="large"
|
||||
type="submit"
|
||||
variant="contained"
|
||||
loading={isSubmitting}
|
||||
loadingIndicator="Sign in..."
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AnimateLogoRotate sx={{ mb: 3, mx: 'auto' }} />
|
||||
|
||||
<FormHead
|
||||
title="Sign in to your account"
|
||||
description={
|
||||
<>
|
||||
{`Don’t have an account? `}
|
||||
<Link component={RouterLink} href={paths.authDemo.centered.signUp} variant="subtitle2">
|
||||
Get started
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<Form methods={methods} onSubmit={onSubmit}>
|
||||
{renderForm()}
|
||||
</Form>
|
||||
|
||||
<FormDivider />
|
||||
|
||||
<FormSocials
|
||||
signInWithGoogle={() => {}}
|
||||
singInWithGithub={() => {}}
|
||||
signInWithTwitter={() => {}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,165 @@
|
||||
import { z as zod } from 'zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useBoolean } from 'minimal-shared/hooks';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Link from '@mui/material/Link';
|
||||
import Button from '@mui/material/Button';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
|
||||
import { paths } from 'src/routes/paths';
|
||||
import { RouterLink } from 'src/routes/components';
|
||||
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import { Form, Field } from 'src/components/hook-form';
|
||||
import { AnimateLogoRotate } from 'src/components/animate';
|
||||
|
||||
import { FormHead } from '../../../components/form-head';
|
||||
import { FormSocials } from '../../../components/form-socials';
|
||||
import { FormDivider } from '../../../components/form-divider';
|
||||
import { SignUpTerms } from '../../../components/sign-up-terms';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export type SignUpSchemaType = zod.infer<typeof SignUpSchema>;
|
||||
|
||||
export const SignUpSchema = zod.object({
|
||||
firstName: zod.string().min(1, { message: 'First name is required!' }),
|
||||
lastName: zod.string().min(1, { message: 'Last name is required!' }),
|
||||
email: zod
|
||||
.string()
|
||||
.min(1, { message: 'Email is required!' })
|
||||
.email({ message: 'Email must be a valid email address!' }),
|
||||
password: zod
|
||||
.string()
|
||||
.min(1, { message: 'Password is required!' })
|
||||
.min(6, { message: 'Password must be at least 6 characters!' }),
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function CenteredSignUpView() {
|
||||
const showPassword = useBoolean();
|
||||
|
||||
const defaultValues: SignUpSchemaType = {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
password: '',
|
||||
};
|
||||
|
||||
const methods = useForm<SignUpSchemaType>({
|
||||
resolver: zodResolver(SignUpSchema),
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
} = methods;
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
console.info('DATA', data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
const renderForm = () => (
|
||||
<Box
|
||||
sx={{
|
||||
gap: 3,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
gap: { xs: 3, sm: 2 },
|
||||
flexDirection: { xs: 'column', sm: 'row' },
|
||||
}}
|
||||
>
|
||||
<Field.Text
|
||||
name="firstName"
|
||||
label="First name"
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
/>
|
||||
<Field.Text
|
||||
name="lastName"
|
||||
label="Last name"
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Field.Text name="email" label="Email address" slotProps={{ inputLabel: { shrink: true } }} />
|
||||
|
||||
<Field.Text
|
||||
name="password"
|
||||
label="Password"
|
||||
placeholder="6+ characters"
|
||||
type={showPassword.value ? 'text' : 'password'}
|
||||
slotProps={{
|
||||
inputLabel: { shrink: true },
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={showPassword.onToggle} edge="end">
|
||||
<Iconify icon={showPassword.value ? 'solar:eye-bold' : 'solar:eye-closed-bold'} />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
color="inherit"
|
||||
size="large"
|
||||
type="submit"
|
||||
variant="contained"
|
||||
loading={isSubmitting}
|
||||
loadingIndicator="Create account..."
|
||||
>
|
||||
Create account
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AnimateLogoRotate sx={{ mb: 3, mx: 'auto' }} />
|
||||
|
||||
<FormHead
|
||||
title="Get started absolutely free"
|
||||
description={
|
||||
<>
|
||||
{`Already have an account? `}
|
||||
<Link component={RouterLink} href={paths.authDemo.centered.signIn} variant="subtitle2">
|
||||
Get started
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<Form methods={methods} onSubmit={onSubmit}>
|
||||
{renderForm()}
|
||||
</Form>
|
||||
|
||||
<SignUpTerms />
|
||||
|
||||
<FormDivider />
|
||||
|
||||
<FormSocials
|
||||
signInWithGoogle={() => {}}
|
||||
singInWithGithub={() => {}}
|
||||
signInWithTwitter={() => {}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,155 @@
|
||||
import { z as zod } from 'zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useBoolean } from 'minimal-shared/hooks';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
|
||||
import { paths } from 'src/routes/paths';
|
||||
|
||||
import { SentIcon } from 'src/assets/icons';
|
||||
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import { Form, Field } from 'src/components/hook-form';
|
||||
|
||||
import { FormHead } from '../../../components/form-head';
|
||||
import { FormResendCode } from '../../../components/form-resend-code';
|
||||
import { FormReturnLink } from '../../../components/form-return-link';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export type UpdatePasswordSchemaType = zod.infer<typeof UpdatePasswordSchema>;
|
||||
|
||||
export const UpdatePasswordSchema = zod
|
||||
.object({
|
||||
code: zod
|
||||
.string()
|
||||
.min(1, { message: 'Code is required!' })
|
||||
.min(6, { message: 'Code must be at least 6 characters!' }),
|
||||
email: zod
|
||||
.string()
|
||||
.min(1, { message: 'Email is required!' })
|
||||
.email({ message: 'Email must be a valid email address!' }),
|
||||
password: zod
|
||||
.string()
|
||||
.min(1, { message: 'Password is required!' })
|
||||
.min(6, { message: 'Password must be at least 6 characters!' }),
|
||||
confirmPassword: zod.string().min(1, { message: 'Confirm password is required!' }),
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
message: 'Passwords do not match!',
|
||||
path: ['confirmPassword'],
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function CenteredUpdatePasswordView() {
|
||||
const showPassword = useBoolean();
|
||||
|
||||
const defaultValues: UpdatePasswordSchemaType = {
|
||||
code: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
};
|
||||
|
||||
const methods = useForm<UpdatePasswordSchemaType>({
|
||||
resolver: zodResolver(UpdatePasswordSchema),
|
||||
defaultValues,
|
||||
});
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
} = methods;
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
console.info('DATA', data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
const renderForm = () => (
|
||||
<Box sx={{ gap: 3, display: 'flex', flexDirection: 'column' }}>
|
||||
<Field.Text
|
||||
name="email"
|
||||
label="Email address"
|
||||
placeholder="example@gmail.com"
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
/>
|
||||
|
||||
<Field.Code name="code" />
|
||||
|
||||
<Field.Text
|
||||
name="password"
|
||||
label="Password"
|
||||
placeholder="6+ characters"
|
||||
type={showPassword.value ? 'text' : 'password'}
|
||||
slotProps={{
|
||||
inputLabel: { shrink: true },
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={showPassword.onToggle} edge="end">
|
||||
<Iconify icon={showPassword.value ? 'solar:eye-bold' : 'solar:eye-closed-bold'} />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Field.Text
|
||||
name="confirmPassword"
|
||||
label="Confirm new password"
|
||||
type={showPassword.value ? 'text' : 'password'}
|
||||
slotProps={{
|
||||
inputLabel: { shrink: true },
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={showPassword.onToggle} edge="end">
|
||||
<Iconify icon={showPassword.value ? 'solar:eye-bold' : 'solar:eye-closed-bold'} />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
size="large"
|
||||
type="submit"
|
||||
variant="contained"
|
||||
loading={isSubmitting}
|
||||
loadingIndicator="Update password..."
|
||||
>
|
||||
Update password
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormHead
|
||||
icon={<SentIcon />}
|
||||
title="Request sent successfully!"
|
||||
description={`We've sent a 6-digit confirmation email to your email. \nPlease enter the code in below box to verify your email.`}
|
||||
/>
|
||||
|
||||
<Form methods={methods} onSubmit={onSubmit}>
|
||||
{renderForm()}
|
||||
</Form>
|
||||
|
||||
<FormResendCode onResendCode={() => {}} value={0} disabled={false} />
|
||||
|
||||
<FormReturnLink href={paths.authDemo.centered.signIn} />
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,101 @@
|
||||
import { z as zod } from 'zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
|
||||
import { paths } from 'src/routes/paths';
|
||||
|
||||
import { EmailInboxIcon } from 'src/assets/icons';
|
||||
|
||||
import { Form, Field } from 'src/components/hook-form';
|
||||
|
||||
import { FormHead } from '../../../components/form-head';
|
||||
import { FormResendCode } from '../../../components/form-resend-code';
|
||||
import { FormReturnLink } from '../../../components/form-return-link';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export type VerifySchemaType = zod.infer<typeof VerifySchema>;
|
||||
|
||||
export const VerifySchema = zod.object({
|
||||
code: zod
|
||||
.string()
|
||||
.min(1, { message: 'Code is required!' })
|
||||
.min(6, { message: 'Code must be at least 6 characters!' }),
|
||||
email: zod
|
||||
.string()
|
||||
.min(1, { message: 'Email is required!' })
|
||||
.email({ message: 'Email must be a valid email address!' }),
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function CenteredVerifyView() {
|
||||
const defaultValues: VerifySchemaType = {
|
||||
code: '',
|
||||
email: '',
|
||||
};
|
||||
|
||||
const methods = useForm<VerifySchemaType>({
|
||||
resolver: zodResolver(VerifySchema),
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
} = methods;
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
console.info('DATA', data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
const renderForm = () => (
|
||||
<Box sx={{ gap: 3, display: 'flex', flexDirection: 'column' }}>
|
||||
<Field.Text
|
||||
name="email"
|
||||
label="Email address"
|
||||
placeholder="example@gmail.com"
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
/>
|
||||
|
||||
<Field.Code name="code" />
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
size="large"
|
||||
type="submit"
|
||||
variant="contained"
|
||||
loading={isSubmitting}
|
||||
loadingIndicator="Verify..."
|
||||
>
|
||||
Verify
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormHead
|
||||
icon={<EmailInboxIcon />}
|
||||
title="Please check your email!"
|
||||
description={`We've emailed a 6-digit confirmation code. \nPlease enter the code in the box below to verify your email.`}
|
||||
/>
|
||||
|
||||
<Form methods={methods} onSubmit={onSubmit}>
|
||||
{renderForm()}
|
||||
</Form>
|
||||
|
||||
<FormResendCode onResendCode={() => {}} value={0} disabled={false} />
|
||||
|
||||
<FormReturnLink href={paths.authDemo.centered.signIn} />
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
export * from './centered-verify-view';
|
||||
|
||||
export * from './centered-sign-in-view';
|
||||
|
||||
export * from './centered-sign-up-view';
|
||||
|
||||
export * from './centered-reset-password-view';
|
||||
|
||||
export * from './centered-update-password-view';
|
@@ -0,0 +1,9 @@
|
||||
export * from './split-verify-view';
|
||||
|
||||
export * from './split-sign-in-view';
|
||||
|
||||
export * from './split-sign-up-view';
|
||||
|
||||
export * from './split-reset-password-view';
|
||||
|
||||
export * from './split-update-password-view';
|
@@ -0,0 +1,92 @@
|
||||
import { z as zod } from 'zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
|
||||
import { paths } from 'src/routes/paths';
|
||||
|
||||
import { PasswordIcon } from 'src/assets/icons';
|
||||
|
||||
import { Form, Field } from 'src/components/hook-form';
|
||||
|
||||
import { FormHead } from '../../../components/form-head';
|
||||
import { FormReturnLink } from '../../../components/form-return-link';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export type ResetPasswordSchemaType = zod.infer<typeof ResetPasswordSchema>;
|
||||
|
||||
export const ResetPasswordSchema = zod.object({
|
||||
email: zod
|
||||
.string()
|
||||
.min(1, { message: 'Email is required!' })
|
||||
.email({ message: 'Email must be a valid email address!' }),
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function SplitResetPasswordView() {
|
||||
const defaultValues: ResetPasswordSchemaType = {
|
||||
email: '',
|
||||
};
|
||||
|
||||
const methods = useForm<ResetPasswordSchemaType>({
|
||||
resolver: zodResolver(ResetPasswordSchema),
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
} = methods;
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
console.info('DATA', data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
const renderForm = () => (
|
||||
<Box sx={{ gap: 3, display: 'flex', flexDirection: 'column' }}>
|
||||
<Field.Text
|
||||
autoFocus
|
||||
name="email"
|
||||
label="Email address"
|
||||
placeholder="example@gmail.com"
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
/>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
size="large"
|
||||
type="submit"
|
||||
variant="contained"
|
||||
loading={isSubmitting}
|
||||
loadingIndicator="Send request..."
|
||||
>
|
||||
Send request
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormHead
|
||||
icon={<PasswordIcon />}
|
||||
title="Forgot your password?"
|
||||
description={`Please enter the email address associated with your account and we'll email you a link to reset your password.`}
|
||||
/>
|
||||
|
||||
<Form methods={methods} onSubmit={onSubmit}>
|
||||
{renderForm()}
|
||||
</Form>
|
||||
|
||||
<FormReturnLink href={paths.authDemo.split.signIn} />
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,157 @@
|
||||
import { z as zod } from 'zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useBoolean } from 'minimal-shared/hooks';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Link from '@mui/material/Link';
|
||||
import Button from '@mui/material/Button';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
|
||||
import { paths } from 'src/routes/paths';
|
||||
import { RouterLink } from 'src/routes/components';
|
||||
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import { Form, Field } from 'src/components/hook-form';
|
||||
|
||||
import { FormHead } from '../../../components/form-head';
|
||||
import { FormSocials } from '../../../components/form-socials';
|
||||
import { FormDivider } from '../../../components/form-divider';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export type SignInSchemaType = zod.infer<typeof SignInSchema>;
|
||||
|
||||
export const SignInSchema = zod.object({
|
||||
email: zod
|
||||
.string()
|
||||
.min(1, { message: 'Email is required!' })
|
||||
.email({ message: 'Email must be a valid email address!' }),
|
||||
password: zod
|
||||
.string()
|
||||
.min(1, { message: 'Password is required!' })
|
||||
.min(6, { message: 'Password must be at least 6 characters!' }),
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function SplitSignInView() {
|
||||
const showPassword = useBoolean();
|
||||
|
||||
const defaultValues: SignInSchemaType = {
|
||||
email: '',
|
||||
password: '',
|
||||
};
|
||||
|
||||
const methods = useForm<SignInSchemaType>({
|
||||
resolver: zodResolver(SignInSchema),
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
} = methods;
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
console.info('DATA', data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
const renderForm = () => (
|
||||
<Box
|
||||
sx={{
|
||||
gap: 3,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<Field.Text name="email" label="Email address" slotProps={{ inputLabel: { shrink: true } }} />
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
gap: 1.5,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
component={RouterLink}
|
||||
href={paths.authDemo.split.resetPassword}
|
||||
variant="body2"
|
||||
color="inherit"
|
||||
sx={{ alignSelf: 'flex-end' }}
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
|
||||
<Field.Text
|
||||
name="password"
|
||||
label="Password"
|
||||
placeholder="6+ characters"
|
||||
type={showPassword.value ? 'text' : 'password'}
|
||||
slotProps={{
|
||||
inputLabel: { shrink: true },
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={showPassword.onToggle} edge="end">
|
||||
<Iconify
|
||||
icon={showPassword.value ? 'solar:eye-bold' : 'solar:eye-closed-bold'}
|
||||
/>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
color="inherit"
|
||||
size="large"
|
||||
type="submit"
|
||||
variant="contained"
|
||||
loading={isSubmitting}
|
||||
loadingIndicator="Sign in..."
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormHead
|
||||
title="Sign in to your account"
|
||||
description={
|
||||
<>
|
||||
{`Don’t have an account? `}
|
||||
<Link component={RouterLink} href={paths.authDemo.split.signUp} variant="subtitle2">
|
||||
Get started
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
sx={{ textAlign: { xs: 'center', md: 'left' } }}
|
||||
/>
|
||||
|
||||
<Form methods={methods} onSubmit={onSubmit}>
|
||||
{renderForm()}
|
||||
</Form>
|
||||
|
||||
<FormDivider />
|
||||
|
||||
<FormSocials
|
||||
signInWithGoogle={() => {}}
|
||||
singInWithGithub={() => {}}
|
||||
signInWithTwitter={() => {}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,153 @@
|
||||
import { z as zod } from 'zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useBoolean } from 'minimal-shared/hooks';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Link from '@mui/material/Link';
|
||||
import Button from '@mui/material/Button';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
|
||||
import { paths } from 'src/routes/paths';
|
||||
import { RouterLink } from 'src/routes/components';
|
||||
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import { Form, Field } from 'src/components/hook-form';
|
||||
|
||||
import { FormHead } from '../../../components/form-head';
|
||||
import { FormDivider } from '../../../components/form-divider';
|
||||
import { FormSocials } from '../../../components/form-socials';
|
||||
import { SignUpTerms } from '../../../components/sign-up-terms';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export type SignUpSchemaType = zod.infer<typeof SignUpSchema>;
|
||||
|
||||
export const SignUpSchema = zod.object({
|
||||
firstName: zod.string().min(1, { message: 'First name is required!' }),
|
||||
lastName: zod.string().min(1, { message: 'Last name is required!' }),
|
||||
email: zod
|
||||
.string()
|
||||
.min(1, { message: 'Email is required!' })
|
||||
.email({ message: 'Email must be a valid email address!' }),
|
||||
password: zod
|
||||
.string()
|
||||
.min(1, { message: 'Password is required!' })
|
||||
.min(6, { message: 'Password must be at least 6 characters!' }),
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function SplitSignUpView() {
|
||||
const showPassword = useBoolean();
|
||||
|
||||
const defaultValues: SignUpSchemaType = {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
password: '',
|
||||
};
|
||||
|
||||
const methods = useForm<SignUpSchemaType>({
|
||||
resolver: zodResolver(SignUpSchema),
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
} = methods;
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
console.info('DATA', data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
const renderForm = () => (
|
||||
<Box sx={{ gap: 3, display: 'flex', flexDirection: 'column' }}>
|
||||
<Box
|
||||
sx={{ display: 'flex', gap: { xs: 3, sm: 2 }, flexDirection: { xs: 'column', sm: 'row' } }}
|
||||
>
|
||||
<Field.Text
|
||||
name="firstName"
|
||||
label="First name"
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
/>
|
||||
<Field.Text
|
||||
name="lastName"
|
||||
label="Last name"
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Field.Text name="email" label="Email address" slotProps={{ inputLabel: { shrink: true } }} />
|
||||
|
||||
<Field.Text
|
||||
name="password"
|
||||
label="Password"
|
||||
placeholder="6+ characters"
|
||||
type={showPassword.value ? 'text' : 'password'}
|
||||
slotProps={{
|
||||
inputLabel: { shrink: true },
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={showPassword.onToggle} edge="end">
|
||||
<Iconify icon={showPassword.value ? 'solar:eye-bold' : 'solar:eye-closed-bold'} />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
color="inherit"
|
||||
size="large"
|
||||
type="submit"
|
||||
variant="contained"
|
||||
loading={isSubmitting}
|
||||
loadingIndicator="Create account..."
|
||||
>
|
||||
Create account
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormHead
|
||||
title="Get started absolutely free"
|
||||
description={
|
||||
<>
|
||||
{`Already have an account? `}
|
||||
<Link component={RouterLink} href={paths.authDemo.split.signIn} variant="subtitle2">
|
||||
Get started
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
sx={{ textAlign: { xs: 'center', md: 'left' } }}
|
||||
/>
|
||||
|
||||
<Form methods={methods} onSubmit={onSubmit}>
|
||||
{renderForm()}
|
||||
</Form>
|
||||
|
||||
<SignUpTerms />
|
||||
|
||||
<FormDivider />
|
||||
|
||||
<FormSocials
|
||||
signInWithGoogle={() => {}}
|
||||
singInWithGithub={() => {}}
|
||||
signInWithTwitter={() => {}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,156 @@
|
||||
import { z as zod } from 'zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useBoolean } from 'minimal-shared/hooks';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
|
||||
import { paths } from 'src/routes/paths';
|
||||
|
||||
import { SentIcon } from 'src/assets/icons';
|
||||
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import { Form, Field } from 'src/components/hook-form';
|
||||
|
||||
import { FormHead } from '../../../components/form-head';
|
||||
import { FormResendCode } from '../../../components/form-resend-code';
|
||||
import { FormReturnLink } from '../../../components/form-return-link';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export type UpdatePasswordSchemaType = zod.infer<typeof UpdatePasswordSchema>;
|
||||
|
||||
export const UpdatePasswordSchema = zod
|
||||
.object({
|
||||
code: zod
|
||||
.string()
|
||||
.min(1, { message: 'Code is required!' })
|
||||
.min(6, { message: 'Code must be at least 6 characters!' }),
|
||||
email: zod
|
||||
.string()
|
||||
.min(1, { message: 'Email is required!' })
|
||||
.email({ message: 'Email must be a valid email address!' }),
|
||||
password: zod
|
||||
.string()
|
||||
.min(1, { message: 'Password is required!' })
|
||||
.min(6, { message: 'Password must be at least 6 characters!' }),
|
||||
confirmPassword: zod.string().min(1, { message: 'Confirm password is required!' }),
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
message: 'Passwords do not match!',
|
||||
path: ['confirmPassword'],
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function SplitUpdatePasswordView() {
|
||||
const showPassword = useBoolean();
|
||||
|
||||
const defaultValues: UpdatePasswordSchemaType = {
|
||||
code: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
};
|
||||
|
||||
const methods = useForm<UpdatePasswordSchemaType>({
|
||||
resolver: zodResolver(UpdatePasswordSchema),
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
} = methods;
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
console.info('DATA', data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
const renderForm = () => (
|
||||
<Box sx={{ gap: 3, display: 'flex', flexDirection: 'column' }}>
|
||||
<Field.Text
|
||||
name="email"
|
||||
label="Email address"
|
||||
placeholder="example@gmail.com"
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
/>
|
||||
|
||||
<Field.Code name="code" />
|
||||
|
||||
<Field.Text
|
||||
name="password"
|
||||
label="Password"
|
||||
placeholder="6+ characters"
|
||||
type={showPassword.value ? 'text' : 'password'}
|
||||
slotProps={{
|
||||
inputLabel: { shrink: true },
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={showPassword.onToggle} edge="end">
|
||||
<Iconify icon={showPassword.value ? 'solar:eye-bold' : 'solar:eye-closed-bold'} />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Field.Text
|
||||
name="confirmPassword"
|
||||
label="Confirm new password"
|
||||
type={showPassword.value ? 'text' : 'password'}
|
||||
slotProps={{
|
||||
inputLabel: { shrink: true },
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={showPassword.onToggle} edge="end">
|
||||
<Iconify icon={showPassword.value ? 'solar:eye-bold' : 'solar:eye-closed-bold'} />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
size="large"
|
||||
type="submit"
|
||||
variant="contained"
|
||||
loading={isSubmitting}
|
||||
loadingIndicator="Update password..."
|
||||
>
|
||||
Update password
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormHead
|
||||
icon={<SentIcon />}
|
||||
title="Request sent successfully!"
|
||||
description={`We've sent a 6-digit confirmation email to your email. \nPlease enter the code in below box to verify your email.`}
|
||||
/>
|
||||
|
||||
<Form methods={methods} onSubmit={onSubmit}>
|
||||
{renderForm()}
|
||||
</Form>
|
||||
|
||||
<FormResendCode onResendCode={() => {}} value={0} disabled={false} />
|
||||
|
||||
<FormReturnLink href={paths.authDemo.split.signIn} />
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,101 @@
|
||||
import { z as zod } from 'zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
|
||||
import { paths } from 'src/routes/paths';
|
||||
|
||||
import { EmailInboxIcon } from 'src/assets/icons';
|
||||
|
||||
import { Form, Field } from 'src/components/hook-form';
|
||||
|
||||
import { FormHead } from '../../../components/form-head';
|
||||
import { FormReturnLink } from '../../../components/form-return-link';
|
||||
import { FormResendCode } from '../../../components/form-resend-code';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export type VerifySchemaType = zod.infer<typeof VerifySchema>;
|
||||
|
||||
export const VerifySchema = zod.object({
|
||||
code: zod
|
||||
.string()
|
||||
.min(1, { message: 'Code is required!' })
|
||||
.min(6, { message: 'Code must be at least 6 characters!' }),
|
||||
email: zod
|
||||
.string()
|
||||
.min(1, { message: 'Email is required!' })
|
||||
.email({ message: 'Email must be a valid email address!' }),
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function SplitVerifyView() {
|
||||
const defaultValues: VerifySchemaType = {
|
||||
code: '',
|
||||
email: '',
|
||||
};
|
||||
|
||||
const methods = useForm<VerifySchemaType>({
|
||||
resolver: zodResolver(VerifySchema),
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
} = methods;
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
console.info('DATA', data);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
const renderForm = () => (
|
||||
<Box sx={{ gap: 3, display: 'flex', flexDirection: 'column' }}>
|
||||
<Field.Text
|
||||
name="email"
|
||||
label="Email address"
|
||||
placeholder="example@gmail.com"
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
/>
|
||||
|
||||
<Field.Code name="code" />
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
size="large"
|
||||
type="submit"
|
||||
variant="contained"
|
||||
loading={isSubmitting}
|
||||
loadingIndicator="Verify..."
|
||||
>
|
||||
Verify
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormHead
|
||||
icon={<EmailInboxIcon />}
|
||||
title="Please check your email!"
|
||||
description={`We've emailed a 6-digit confirmation code. \nPlease enter the code in the box below to verify your email.`}
|
||||
/>
|
||||
|
||||
<Form methods={methods} onSubmit={onSubmit}>
|
||||
{renderForm()}
|
||||
</Form>
|
||||
|
||||
<FormResendCode onResendCode={() => {}} value={0} disabled={false} />
|
||||
|
||||
<FormReturnLink href={paths.authDemo.split.signIn} />
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,99 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useAuth0 } from '@auth0/auth0-react';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
import { useSearchParams } from 'src/routes/hooks';
|
||||
|
||||
import { CONFIG } from 'src/global-config';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function Auth0SignInView() {
|
||||
const { loginWithPopup, loginWithRedirect } = useAuth0();
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const returnTo = searchParams.get('returnTo');
|
||||
|
||||
const handleSignInWithPopup = useCallback(async () => {
|
||||
try {
|
||||
await loginWithPopup();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, [loginWithPopup]);
|
||||
|
||||
const handleSignUpWithPopup = useCallback(async () => {
|
||||
try {
|
||||
await loginWithPopup({ authorizationParams: { screen_hint: 'signup' } });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, [loginWithPopup]);
|
||||
|
||||
const handleSignInWithRedirect = useCallback(async () => {
|
||||
try {
|
||||
await loginWithRedirect({ appState: { returnTo: returnTo || CONFIG.auth.redirectPath } });
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, [loginWithRedirect, returnTo]);
|
||||
|
||||
const handleSignUpWithRedirect = useCallback(async () => {
|
||||
try {
|
||||
await loginWithRedirect({
|
||||
appState: { returnTo: returnTo || CONFIG.auth.redirectPath },
|
||||
authorizationParams: { screen_hint: 'signup' },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}, [loginWithRedirect, returnTo]);
|
||||
|
||||
return (
|
||||
<Box sx={{ gap: 3, display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography variant="h5" sx={{ textAlign: 'center' }}>
|
||||
Sign in to your account
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
color="primary"
|
||||
size="large"
|
||||
variant="contained"
|
||||
onClick={handleSignInWithRedirect}
|
||||
>
|
||||
Sign in with Redirect
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
color="primary"
|
||||
size="large"
|
||||
variant="soft"
|
||||
onClick={handleSignUpWithRedirect}
|
||||
>
|
||||
Sign up with Redirect
|
||||
</Button>
|
||||
|
||||
<Divider sx={{ borderStyle: 'dashed' }} />
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
color="inherit"
|
||||
size="large"
|
||||
variant="contained"
|
||||
onClick={handleSignInWithPopup}
|
||||
>
|
||||
Sign in with Popup
|
||||
</Button>
|
||||
<Button fullWidth color="inherit" size="large" variant="soft" onClick={handleSignUpWithPopup}>
|
||||
Sign up with Popup
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}
|
1
03_source/frontend/src/auth/view/auth0/index.ts
Normal file
1
03_source/frontend/src/auth/view/auth0/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './auth0-sign-in-view';
|
@@ -0,0 +1,104 @@
|
||||
import { z as zod } from 'zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
|
||||
import { paths } from 'src/routes/paths';
|
||||
import { useRouter } from 'src/routes/hooks';
|
||||
|
||||
import { PasswordIcon } from 'src/assets/icons';
|
||||
|
||||
import { Form, Field } from 'src/components/hook-form';
|
||||
|
||||
import { FormHead } from '../../components/form-head';
|
||||
import { sendPasswordResetEmail } from '../../context/firebase';
|
||||
import { FormReturnLink } from '../../components/form-return-link';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export type ResetPasswordSchemaType = zod.infer<typeof ResetPasswordSchema>;
|
||||
|
||||
export const ResetPasswordSchema = zod.object({
|
||||
email: zod
|
||||
.string()
|
||||
.min(1, { message: 'Email is required!' })
|
||||
.email({ message: 'Email must be a valid email address!' }),
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function FirebaseResetPasswordView() {
|
||||
const router = useRouter();
|
||||
|
||||
const defaultValues: ResetPasswordSchemaType = {
|
||||
email: '',
|
||||
};
|
||||
|
||||
const methods = useForm<ResetPasswordSchemaType>({
|
||||
resolver: zodResolver(ResetPasswordSchema),
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
} = methods;
|
||||
|
||||
const createRedirectPath = (query: string) => {
|
||||
const queryString = new URLSearchParams({ email: query }).toString();
|
||||
return `${paths.auth.firebase.verify}?${queryString}`;
|
||||
};
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
try {
|
||||
await sendPasswordResetEmail({ email: data.email });
|
||||
|
||||
const redirectPath = createRedirectPath(data.email);
|
||||
|
||||
router.push(redirectPath);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
const renderForm = () => (
|
||||
<Box sx={{ gap: 3, display: 'flex', flexDirection: 'column' }}>
|
||||
<Field.Text
|
||||
autoFocus
|
||||
name="email"
|
||||
label="Email address"
|
||||
placeholder="example@gmail.com"
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
/>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
size="large"
|
||||
type="submit"
|
||||
variant="contained"
|
||||
loading={isSubmitting}
|
||||
loadingIndicator="Send request..."
|
||||
>
|
||||
Send request
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormHead
|
||||
icon={<PasswordIcon />}
|
||||
title="Forgot your password?"
|
||||
description={`Please enter the email address associated with your account and we'll email you a link to reset your password.`}
|
||||
/>
|
||||
|
||||
<Form methods={methods} onSubmit={onSubmit}>
|
||||
{renderForm()}
|
||||
</Form>
|
||||
|
||||
<FormReturnLink href={paths.auth.firebase.signIn} />
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,196 @@
|
||||
import { z as zod } from 'zod';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useBoolean } from 'minimal-shared/hooks';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Link from '@mui/material/Link';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Button from '@mui/material/Button';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
|
||||
import { paths } from 'src/routes/paths';
|
||||
import { useRouter } from 'src/routes/hooks';
|
||||
import { RouterLink } from 'src/routes/components';
|
||||
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import { Form, Field } from 'src/components/hook-form';
|
||||
|
||||
import { useAuthContext } from '../../hooks';
|
||||
import { getErrorMessage } from '../../utils';
|
||||
import { FormHead } from '../../components/form-head';
|
||||
import { FormDivider } from '../../components/form-divider';
|
||||
import { FormSocials } from '../../components/form-socials';
|
||||
import {
|
||||
signInWithGoogle,
|
||||
signInWithGithub,
|
||||
signInWithTwitter,
|
||||
signInWithPassword,
|
||||
} from '../../context/firebase';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export type SignInSchemaType = zod.infer<typeof SignInSchema>;
|
||||
|
||||
export const SignInSchema = zod.object({
|
||||
email: zod
|
||||
.string()
|
||||
.min(1, { message: 'Email is required!' })
|
||||
.email({ message: 'Email must be a valid email address!' }),
|
||||
password: zod
|
||||
.string()
|
||||
.min(1, { message: 'Password is required!' })
|
||||
.min(6, { message: 'Password must be at least 6 characters!' }),
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function FirebaseSignInView() {
|
||||
const router = useRouter();
|
||||
|
||||
const showPassword = useBoolean();
|
||||
|
||||
const { checkUserSession } = useAuthContext();
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const defaultValues: SignInSchemaType = {
|
||||
email: '',
|
||||
password: '',
|
||||
};
|
||||
|
||||
const methods = useForm<SignInSchemaType>({
|
||||
resolver: zodResolver(SignInSchema),
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
} = methods;
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
try {
|
||||
await signInWithPassword({ email: data.email, password: data.password });
|
||||
await checkUserSession?.();
|
||||
|
||||
router.refresh();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const feedbackMessage = getErrorMessage(error);
|
||||
setErrorMessage(feedbackMessage);
|
||||
}
|
||||
});
|
||||
|
||||
const handleSignInWithGoogle = async () => {
|
||||
try {
|
||||
await signInWithGoogle();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSignInWithGithub = async () => {
|
||||
try {
|
||||
await signInWithGithub();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSignInWithTwitter = async () => {
|
||||
try {
|
||||
await signInWithTwitter();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const renderForm = () => (
|
||||
<Box sx={{ gap: 3, display: 'flex', flexDirection: 'column' }}>
|
||||
<Field.Text name="email" label="Email address" slotProps={{ inputLabel: { shrink: true } }} />
|
||||
|
||||
<Box sx={{ gap: 1.5, display: 'flex', flexDirection: 'column' }}>
|
||||
<Link
|
||||
component={RouterLink}
|
||||
href={paths.auth.firebase.resetPassword}
|
||||
variant="body2"
|
||||
color="inherit"
|
||||
sx={{ alignSelf: 'flex-end' }}
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
|
||||
<Field.Text
|
||||
name="password"
|
||||
label="Password"
|
||||
placeholder="6+ characters"
|
||||
type={showPassword.value ? 'text' : 'password'}
|
||||
slotProps={{
|
||||
inputLabel: { shrink: true },
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={showPassword.onToggle} edge="end">
|
||||
<Iconify
|
||||
icon={showPassword.value ? 'solar:eye-bold' : 'solar:eye-closed-bold'}
|
||||
/>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
color="inherit"
|
||||
size="large"
|
||||
type="submit"
|
||||
variant="contained"
|
||||
loading={isSubmitting}
|
||||
loadingIndicator="Sign in..."
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormHead
|
||||
title="Sign in to your account"
|
||||
description={
|
||||
<>
|
||||
{`Don’t have an account? `}
|
||||
<Link component={RouterLink} href={paths.auth.firebase.signUp} variant="subtitle2">
|
||||
Get started
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
sx={{ textAlign: { xs: 'center', md: 'left' } }}
|
||||
/>
|
||||
|
||||
{!!errorMessage && (
|
||||
<Alert severity="error" sx={{ mb: 3 }}>
|
||||
{errorMessage}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Form methods={methods} onSubmit={onSubmit}>
|
||||
{renderForm()}
|
||||
</Form>
|
||||
|
||||
<FormDivider />
|
||||
|
||||
<FormSocials
|
||||
signInWithGoogle={handleSignInWithGoogle}
|
||||
singInWithGithub={handleSignInWithGithub}
|
||||
signInWithTwitter={handleSignInWithTwitter}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,212 @@
|
||||
import { z as zod } from 'zod';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useBoolean } from 'minimal-shared/hooks';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Link from '@mui/material/Link';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Button from '@mui/material/Button';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
|
||||
import { paths } from 'src/routes/paths';
|
||||
import { useRouter } from 'src/routes/hooks';
|
||||
import { RouterLink } from 'src/routes/components';
|
||||
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import { Form, Field } from 'src/components/hook-form';
|
||||
|
||||
import { getErrorMessage } from '../../utils';
|
||||
import { FormHead } from '../../components/form-head';
|
||||
import { FormDivider } from '../../components/form-divider';
|
||||
import { FormSocials } from '../../components/form-socials';
|
||||
import { SignUpTerms } from '../../components/sign-up-terms';
|
||||
import {
|
||||
signUp,
|
||||
signInWithGithub,
|
||||
signInWithGoogle,
|
||||
signInWithTwitter,
|
||||
} from '../../context/firebase';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export type SignUpSchemaType = zod.infer<typeof SignUpSchema>;
|
||||
|
||||
export const SignUpSchema = zod.object({
|
||||
firstName: zod.string().min(1, { message: 'First name is required!' }),
|
||||
lastName: zod.string().min(1, { message: 'Last name is required!' }),
|
||||
email: zod
|
||||
.string()
|
||||
.min(1, { message: 'Email is required!' })
|
||||
.email({ message: 'Email must be a valid email address!' }),
|
||||
password: zod
|
||||
.string()
|
||||
.min(1, { message: 'Password is required!' })
|
||||
.min(6, { message: 'Password must be at least 6 characters!' }),
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function FirebaseSignUpView() {
|
||||
const router = useRouter();
|
||||
|
||||
const showPassword = useBoolean();
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const defaultValues: SignUpSchemaType = {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
password: '',
|
||||
};
|
||||
|
||||
const methods = useForm<SignUpSchemaType>({
|
||||
resolver: zodResolver(SignUpSchema),
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
} = methods;
|
||||
|
||||
const createRedirectPath = (query: string) => {
|
||||
const queryString = new URLSearchParams({ email: query }).toString();
|
||||
return `${paths.auth.firebase.verify}?${queryString}`;
|
||||
};
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
try {
|
||||
await signUp({
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
});
|
||||
|
||||
const redirectPath = createRedirectPath(data.email);
|
||||
|
||||
router.push(redirectPath);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const feedbackMessage = getErrorMessage(error);
|
||||
setErrorMessage(feedbackMessage);
|
||||
}
|
||||
});
|
||||
|
||||
const handleSignInWithGoogle = async () => {
|
||||
try {
|
||||
await signInWithGoogle();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSignInWithGithub = async () => {
|
||||
try {
|
||||
await signInWithGithub();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSignInWithTwitter = async () => {
|
||||
try {
|
||||
await signInWithTwitter();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const renderForm = () => (
|
||||
<Box sx={{ gap: 3, display: 'flex', flexDirection: 'column' }}>
|
||||
<Box
|
||||
sx={{ display: 'flex', gap: { xs: 3, sm: 2 }, flexDirection: { xs: 'column', sm: 'row' } }}
|
||||
>
|
||||
<Field.Text
|
||||
name="firstName"
|
||||
label="First name"
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
/>
|
||||
<Field.Text
|
||||
name="lastName"
|
||||
label="Last name"
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Field.Text name="email" label="Email address" slotProps={{ inputLabel: { shrink: true } }} />
|
||||
|
||||
<Field.Text
|
||||
name="password"
|
||||
label="Password"
|
||||
placeholder="6+ characters"
|
||||
type={showPassword.value ? 'text' : 'password'}
|
||||
slotProps={{
|
||||
inputLabel: { shrink: true },
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={showPassword.onToggle} edge="end">
|
||||
<Iconify icon={showPassword.value ? 'solar:eye-bold' : 'solar:eye-closed-bold'} />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
color="inherit"
|
||||
size="large"
|
||||
type="submit"
|
||||
variant="contained"
|
||||
loading={isSubmitting}
|
||||
loadingIndicator="Create account..."
|
||||
>
|
||||
Create account
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormHead
|
||||
title="Get started absolutely free"
|
||||
description={
|
||||
<>
|
||||
{`Already have an account? `}
|
||||
<Link component={RouterLink} href={paths.auth.firebase.signIn} variant="subtitle2">
|
||||
Get started
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
sx={{ textAlign: { xs: 'center', md: 'left' } }}
|
||||
/>
|
||||
|
||||
{!!errorMessage && (
|
||||
<Alert severity="error" sx={{ mb: 3 }}>
|
||||
{errorMessage}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Form methods={methods} onSubmit={onSubmit}>
|
||||
{renderForm()}
|
||||
</Form>
|
||||
|
||||
<SignUpTerms />
|
||||
|
||||
<FormDivider />
|
||||
|
||||
<FormSocials
|
||||
signInWithGoogle={handleSignInWithGoogle}
|
||||
singInWithGithub={handleSignInWithGithub}
|
||||
signInWithTwitter={handleSignInWithTwitter}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
import { paths } from 'src/routes/paths';
|
||||
|
||||
import { EmailInboxIcon } from 'src/assets/icons';
|
||||
|
||||
import { FormHead } from '../../components/form-head';
|
||||
import { FormReturnLink } from '../../components/form-return-link';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function FirebaseVerifyView() {
|
||||
return (
|
||||
<>
|
||||
<FormHead
|
||||
icon={<EmailInboxIcon />}
|
||||
title="Please check your email!"
|
||||
description={`We've emailed a 6-digit confirmation code. \nPlease enter the code in the box below to verify your email.`}
|
||||
/>
|
||||
|
||||
<FormReturnLink href={paths.auth.firebase.signIn} sx={{ mt: 0 }} />
|
||||
</>
|
||||
);
|
||||
}
|
7
03_source/frontend/src/auth/view/firebase/index.ts
Normal file
7
03_source/frontend/src/auth/view/firebase/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export * from './firebase-verify-view';
|
||||
|
||||
export * from './firebase-sign-in-view';
|
||||
|
||||
export * from './firebase-sign-up-view';
|
||||
|
||||
export * from './firebase-reset-password-view';
|
3
03_source/frontend/src/auth/view/jwt/index.ts
Normal file
3
03_source/frontend/src/auth/view/jwt/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './jwt-sign-in-view';
|
||||
|
||||
export * from './jwt-sign-up-view';
|
163
03_source/frontend/src/auth/view/jwt/jwt-sign-in-view.tsx
Normal file
163
03_source/frontend/src/auth/view/jwt/jwt-sign-in-view.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import { z as zod } from 'zod';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useBoolean } from 'minimal-shared/hooks';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Link from '@mui/material/Link';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Button from '@mui/material/Button';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
|
||||
import { paths } from 'src/routes/paths';
|
||||
import { useRouter } from 'src/routes/hooks';
|
||||
import { RouterLink } from 'src/routes/components';
|
||||
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import { Form, Field } from 'src/components/hook-form';
|
||||
|
||||
import { useAuthContext } from '../../hooks';
|
||||
import { getErrorMessage } from '../../utils';
|
||||
import { FormHead } from '../../components/form-head';
|
||||
import { signInWithPassword } from '../../context/jwt';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export type SignInSchemaType = zod.infer<typeof SignInSchema>;
|
||||
|
||||
export const SignInSchema = zod.object({
|
||||
email: zod
|
||||
.string()
|
||||
.min(1, { message: 'Email is required!' })
|
||||
.email({ message: 'Email must be a valid email address!' }),
|
||||
password: zod
|
||||
.string()
|
||||
.min(1, { message: 'Password is required!' })
|
||||
.min(6, { message: 'Password must be at least 6 characters!' }),
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function JwtSignInView() {
|
||||
const router = useRouter();
|
||||
|
||||
const showPassword = useBoolean();
|
||||
|
||||
const { checkUserSession } = useAuthContext();
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const defaultValues: SignInSchemaType = {
|
||||
email: 'demo@minimals.cc',
|
||||
password: '@2Minimal',
|
||||
};
|
||||
|
||||
const methods = useForm<SignInSchemaType>({
|
||||
resolver: zodResolver(SignInSchema),
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
} = methods;
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
try {
|
||||
await signInWithPassword({ email: data.email, password: data.password });
|
||||
await checkUserSession?.();
|
||||
|
||||
router.refresh();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const feedbackMessage = getErrorMessage(error);
|
||||
setErrorMessage(feedbackMessage);
|
||||
}
|
||||
});
|
||||
|
||||
const renderForm = () => (
|
||||
<Box sx={{ gap: 3, display: 'flex', flexDirection: 'column' }}>
|
||||
<Field.Text name="email" label="Email address" slotProps={{ inputLabel: { shrink: true } }} />
|
||||
|
||||
<Box sx={{ gap: 1.5, display: 'flex', flexDirection: 'column' }}>
|
||||
<Link
|
||||
component={RouterLink}
|
||||
href="#"
|
||||
variant="body2"
|
||||
color="inherit"
|
||||
sx={{ alignSelf: 'flex-end' }}
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
|
||||
<Field.Text
|
||||
name="password"
|
||||
label="Password"
|
||||
placeholder="6+ characters"
|
||||
type={showPassword.value ? 'text' : 'password'}
|
||||
slotProps={{
|
||||
inputLabel: { shrink: true },
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={showPassword.onToggle} edge="end">
|
||||
<Iconify
|
||||
icon={showPassword.value ? 'solar:eye-bold' : 'solar:eye-closed-bold'}
|
||||
/>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
color="inherit"
|
||||
size="large"
|
||||
type="submit"
|
||||
variant="contained"
|
||||
loading={isSubmitting}
|
||||
loadingIndicator="Sign in..."
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormHead
|
||||
title="Sign in to your account"
|
||||
description={
|
||||
<>
|
||||
{`Don’t have an account? `}
|
||||
<Link component={RouterLink} href={paths.auth.jwt.signUp} variant="subtitle2">
|
||||
Get started
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
sx={{ textAlign: { xs: 'center', md: 'left' } }}
|
||||
/>
|
||||
|
||||
<Alert severity="info" sx={{ mb: 3 }}>
|
||||
Use <strong>{defaultValues.email}</strong>
|
||||
{' with password '}
|
||||
<strong>{defaultValues.password}</strong>
|
||||
</Alert>
|
||||
|
||||
{!!errorMessage && (
|
||||
<Alert severity="error" sx={{ mb: 3 }}>
|
||||
{errorMessage}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Form methods={methods} onSubmit={onSubmit}>
|
||||
{renderForm()}
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
}
|
170
03_source/frontend/src/auth/view/jwt/jwt-sign-up-view.tsx
Normal file
170
03_source/frontend/src/auth/view/jwt/jwt-sign-up-view.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import { z as zod } from 'zod';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useBoolean } from 'minimal-shared/hooks';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Link from '@mui/material/Link';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Button from '@mui/material/Button';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
|
||||
import { paths } from 'src/routes/paths';
|
||||
import { useRouter } from 'src/routes/hooks';
|
||||
import { RouterLink } from 'src/routes/components';
|
||||
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import { Form, Field } from 'src/components/hook-form';
|
||||
|
||||
import { signUp } from '../../context/jwt';
|
||||
import { useAuthContext } from '../../hooks';
|
||||
import { getErrorMessage } from '../../utils';
|
||||
import { FormHead } from '../../components/form-head';
|
||||
import { SignUpTerms } from '../../components/sign-up-terms';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export type SignUpSchemaType = zod.infer<typeof SignUpSchema>;
|
||||
|
||||
export const SignUpSchema = zod.object({
|
||||
firstName: zod.string().min(1, { message: 'First name is required!' }),
|
||||
lastName: zod.string().min(1, { message: 'Last name is required!' }),
|
||||
email: zod
|
||||
.string()
|
||||
.min(1, { message: 'Email is required!' })
|
||||
.email({ message: 'Email must be a valid email address!' }),
|
||||
password: zod
|
||||
.string()
|
||||
.min(1, { message: 'Password is required!' })
|
||||
.min(6, { message: 'Password must be at least 6 characters!' }),
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function JwtSignUpView() {
|
||||
const router = useRouter();
|
||||
|
||||
const showPassword = useBoolean();
|
||||
|
||||
const { checkUserSession } = useAuthContext();
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const defaultValues: SignUpSchemaType = {
|
||||
firstName: 'Hello',
|
||||
lastName: 'Friend',
|
||||
email: 'hello@gmail.com',
|
||||
password: '@2Minimal',
|
||||
};
|
||||
|
||||
const methods = useForm<SignUpSchemaType>({
|
||||
resolver: zodResolver(SignUpSchema),
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
} = methods;
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
try {
|
||||
await signUp({
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
});
|
||||
await checkUserSession?.();
|
||||
|
||||
router.refresh();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const feedbackMessage = getErrorMessage(error);
|
||||
setErrorMessage(feedbackMessage);
|
||||
}
|
||||
});
|
||||
|
||||
const renderForm = () => (
|
||||
<Box sx={{ gap: 3, display: 'flex', flexDirection: 'column' }}>
|
||||
<Box
|
||||
sx={{ display: 'flex', gap: { xs: 3, sm: 2 }, flexDirection: { xs: 'column', sm: 'row' } }}
|
||||
>
|
||||
<Field.Text
|
||||
name="firstName"
|
||||
label="First name"
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
/>
|
||||
<Field.Text
|
||||
name="lastName"
|
||||
label="Last name"
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Field.Text name="email" label="Email address" slotProps={{ inputLabel: { shrink: true } }} />
|
||||
|
||||
<Field.Text
|
||||
name="password"
|
||||
label="Password"
|
||||
placeholder="6+ characters"
|
||||
type={showPassword.value ? 'text' : 'password'}
|
||||
slotProps={{
|
||||
inputLabel: { shrink: true },
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={showPassword.onToggle} edge="end">
|
||||
<Iconify icon={showPassword.value ? 'solar:eye-bold' : 'solar:eye-closed-bold'} />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
color="inherit"
|
||||
size="large"
|
||||
type="submit"
|
||||
variant="contained"
|
||||
loading={isSubmitting}
|
||||
loadingIndicator="Create account..."
|
||||
>
|
||||
Create account
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormHead
|
||||
title="Get started absolutely free"
|
||||
description={
|
||||
<>
|
||||
{`Already have an account? `}
|
||||
<Link component={RouterLink} href={paths.auth.jwt.signIn} variant="subtitle2">
|
||||
Get started
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
sx={{ textAlign: { xs: 'center', md: 'left' } }}
|
||||
/>
|
||||
|
||||
{!!errorMessage && (
|
||||
<Alert severity="error" sx={{ mb: 3 }}>
|
||||
{errorMessage}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Form methods={methods} onSubmit={onSubmit}>
|
||||
{renderForm()}
|
||||
</Form>
|
||||
|
||||
<SignUpTerms />
|
||||
</>
|
||||
);
|
||||
}
|
9
03_source/frontend/src/auth/view/supabase/index.ts
Normal file
9
03_source/frontend/src/auth/view/supabase/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export * from './supabase-verify-view';
|
||||
|
||||
export * from './supabase-sign-in-view';
|
||||
|
||||
export * from './supabase-sign-up-view';
|
||||
|
||||
export * from './supabase-reset-password-view';
|
||||
|
||||
export * from './supabase-update-password-view';
|
@@ -0,0 +1,97 @@
|
||||
import { z as zod } from 'zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
|
||||
import { paths } from 'src/routes/paths';
|
||||
import { useRouter } from 'src/routes/hooks';
|
||||
|
||||
import { PasswordIcon } from 'src/assets/icons';
|
||||
|
||||
import { Form, Field } from 'src/components/hook-form';
|
||||
|
||||
import { FormHead } from '../../components/form-head';
|
||||
import { resetPassword } from '../../context/supabase';
|
||||
import { FormReturnLink } from '../../components/form-return-link';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export type ResetPasswordSchemaType = zod.infer<typeof ResetPasswordSchema>;
|
||||
|
||||
export const ResetPasswordSchema = zod.object({
|
||||
email: zod
|
||||
.string()
|
||||
.min(1, { message: 'Email is required!' })
|
||||
.email({ message: 'Email must be a valid email address!' }),
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function SupabaseResetPasswordView() {
|
||||
const router = useRouter();
|
||||
|
||||
const defaultValues: ResetPasswordSchemaType = {
|
||||
email: '',
|
||||
};
|
||||
|
||||
const methods = useForm<ResetPasswordSchemaType>({
|
||||
resolver: zodResolver(ResetPasswordSchema),
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
} = methods;
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
try {
|
||||
await resetPassword({ email: data.email });
|
||||
|
||||
router.push(paths.auth.supabase.verify);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
const renderForm = () => (
|
||||
<Box sx={{ gap: 3, display: 'flex', flexDirection: 'column' }}>
|
||||
<Field.Text
|
||||
autoFocus
|
||||
name="email"
|
||||
label="Email address"
|
||||
placeholder="example@gmail.com"
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
/>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
size="large"
|
||||
type="submit"
|
||||
variant="contained"
|
||||
loading={isSubmitting}
|
||||
loadingIndicator="Send request..."
|
||||
>
|
||||
Send request
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormHead
|
||||
icon={<PasswordIcon />}
|
||||
title="Forgot your password?"
|
||||
description={`Please enter the email address associated with your account and we'll email you a link to reset your password.`}
|
||||
/>
|
||||
|
||||
<Form methods={methods} onSubmit={onSubmit}>
|
||||
{renderForm()}
|
||||
</Form>
|
||||
|
||||
<FormReturnLink href={paths.auth.supabase.signIn} />
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,157 @@
|
||||
import { z as zod } from 'zod';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useBoolean } from 'minimal-shared/hooks';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Link from '@mui/material/Link';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Button from '@mui/material/Button';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
|
||||
import { paths } from 'src/routes/paths';
|
||||
import { useRouter } from 'src/routes/hooks';
|
||||
import { RouterLink } from 'src/routes/components';
|
||||
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import { Form, Field } from 'src/components/hook-form';
|
||||
|
||||
import { useAuthContext } from '../../hooks';
|
||||
import { getErrorMessage } from '../../utils';
|
||||
import { FormHead } from '../../components/form-head';
|
||||
import { signInWithPassword } from '../../context/supabase';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export type SignInSchemaType = zod.infer<typeof SignInSchema>;
|
||||
|
||||
export const SignInSchema = zod.object({
|
||||
email: zod
|
||||
.string()
|
||||
.min(1, { message: 'Email is required!' })
|
||||
.email({ message: 'Email must be a valid email address!' }),
|
||||
password: zod
|
||||
.string()
|
||||
.min(1, { message: 'Password is required!' })
|
||||
.min(6, { message: 'Password must be at least 6 characters!' }),
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function SupabaseSignInView() {
|
||||
const router = useRouter();
|
||||
|
||||
const showPassword = useBoolean();
|
||||
|
||||
const { checkUserSession } = useAuthContext();
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const defaultValues: SignInSchemaType = {
|
||||
email: '',
|
||||
password: '',
|
||||
};
|
||||
|
||||
const methods = useForm<SignInSchemaType>({
|
||||
resolver: zodResolver(SignInSchema),
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
} = methods;
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
try {
|
||||
await signInWithPassword({ email: data.email, password: data.password });
|
||||
await checkUserSession?.();
|
||||
|
||||
router.refresh();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const feedbackMessage = getErrorMessage(error);
|
||||
setErrorMessage(feedbackMessage);
|
||||
}
|
||||
});
|
||||
|
||||
const renderForm = () => (
|
||||
<Box sx={{ gap: 3, display: 'flex', flexDirection: 'column' }}>
|
||||
<Field.Text name="email" label="Email address" slotProps={{ inputLabel: { shrink: true } }} />
|
||||
|
||||
<Box sx={{ gap: 1.5, display: 'flex', flexDirection: 'column' }}>
|
||||
<Link
|
||||
component={RouterLink}
|
||||
href={paths.auth.supabase.resetPassword}
|
||||
variant="body2"
|
||||
color="inherit"
|
||||
sx={{ alignSelf: 'flex-end' }}
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
|
||||
<Field.Text
|
||||
name="password"
|
||||
label="Password"
|
||||
placeholder="6+ characters"
|
||||
type={showPassword.value ? 'text' : 'password'}
|
||||
slotProps={{
|
||||
inputLabel: { shrink: true },
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={showPassword.onToggle} edge="end">
|
||||
<Iconify
|
||||
icon={showPassword.value ? 'solar:eye-bold' : 'solar:eye-closed-bold'}
|
||||
/>
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
color="inherit"
|
||||
size="large"
|
||||
type="submit"
|
||||
variant="contained"
|
||||
loading={isSubmitting}
|
||||
loadingIndicator="Sign in..."
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormHead
|
||||
title="Sign in to your account"
|
||||
description={
|
||||
<>
|
||||
{`Don’t have an account? `}
|
||||
<Link component={RouterLink} href={paths.auth.supabase.signUp} variant="subtitle2">
|
||||
Get started
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
sx={{ textAlign: { xs: 'center', md: 'left' } }}
|
||||
/>
|
||||
|
||||
{!!errorMessage && (
|
||||
<Alert severity="error" sx={{ mb: 3 }}>
|
||||
{errorMessage}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Form methods={methods} onSubmit={onSubmit}>
|
||||
{renderForm()}
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,166 @@
|
||||
import { z as zod } from 'zod';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useBoolean } from 'minimal-shared/hooks';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Link from '@mui/material/Link';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Button from '@mui/material/Button';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
|
||||
import { paths } from 'src/routes/paths';
|
||||
import { useRouter } from 'src/routes/hooks';
|
||||
import { RouterLink } from 'src/routes/components';
|
||||
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import { Form, Field } from 'src/components/hook-form';
|
||||
|
||||
import { getErrorMessage } from '../../utils';
|
||||
import { signUp } from '../../context/supabase';
|
||||
import { FormHead } from '../../components/form-head';
|
||||
import { SignUpTerms } from '../../components/sign-up-terms';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export type SignUpSchemaType = zod.infer<typeof SignUpSchema>;
|
||||
|
||||
export const SignUpSchema = zod.object({
|
||||
firstName: zod.string().min(1, { message: 'First name is required!' }),
|
||||
lastName: zod.string().min(1, { message: 'Last name is required!' }),
|
||||
email: zod
|
||||
.string()
|
||||
.min(1, { message: 'Email is required!' })
|
||||
.email({ message: 'Email must be a valid email address!' }),
|
||||
password: zod
|
||||
.string()
|
||||
.min(1, { message: 'Password is required!' })
|
||||
.min(6, { message: 'Password must be at least 6 characters!' }),
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function SupabaseSignUpView() {
|
||||
const router = useRouter();
|
||||
|
||||
const showPassword = useBoolean();
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const defaultValues: SignUpSchemaType = {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
password: '',
|
||||
};
|
||||
|
||||
const methods = useForm<SignUpSchemaType>({
|
||||
resolver: zodResolver(SignUpSchema),
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
} = methods;
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
try {
|
||||
await signUp({
|
||||
email: data.email,
|
||||
password: data.password,
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
});
|
||||
|
||||
router.push(paths.auth.supabase.verify);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const feedbackMessage = getErrorMessage(error);
|
||||
setErrorMessage(feedbackMessage);
|
||||
}
|
||||
});
|
||||
|
||||
const renderForm = () => (
|
||||
<Box sx={{ gap: 3, display: 'flex', flexDirection: 'column' }}>
|
||||
<Box
|
||||
sx={{ display: 'flex', gap: { xs: 3, sm: 2 }, flexDirection: { xs: 'column', sm: 'row' } }}
|
||||
>
|
||||
<Field.Text
|
||||
name="firstName"
|
||||
label="First name"
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
/>
|
||||
<Field.Text
|
||||
name="lastName"
|
||||
label="Last name"
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Field.Text name="email" label="Email address" slotProps={{ inputLabel: { shrink: true } }} />
|
||||
|
||||
<Field.Text
|
||||
name="password"
|
||||
label="Password"
|
||||
placeholder="6+ characters"
|
||||
type={showPassword.value ? 'text' : 'password'}
|
||||
slotProps={{
|
||||
inputLabel: { shrink: true },
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={showPassword.onToggle} edge="end">
|
||||
<Iconify icon={showPassword.value ? 'solar:eye-bold' : 'solar:eye-closed-bold'} />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
color="inherit"
|
||||
size="large"
|
||||
type="submit"
|
||||
variant="contained"
|
||||
loading={isSubmitting}
|
||||
loadingIndicator="Create account..."
|
||||
>
|
||||
Create account
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormHead
|
||||
title="Get started absolutely free"
|
||||
description={
|
||||
<>
|
||||
{`Already have an account? `}
|
||||
<Link component={RouterLink} href={paths.auth.supabase.signIn} variant="subtitle2">
|
||||
Get started
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
sx={{ textAlign: { xs: 'center', md: 'left' } }}
|
||||
/>
|
||||
|
||||
{!!errorMessage && (
|
||||
<Alert severity="error" sx={{ mb: 3 }}>
|
||||
{errorMessage}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Form methods={methods} onSubmit={onSubmit}>
|
||||
{renderForm()}
|
||||
</Form>
|
||||
|
||||
<SignUpTerms />
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,149 @@
|
||||
import { z as zod } from 'zod';
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useBoolean } from 'minimal-shared/hooks';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
|
||||
import Box from '@mui/material/Box';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Button from '@mui/material/Button';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
|
||||
import { paths } from 'src/routes/paths';
|
||||
import { useRouter } from 'src/routes/hooks';
|
||||
|
||||
import { NewPasswordIcon } from 'src/assets/icons';
|
||||
|
||||
import { Iconify } from 'src/components/iconify';
|
||||
import { Form, Field } from 'src/components/hook-form';
|
||||
|
||||
import { getErrorMessage } from '../../utils';
|
||||
import { FormHead } from '../../components/form-head';
|
||||
import { updatePassword } from '../../context/supabase';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export type UpdatePasswordSchemaType = zod.infer<typeof UpdatePasswordSchema>;
|
||||
|
||||
export const UpdatePasswordSchema = zod
|
||||
.object({
|
||||
password: zod
|
||||
.string()
|
||||
.min(1, { message: 'Password is required!' })
|
||||
.min(6, { message: 'Password must be at least 6 characters!' }),
|
||||
confirmPassword: zod.string().min(1, { message: 'Confirm password is required!' }),
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
message: 'Passwords do not match!',
|
||||
path: ['confirmPassword'],
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function SupabaseUpdatePasswordView() {
|
||||
const router = useRouter();
|
||||
|
||||
const showPassword = useBoolean();
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const defaultValues: UpdatePasswordSchemaType = {
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
};
|
||||
|
||||
const methods = useForm<UpdatePasswordSchemaType>({
|
||||
resolver: zodResolver(UpdatePasswordSchema),
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
} = methods;
|
||||
|
||||
const onSubmit = handleSubmit(async (data) => {
|
||||
try {
|
||||
await updatePassword({ password: data.password });
|
||||
|
||||
router.push(paths.dashboard.root);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const feedbackMessage = getErrorMessage(error);
|
||||
setErrorMessage(feedbackMessage);
|
||||
}
|
||||
});
|
||||
|
||||
const renderForm = () => (
|
||||
<Box sx={{ gap: 3, display: 'flex', flexDirection: 'column' }}>
|
||||
<Field.Text
|
||||
name="password"
|
||||
label="Password"
|
||||
placeholder="6+ characters"
|
||||
type={showPassword.value ? 'text' : 'password'}
|
||||
slotProps={{
|
||||
inputLabel: { shrink: true },
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={showPassword.onToggle} edge="end">
|
||||
<Iconify icon={showPassword.value ? 'solar:eye-bold' : 'solar:eye-closed-bold'} />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Field.Text
|
||||
name="confirmPassword"
|
||||
label="Confirm password"
|
||||
type={showPassword.value ? 'text' : 'password'}
|
||||
slotProps={{
|
||||
inputLabel: { shrink: true },
|
||||
input: {
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton onClick={showPassword.onToggle} edge="end">
|
||||
<Iconify icon={showPassword.value ? 'solar:eye-bold' : 'solar:eye-closed-bold'} />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
type="submit"
|
||||
size="large"
|
||||
variant="contained"
|
||||
loading={isSubmitting}
|
||||
loadingIndicator="Update password..."
|
||||
>
|
||||
Update password
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormHead
|
||||
icon={<NewPasswordIcon />}
|
||||
title="Update password"
|
||||
description="Successful updates enable access using the new password."
|
||||
/>
|
||||
|
||||
{!!errorMessage && (
|
||||
<Alert severity="error" sx={{ mb: 3 }}>
|
||||
{errorMessage}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Form methods={methods} onSubmit={onSubmit}>
|
||||
{renderForm()}
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
import { paths } from 'src/routes/paths';
|
||||
|
||||
import { EmailInboxIcon } from 'src/assets/icons';
|
||||
|
||||
import { FormHead } from '../../components/form-head';
|
||||
import { FormReturnLink } from '../../components/form-return-link';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export function SupabaseVerifyView() {
|
||||
return (
|
||||
<>
|
||||
<FormHead
|
||||
icon={<EmailInboxIcon />}
|
||||
title="Please check your email!"
|
||||
description={`We've emailed a 6-digit confirmation code. \nPlease enter the code in the box below to verify your email.`}
|
||||
/>
|
||||
|
||||
<FormReturnLink href={paths.auth.supabase.signIn} sx={{ mt: 0 }} />
|
||||
</>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user