client portal login api

This commit is contained in:
Muhammad Fajar
2022-11-11 08:35:43 +07:00
parent 174f065191
commit 3e30ce6e90
11 changed files with 226 additions and 155 deletions

View File

@@ -33,17 +33,13 @@ class AuthController extends Controller
} }
if (filter_var($request->phone_or_email, FILTER_VALIDATE_EMAIL)) { if (filter_var($request->phone_or_email, FILTER_VALIDATE_EMAIL)) {
$user = User::query()->update([ User::query()->find($user->id)->update([
'email' => $request->phone_or_email
], [
'email' => $request->phone_or_email, 'email' => $request->phone_or_email,
'otp' => rand(1000, 9999), 'otp' => rand(1000, 9999),
'otp_created_at' => now() 'otp_created_at' => now()
]); ]);
} else { } else {
$user = User::query()->update([ User::query()->find($user->id)->update([
'phone' => $request->phone_or_email
], [
'phone' => $request->phone_or_email, 'phone' => $request->phone_or_email,
'otp' => rand(1000, 9999), 'otp' => rand(1000, 9999),
'otp_created_at' => now() 'otp_created_at' => now()

View File

@@ -42,6 +42,7 @@
"react-hook-form": "^7.34.2", "react-hook-form": "^7.34.2",
"react-intersection-observer": "^8.34.0", "react-intersection-observer": "^8.34.0",
"react-lazy-load-image-component": "^1.5.5", "react-lazy-load-image-component": "^1.5.5",
"react-number-format": "^5.1.1",
"react-quill": "2.0.0-beta.4", "react-quill": "2.0.0-beta.4",
"react-router": "^6.3.0", "react-router": "^6.3.0",
"react-router-dom": "^6.3.0", "react-router-dom": "^6.3.0",
@@ -7668,6 +7669,18 @@
"react-dom": "^15.x.x || ^16.x.x || ^17.x.x || ^18.x.x" "react-dom": "^15.x.x || ^16.x.x || ^17.x.x || ^18.x.x"
} }
}, },
"node_modules/react-number-format": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.1.1.tgz",
"integrity": "sha512-wLRUP9G0B79hZrTRgj380/ygtKRJWrCnTBIlQkoYAp/AMNQK7qGkUAWM5EpS1YRxz1w003UmZoon8WRutnlFuw==",
"dependencies": {
"prop-types": "^15.7.2"
},
"peerDependencies": {
"react": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^18.0.0",
"react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^18.0.0"
}
},
"node_modules/react-quill": { "node_modules/react-quill": {
"version": "2.0.0-beta.4", "version": "2.0.0-beta.4",
"resolved": "https://registry.npmjs.org/react-quill/-/react-quill-2.0.0-beta.4.tgz", "resolved": "https://registry.npmjs.org/react-quill/-/react-quill-2.0.0-beta.4.tgz",
@@ -14632,6 +14645,14 @@
"lodash.throttle": "^4.1.1" "lodash.throttle": "^4.1.1"
} }
}, },
"react-number-format": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.1.1.tgz",
"integrity": "sha512-wLRUP9G0B79hZrTRgj380/ygtKRJWrCnTBIlQkoYAp/AMNQK7qGkUAWM5EpS1YRxz1w003UmZoon8WRutnlFuw==",
"requires": {
"prop-types": "^15.7.2"
}
},
"react-quill": { "react-quill": {
"version": "2.0.0-beta.4", "version": "2.0.0-beta.4",
"resolved": "https://registry.npmjs.org/react-quill/-/react-quill-2.0.0-beta.4.tgz", "resolved": "https://registry.npmjs.org/react-quill/-/react-quill-2.0.0-beta.4.tgz",

View File

@@ -71,6 +71,7 @@
"react-hook-form": "^7.34.2", "react-hook-form": "^7.34.2",
"react-intersection-observer": "^8.34.0", "react-intersection-observer": "^8.34.0",
"react-lazy-load-image-component": "^1.5.5", "react-lazy-load-image-component": "^1.5.5",
"react-number-format": "^5.1.1",
"react-quill": "2.0.0-beta.4", "react-quill": "2.0.0-beta.4",
"react-router": "^6.3.0", "react-router": "^6.3.0",
"react-router-dom": "^6.3.0", "react-router-dom": "^6.3.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

View File

@@ -26,8 +26,7 @@ export type JWTContextType = {
isInitialized: boolean; isInitialized: boolean;
user: AuthUser; user: AuthUser;
method: 'jwt'; method: 'jwt';
login: (email: string, password: string) => Promise<void>; // login: (phone_or_email: string) => Promise<void>;
register: (email: string, password: string, firstName: string, lastName: string) => Promise<void>;
logout: () => Promise<void>; logout: () => Promise<void>;
}; };

View File

@@ -80,15 +80,15 @@ type AuthProviderProps = {
function AuthProvider({ children }: AuthProviderProps) { function AuthProvider({ children }: AuthProviderProps) {
const [state, dispatch] = useReducer(JWTReducer, initialState); const [state, dispatch] = useReducer(JWTReducer, initialState);
let location = useLocation(); // let location = useLocation();
useEffect(() => { useEffect(() => {
const initialize = async () => { const initialize = async () => {
console.log('initialize', state) console.log('initialize', state);
try { try {
const accessToken = getSession(); const accessToken = getSession();
console.log('') console.log('');
if (accessToken) { if (accessToken) {
setSession(accessToken); setSession(accessToken);
@@ -125,56 +125,56 @@ function AuthProvider({ children }: AuthProviderProps) {
initialize(); initialize();
}, []); }, []);
const login = async (email: string, password: string) => { // const login = async (phone_or_email: string) => {
return axios // axios
.post('/login', { email, password }) // .post('/otp-request', { phone_or_email })
.then((response) => { // .then((response: any) => {
const { user, token } = response.data; // const { user, token } = response.data;
setSession(token); // setSession(token);
dispatch({ // dispatch({
type: Types.Login, // type: Types.Login,
payload: { // payload: {
user, // user,
} // },
}); // });
}) // })
.catch(error => { // .catch((error) => {
if (error.response.status !== 404) throw error.response // if (error.response.status !== 404) throw error.response;
if (error.response.status !== 422) throw error.response // if (error.response.status !== 422) throw error.response;
}) // });
}; // };
const register = async (email: string, password: string, firstName: string, lastName: string) => { // const register = async (email: string, password: string, firstName: string, lastName: string) => {
const response = await axios.post('/api/register', { // const response = await axios.post('/api/register', {
email, // email,
password, // password,
firstName, // firstName,
lastName, // lastName,
}); // });
const { accessToken, user } = response.data; // const { accessToken, user } = response.data;
window.localStorage.setItem('accessToken', accessToken); // window.localStorage.setItem('accessToken', accessToken);
dispatch({ // dispatch({
type: Types.Register, // type: Types.Register,
payload: { // payload: {
user, // user,
}, // },
}); // });
}; // };
const logout = async () => { const logout = async () => {
await axios.post('/logout', { token: getSession() });
setSession(null); setSession(null);
dispatch({ type: Types.Logout }); dispatch({ type: Types.Logout });
}; };
return (<AuthContext.Provider return (
<AuthContext.Provider
value={{ value={{
...state, ...state,
method: 'jwt', method: 'jwt',
login,
logout, logout,
register,
}} }}
> >
{children} {children}
@@ -182,9 +182,9 @@ function AuthProvider({ children }: AuthProviderProps) {
); );
// if (state.isInitialized) { // if (state.isInitialized) {
// return (!state.isAuthenticated && location.pathname !== '/auth/login') ? // return (!state.isAuthenticated && location.pathname !== '/auth/login') ?
// (<Navigate to="/auth/login" replace={true} />) // (<Navigate to="/auth/login" replace={true} />)
// : false && location.pathname == '/auth/login' ? // : false && location.pathname == '/auth/login' ?
// (<Navigate to="/dashboard" replace={true} />) // (<Navigate to="/dashboard" replace={true} />)
// : ( // : (
// <AuthContext.Provider // <AuthContext.Provider

View File

@@ -1,19 +1,17 @@
import { capitalCase } from 'change-case'; import { capitalCase } from 'change-case';
import { Link as RouterLink } from 'react-router-dom';
// @mui // @mui
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
import { Box, Card, Stack, Link, Alert, Tooltip, Container, Typography } from '@mui/material'; import { Box, Card, Divider, Grid, Link, Stack, Tooltip, Typography } from '@mui/material';
// routes
import { PATH_AUTH } from '../../routes/paths';
// hooks // hooks
import useAuth from '../../hooks/useAuth'; import useAuth from '../../hooks/useAuth';
import useResponsive from '../../hooks/useResponsive';
// components // components
import Page from '../../components/Page'; import Page from '../../components/Page';
import Logo from '../../components/Logo';
import Image from '../../components/Image'; import Image from '../../components/Image';
// sections // sections
import { LoginForm } from '../../sections/auth/login'; import { LoginEmailForm, LoginPhoneForm } from '../../sections/auth/login';
import Logo from '../../components/Logo';
// react
import { useState } from 'react';
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
@@ -21,117 +19,82 @@ const RootStyle = styled('div')(({ theme }) => ({
[theme.breakpoints.up('md')]: { [theme.breakpoints.up('md')]: {
display: 'flex', display: 'flex',
}, },
}));
const HeaderStyle = styled('header')(({ theme }) => ({
top: 0,
zIndex: 9,
lineHeight: 0,
width: '100%',
display: 'flex',
alignItems: 'center',
position: 'absolute',
padding: theme.spacing(3),
justifyContent: 'space-between',
[theme.breakpoints.up('md')]: {
alignItems: 'flex-start',
padding: theme.spacing(7, 5, 0, 7),
},
}));
const SectionStyle = styled(Card)(({ theme }) => ({
width: '100%',
maxWidth: 464,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
margin: theme.spacing(2, 0, 2, 2),
}));
const ContentStyle = styled('div')(({ theme }) => ({
maxWidth: 480,
margin: 'auto',
display: 'flex',
minHeight: '100vh', minHeight: '100vh',
flexDirection: 'column', flexDirection: 'column',
justifyContent: 'center', justifyContent: 'center',
padding: theme.spacing(12, 0), alignItems: 'center',
}));
const ContentStyle = styled(Card)(({ theme }) => ({
[theme.breakpoints.up('md')]: {
maxHeight: '600px',
maxWidth: '1000px',
},
})); }));
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
export default function Login() { export default function Login() {
const { method } = useAuth(); const { method } = useAuth();
const [formPhone, setFormPhone] = useState(false);
const smUp = useResponsive('up', 'sm'); const handlerChange = (event: any, setForm: boolean) => {
event.preventDefault();
const mdUp = useResponsive('up', 'md'); setFormPhone(setForm);
};
return ( return (
<Page title="Login"> <Page title="Login">
<RootStyle> <RootStyle>
<HeaderStyle> <ContentStyle>
<Logo sx={{ width: 150, height: 150 }} /> <Grid container>
{smUp && ( <Grid item xs={6}>
<Typography variant="body2" sx={{ mt: { md: -2 } }}> <Image visibleByDefault disabledEffect src="/images/login-image.gif" alt="login" />
Has problem with your account? {''} </Grid>
<Link variant="subtitle2" component={RouterLink} to="#" onClick={(e) => { <Grid item xs={6} sx={{ padding: 3 }}>
window.location.href = "mailto:admin@linksehat.com"; <Stack direction="row" alignItems="center" sx={{ mb: 5 }}>
e.preventDefault(); <Tooltip title={capitalCase(method)} placement="left">
}}> <Logo sx={{ width: 90, height: 90 }} />
Contact Us </Tooltip>
</Link>
</Typography>
)}
</HeaderStyle>
{/* {mdUp && ( <Box sx={{ flexGrow: 1 }}>
<SectionStyle> <Typography variant="h4" gutterBottom>
<Typography variant="h3" sx={{ px: 5, mt: 10, mb: 5 }}> Sign in to LinkSehat
Hi, Welcome Back </Typography>
</Typography> <Typography variant="body1" sx={{ color: 'text.secondary' }}>
<Image Enter your details below.
visibleByDefault </Typography>
disabledEffect </Box>
src="https://minimal-assets-api.vercel.app/assets/illustrations/illustration_login.png" </Stack>
alt="login"
/>
</SectionStyle>
)} */}
<Container maxWidth="sm"> {formPhone === false ? <LoginEmailForm /> : <LoginPhoneForm />}
<ContentStyle>
<Stack direction="row" alignItems="center" sx={{ mb: 5 }}>
<Box sx={{ flexGrow: 1 }}>
<Typography variant="h4" gutterBottom>
Sign in to LinkSehat
</Typography>
<Typography sx={{ color: 'text.secondary' }}>Enter your details below.</Typography>
</Box>
<Tooltip title={capitalCase(method)} placement="right"> <Divider sx={{ marginTop: 5 }}>Atau</Divider>
<>
<Image
disabledEffect
src={`https://minimal-assets-api.vercel.app/assets/icons/auth/ic_${method}.png`}
sx={{ width: 32, height: 32 }}
/>
</>
</Tooltip>
</Stack>
<LoginForm /> <Stack sx={{ marginTop: 5 }}>
{formPhone === false ? (
{false && !smUp && ( <Link
<Typography variant="body2" align="center" sx={{ mt: 3 }}> href=""
Dont have an account?{' '} align="center"
<Link variant="subtitle2" component={RouterLink} to={PATH_AUTH.register}> underline="hover"
Get started onClick={(event) => handlerChange(event, true)}
</Link> >
</Typography> Masuk menggunakan nomor handphone
)} </Link>
</ContentStyle> ) : (
</Container> <Link
href=""
align="center"
underline="hover"
onClick={(event) => handlerChange(event, false)}
>
Masuk menggunakan email
</Link>
)}
</Stack>
</Grid>
</Grid>
</ContentStyle>
</RootStyle> </RootStyle>
</Page> </Page>
); );

View File

@@ -26,7 +26,6 @@ export default function LoginForm() {
const LoginSchema = Yup.object().shape({ const LoginSchema = Yup.object().shape({
email: Yup.string().email('Email must be a valid email address').required('Email is required'), email: Yup.string().email('Email must be a valid email address').required('Email is required'),
password: Yup.string().required('Password is required'),
}); });
const methods = useForm<FormValuesProps>({ const methods = useForm<FormValuesProps>({
@@ -71,6 +70,7 @@ export default function LoginForm() {
type="submit" type="submit"
variant="contained" variant="contained"
loading={isSubmitting} loading={isSubmitting}
sx={{ marginTop: 2 }}
> >
Login Login
</LoadingButton> </LoadingButton>

View File

@@ -0,0 +1,83 @@
import * as Yup from 'yup';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import axios from '../../../utils/axios';
// form
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
// @mui
import { Stack, Alert, InputAdornment } from '@mui/material';
import { LoadingButton } from '@mui/lab';
// components
import { FormProvider, RHFTextField } from '../../../components/hook-form';
// ----------------------------------------------------------------------
type FormValuesProps = {
phone: string;
afterSubmit?: string;
};
export default function LoginPhoneForm() {
const navigate = useNavigate();
const LoginSchema = Yup.object().shape({
phone: Yup.string().required('Phone is required'),
});
const defaultValues = {
phone: '',
};
const methods = useForm<FormValuesProps>({
resolver: yupResolver(LoginSchema),
defaultValues,
});
const {
reset,
setError,
handleSubmit,
formState: { errors, isSubmitting },
} = methods;
const onSubmit = async (data: FormValuesProps) => {
try {
await axios.post('/otp-request', { phone_or_email: 0 + data.phone });
// console.log(response);
// navigate('/dashboard');
} catch (error: any) {
reset();
setError('afterSubmit', { ...error, message: error.response.data.message });
}
};
return (
<FormProvider methods={methods} onSubmit={handleSubmit(onSubmit)}>
<Stack spacing={3}>
<Alert severity="info">Masukkan akun yang telah terdaftar</Alert>
{!!errors.afterSubmit && <Alert severity="error">{errors.afterSubmit.message}</Alert>}
<RHFTextField
name="phone"
label="Phone Number"
type={'number'}
InputProps={{
startAdornment: <InputAdornment position="start">+62</InputAdornment>,
}}
/>
</Stack>
<LoadingButton
fullWidth
size="large"
type="submit"
variant="contained"
loading={isSubmitting}
sx={{ marginTop: 2 }}
>
Login
</LoadingButton>
</FormProvider>
);
}

View File

@@ -1 +1,2 @@
export { default as LoginForm } from './LoginForm'; export { default as LoginEmailForm } from './LoginEmailForm';
export { default as LoginPhoneForm } from './LoginPhoneForm';

View File

@@ -4205,7 +4205,7 @@
dependencies: dependencies:
"prop-types" "^15.5.7" "prop-types" "^15.5.7"
"react-dom@^0.14.9 || ^15.3.0 || ^16.0.0-rc || ^16.0 || ^17.0 || ^18.0.0", "react-dom@^15.x.x || ^16.x.x || ^17.x.x || ^18.x.x", "react-dom@^16 || ^17", "react-dom@^16.6.0 || ^17.0.0 || ^18.0.0", "react-dom@^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom@^17.0.0 || ^18.0.0", "react-dom@^17.0.2", "react-dom@^17.0.2 || ^18.0.0", "react-dom@>=16.6.0", "react-dom@>=16.8", "react-dom@>=16.8 || ^17.0.0 || ^18.0.0": "react-dom@^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^18.0.0", "react-dom@^0.14.9 || ^15.3.0 || ^16.0.0-rc || ^16.0 || ^17.0 || ^18.0.0", "react-dom@^15.x.x || ^16.x.x || ^17.x.x || ^18.x.x", "react-dom@^16 || ^17", "react-dom@^16.6.0 || ^17.0.0 || ^18.0.0", "react-dom@^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom@^17.0.0 || ^18.0.0", "react-dom@^17.0.2", "react-dom@^17.0.2 || ^18.0.0", "react-dom@>=16.6.0", "react-dom@>=16.8", "react-dom@>=16.8 || ^17.0.0 || ^18.0.0":
"integrity" "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==" "integrity" "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA=="
"resolved" "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz" "resolved" "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz"
"version" "17.0.2" "version" "17.0.2"
@@ -4277,6 +4277,13 @@
"lodash.debounce" "^4.0.8" "lodash.debounce" "^4.0.8"
"lodash.throttle" "^4.1.1" "lodash.throttle" "^4.1.1"
"react-number-format@^5.1.1":
"integrity" "sha512-wLRUP9G0B79hZrTRgj380/ygtKRJWrCnTBIlQkoYAp/AMNQK7qGkUAWM5EpS1YRxz1w003UmZoon8WRutnlFuw=="
"resolved" "https://registry.npmjs.org/react-number-format/-/react-number-format-5.1.1.tgz"
"version" "5.1.1"
dependencies:
"prop-types" "^15.7.2"
"react-quill@2.0.0-beta.4": "react-quill@2.0.0-beta.4":
"integrity" "sha512-KyAHvAlPjP4xLElKZJefMth91Z6FbbXRvq9OSu6xN3KBaoasLP9p+3dcxg4Ywr4tBlpMGXcPszYSAgd5CpJ45Q==" "integrity" "sha512-KyAHvAlPjP4xLElKZJefMth91Z6FbbXRvq9OSu6xN3KBaoasLP9p+3dcxg4Ywr4tBlpMGXcPszYSAgd5CpJ45Q=="
"resolved" "https://registry.npmjs.org/react-quill/-/react-quill-2.0.0-beta.4.tgz" "resolved" "https://registry.npmjs.org/react-quill/-/react-quill-2.0.0-beta.4.tgz"
@@ -4316,7 +4323,7 @@
"loose-envify" "^1.4.0" "loose-envify" "^1.4.0"
"prop-types" "^15.6.2" "prop-types" "^15.6.2"
"react@^0.14.9 || ^15.3.0 || ^16.0.0-rc || ^16.0 || ^17.0 || ^18.0.0", "react@^15.0.0 || ^16.0.0 || ^17.0.0|| ^18.0.0", "react@^15.x.x || ^16.x.x || ^17.x.x || ^18.x.x", "react@^16 || ^17", "react@^16.6.0 || ^17.0.0 || ^18.0.0", "react@^16.8.0 || ^17 || ^18", "react@^16.8.0 || ^17.0.0 || ^18.0.0", "react@^17.0.0 || ^18.0.0", "react@^17.0.2", "react@^17.0.2 || ^18.0.0", "react@>= 16.8 || 18.0.0", "react@>=0.13", "react@>=16", "react@>=16.6.0", "react@>=16.8", "react@>=16.8 || ^17.0.0 || ^18.0.0", "react@>=16.8.0", "react@17.0.2": "react@^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^18.0.0", "react@^0.14.9 || ^15.3.0 || ^16.0.0-rc || ^16.0 || ^17.0 || ^18.0.0", "react@^15.0.0 || ^16.0.0 || ^17.0.0|| ^18.0.0", "react@^15.x.x || ^16.x.x || ^17.x.x || ^18.x.x", "react@^16 || ^17", "react@^16.6.0 || ^17.0.0 || ^18.0.0", "react@^16.8.0 || ^17 || ^18", "react@^16.8.0 || ^17.0.0 || ^18.0.0", "react@^17.0.0 || ^18.0.0", "react@^17.0.2", "react@^17.0.2 || ^18.0.0", "react@>= 16.8 || 18.0.0", "react@>=0.13", "react@>=16", "react@>=16.6.0", "react@>=16.8", "react@>=16.8 || ^17.0.0 || ^18.0.0", "react@>=16.8.0", "react@17.0.2":
"integrity" "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==" "integrity" "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA=="
"resolved" "https://registry.npmjs.org/react/-/react-17.0.2.tgz" "resolved" "https://registry.npmjs.org/react/-/react-17.0.2.tgz"
"version" "17.0.2" "version" "17.0.2"