revamp template
This commit is contained in:
31
src/auth/AuthPage.tsx
Normal file
31
src/auth/AuthPage.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { Navigate, Route, Routes } from 'react-router';
|
||||
import {
|
||||
Login,
|
||||
ResetPassword,
|
||||
ResetPasswordChange,
|
||||
ResetPasswordChanged,
|
||||
ResetPasswordCheckEmail,
|
||||
ResetPasswordEnterEmail,
|
||||
TwoFactorAuth
|
||||
} from './pages/jwt';
|
||||
import { AuthBrandedLayout } from '@/layouts/auth-branded';
|
||||
import { CheckEmail } from '@/auth/pages/jwt';
|
||||
|
||||
const AuthPage = () => (
|
||||
<Routes>
|
||||
<Route element={<AuthBrandedLayout />}>
|
||||
<Route index element={<Login />} />
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/2fa" element={<TwoFactorAuth />} />
|
||||
<Route path="/check-email" element={<CheckEmail />} />
|
||||
<Route path="/reset-password" element={<ResetPassword />} />
|
||||
<Route path="/reset-password/enter-email" element={<ResetPasswordEnterEmail />} />
|
||||
<Route path="/reset-password/check-email" element={<ResetPasswordCheckEmail />} />
|
||||
<Route path="/reset-password/:id" element={<ResetPasswordChange />} />
|
||||
<Route path="/reset-password/changed" element={<ResetPasswordChanged />} />
|
||||
<Route path="*" element={<Navigate to="/error/404" />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
);
|
||||
|
||||
export { AuthPage };
|
||||
18
src/auth/RequireAuth.tsx
Normal file
18
src/auth/RequireAuth.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import { Navigate, Outlet, useLocation } from 'react-router-dom';
|
||||
|
||||
import { ScreenLoader } from '@/components/loaders';
|
||||
|
||||
import { useAuthContext } from './useAuthContext';
|
||||
|
||||
const RequireAuth = () => {
|
||||
const { auth, loading } = useAuthContext();
|
||||
const location = useLocation();
|
||||
|
||||
if (loading) {
|
||||
return <ScreenLoader />;
|
||||
}
|
||||
|
||||
return auth ? <Outlet /> : <Navigate to="/auth/login" state={{ from: location }} replace />;
|
||||
};
|
||||
|
||||
export { RequireAuth };
|
||||
68
src/auth/_helpers.ts
Normal file
68
src/auth/_helpers.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { User as Auth0UserModel } from '@auth0/auth0-spa-js';
|
||||
|
||||
import { getData, setData } from '@/utils';
|
||||
import { type AuthModel } from './_models';
|
||||
|
||||
const AUTH_LOCAL_STORAGE_KEY = `${import.meta.env.VITE_APP_NAME}-auth-v${
|
||||
import.meta.env.VITE_APP_VERSION
|
||||
}`;
|
||||
|
||||
const getAuth = (): AuthModel | undefined => {
|
||||
try {
|
||||
const auth = getData(AUTH_LOCAL_STORAGE_KEY) as AuthModel | undefined;
|
||||
|
||||
if (auth) {
|
||||
return auth;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('AUTH LOCAL STORAGE PARSE ERROR', error);
|
||||
}
|
||||
};
|
||||
|
||||
const setAuth = (auth: AuthModel | Auth0UserModel) => {
|
||||
setData(AUTH_LOCAL_STORAGE_KEY, auth);
|
||||
};
|
||||
|
||||
const removeAuth = () => {
|
||||
if (!localStorage) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
localStorage.removeItem(AUTH_LOCAL_STORAGE_KEY);
|
||||
} catch (error) {
|
||||
console.error('AUTH LOCAL STORAGE REMOVE ERROR', error);
|
||||
}
|
||||
};
|
||||
|
||||
export function setupAxios(axios: any) {
|
||||
axios.defaults.headers.Accept = 'application/json';
|
||||
axios.interceptors.request.use(
|
||||
(config: { headers: { Authorization: string }; params?: any; url?: any }) => {
|
||||
const auth = getAuth();
|
||||
|
||||
if (auth?.access_token) {
|
||||
config.headers.Authorization = `Bearer ${auth.access_token}`;
|
||||
|
||||
if (
|
||||
config.url &&
|
||||
!config.url.includes('api/b/') &&
|
||||
!config.url.includes('api/bg/') &&
|
||||
!config.url.includes('api/c/')
|
||||
) {
|
||||
config.params = {
|
||||
...config.params,
|
||||
token: auth.access_token || ''
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
async (err: any) => await Promise.reject(err)
|
||||
);
|
||||
}
|
||||
|
||||
export { AUTH_LOCAL_STORAGE_KEY, getAuth, removeAuth, setAuth };
|
||||
34
src/auth/_models.ts
Normal file
34
src/auth/_models.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { type TLanguageCode } from '@/i18n';
|
||||
|
||||
export interface AuthModel {
|
||||
id: string;
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
role_name: string;
|
||||
roles_list: [];
|
||||
company: any;
|
||||
user: any;
|
||||
// api_token: string;
|
||||
}
|
||||
|
||||
export interface UserModel {
|
||||
id: number;
|
||||
username: string;
|
||||
name: string;
|
||||
password: string | undefined;
|
||||
email: string;
|
||||
id_role: string;
|
||||
role_name: string;
|
||||
roles_list: [];
|
||||
token: AuthModel;
|
||||
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
fullname?: string;
|
||||
occupation?: string;
|
||||
companyName?: string;
|
||||
phone?: string;
|
||||
roles?: number[];
|
||||
pic?: string;
|
||||
language?: TLanguageCode;
|
||||
}
|
||||
5
src/auth/index.ts
Normal file
5
src/auth/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from './_helpers';
|
||||
export * from './_models';
|
||||
export * from './AuthPage';
|
||||
export * from './RequireAuth';
|
||||
export * from './useAuthContext';
|
||||
49
src/auth/pages/jwt/CheckEmail.tsx
Normal file
49
src/auth/pages/jwt/CheckEmail.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { toAbsoluteUrl } from '@/utils';
|
||||
|
||||
const CheckEmail = () => {
|
||||
return (
|
||||
<div className="card max-w-[440px] w-full">
|
||||
<div className="card-body p-10">
|
||||
<div className="flex justify-center py-10">
|
||||
<img
|
||||
src={toAbsoluteUrl('/media/illustrations/30.svg')}
|
||||
className="dark:hidden max-h-[130px]"
|
||||
alt=""
|
||||
/>
|
||||
<img
|
||||
src={toAbsoluteUrl('/media/illustrations/30-dark.svg')}
|
||||
className="light:hidden max-h-[130px]"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-medium text-gray-900 text-center mb-3">Check your email</h3>
|
||||
<div className="text-2sm text-center text-gray-700 mb-7.5">
|
||||
Please click the link sent to your email
|
||||
<a href="#" className="text-2sm text-gray-900 font-medium hover:text-primary-active">
|
||||
bob@keenthemes.com
|
||||
</a>
|
||||
<br />
|
||||
to verify your account. Thank you
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center mb-5">
|
||||
<Link to="/" className="btn btn-primary flex justify-center">
|
||||
Back to Home
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<span className="text-xs text-gray-700">Didn’t receive an email?</span>
|
||||
<Link to="/auth/classic/login" className="text-xs font-medium link">
|
||||
Resend
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { CheckEmail };
|
||||
189
src/auth/pages/jwt/Login.tsx
Normal file
189
src/auth/pages/jwt/Login.tsx
Normal file
@ -0,0 +1,189 @@
|
||||
import { type MouseEvent, useState } from 'react';
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
import clsx from 'clsx';
|
||||
import * as Yup from 'yup';
|
||||
import { useFormik } from 'formik';
|
||||
import { KeenIcon } from '@/components';
|
||||
import { toAbsoluteUrl } from '@/utils';
|
||||
import { useAuthContext } from '@/auth';
|
||||
import { useLayout } from '@/providers';
|
||||
import { Alert } from '@/components';
|
||||
import moment from 'moment';
|
||||
|
||||
const loginSchema = Yup.object().shape({
|
||||
username: Yup.string().required('Username is required'),
|
||||
password: Yup.string()
|
||||
.min(3, 'Minimum 3 symbols')
|
||||
.max(50, 'Maximum 50 symbols')
|
||||
.required('Password is required'),
|
||||
remember: Yup.boolean()
|
||||
});
|
||||
|
||||
const initialValues = {
|
||||
username: '',
|
||||
password: '',
|
||||
application: 'credit',
|
||||
remember: false
|
||||
};
|
||||
|
||||
const Login = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { login } = useAuthContext();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const from = location.state?.from?.pathname || '/';
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const { currentLayout } = useLayout();
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues,
|
||||
validationSchema: loginSchema,
|
||||
onSubmit: async (values, { setStatus, setSubmitting }) => {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
if (!login) {
|
||||
throw new Error('JWTProvider is required for this form.');
|
||||
}
|
||||
|
||||
await login(values.username, values.password, values.application);
|
||||
console.log(login);
|
||||
|
||||
if (values.remember) {
|
||||
localStorage.setItem('username', values.username);
|
||||
} else {
|
||||
localStorage.removeItem('username');
|
||||
}
|
||||
|
||||
navigate(from, { replace: true });
|
||||
} catch (error: any) {
|
||||
if (error.response && error.response.data) {
|
||||
setStatus(error.response.data.message);
|
||||
} else {
|
||||
setStatus('The login details are incorrect');
|
||||
}
|
||||
|
||||
setSubmitting(false);
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
const togglePassword = (event: MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
setShowPassword(!showPassword);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card max-w-[390px] border-0 shadow-none w-full">
|
||||
<form className="card-body flex flex-col gap-5 p-5" onSubmit={formik.handleSubmit} noValidate>
|
||||
<div className="flex align-center justify-center text-center mb-2.5">
|
||||
<img
|
||||
src={toAbsoluteUrl('/media/app/bri_tl_logo.png')}
|
||||
className="h-[50px] max-w-none mb-2 text-center"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex gap-3 items-center">
|
||||
<div>
|
||||
<img
|
||||
src={toAbsoluteUrl('/media/app/brilian_apps_logo.png')}
|
||||
className="h-[40px] max-w-none"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<h3 className="font-semibold text-gray-700" style={{ fontSize: '18px' }}>
|
||||
/ Pengajuan Kredit
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
{formik.status && <Alert variant="danger">{formik.status}</Alert>}
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="form-label text-gray-900 ps-2.5">Login</label>
|
||||
<label className="input">
|
||||
<input
|
||||
placeholder="Enter username"
|
||||
autoComplete="off"
|
||||
{...formik.getFieldProps('username')}
|
||||
className={clsx('form-control', {
|
||||
'is-invalid': formik.touched.username && formik.errors.username
|
||||
})}
|
||||
/>
|
||||
</label>
|
||||
{formik.touched.username && formik.errors.username && (
|
||||
<span role="alert" className="text-danger text-xs mt-1">
|
||||
{formik.errors.username}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<label className="form-label text-gray-900 ps-2.5">Password</label>
|
||||
</div>
|
||||
<label className="input">
|
||||
<input
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
placeholder="Enter Password"
|
||||
autoComplete="off"
|
||||
{...formik.getFieldProps('password')}
|
||||
className={clsx('form-control', {
|
||||
'is-invalid': formik.touched.password && formik.errors.password
|
||||
})}
|
||||
/>
|
||||
<button className="btn btn-icon" onClick={togglePassword}>
|
||||
<KeenIcon icon="eye" className={clsx('text-gray-500', { hidden: showPassword })} />
|
||||
<KeenIcon
|
||||
icon="eye-slash"
|
||||
className={clsx('text-gray-500', { hidden: !showPassword })}
|
||||
/>
|
||||
</button>
|
||||
</label>
|
||||
{formik.touched.password && formik.errors.password && (
|
||||
<span role="alert" className="text-danger text-xs mt-1">
|
||||
{formik.errors.password}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<label className="checkbox-group">
|
||||
<input
|
||||
className="checkbox checkbox-sm"
|
||||
type="checkbox"
|
||||
{...formik.getFieldProps('remember')}
|
||||
/>
|
||||
<span className="checkbox-label">Remember me</span>
|
||||
</label>
|
||||
<Link
|
||||
to={
|
||||
currentLayout?.name === 'auth-branded'
|
||||
? '/auth/reset-password'
|
||||
: '/auth/classic/reset-password'
|
||||
}
|
||||
className="text-2sm link shrink-0"
|
||||
>
|
||||
Forgot Password?
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary flex justify-center grow"
|
||||
disabled={loading || formik.isSubmitting}
|
||||
>
|
||||
{loading ? 'Please wait...' : 'Sign In'}
|
||||
</button>
|
||||
<div>
|
||||
<p className="text-2sm" style={{ fontSize: '12px', letterSpacing: 0.25 }}>
|
||||
Copyright {moment().year()} © Bank Rakyat Indonesia Timor Leste.
All rights
|
||||
reserved.
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { Login };
|
||||
76
src/auth/pages/jwt/TwoFactorAuth.tsx
Normal file
76
src/auth/pages/jwt/TwoFactorAuth.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { toAbsoluteUrl } from '@/utils';
|
||||
import { KeenIcon } from '@/components';
|
||||
|
||||
const TwoFactorAuth = () => {
|
||||
const [codeInputs, setCodeInputs] = useState(Array(6).fill(''));
|
||||
|
||||
const handleInputChange = (index: number, value: string) => {
|
||||
if (value.length > 1) return;
|
||||
const updatedInputs = [...codeInputs];
|
||||
updatedInputs[index] = value;
|
||||
setCodeInputs(updatedInputs);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="card max-w-[380px] w-full">
|
||||
<form className="card-body flex flex-col gap-5 p-10">
|
||||
<img
|
||||
src={toAbsoluteUrl('/media/illustrations/34.svg')}
|
||||
className="dark:hidden h-20 mb-2"
|
||||
alt=""
|
||||
/>
|
||||
<img
|
||||
src={toAbsoluteUrl('/media/illustrations/34-dark.svg')}
|
||||
className="light:hidden h-20 mb-2"
|
||||
alt=""
|
||||
/>
|
||||
|
||||
<div className="text-center mb-2">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-5">Verify your phone</h3>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-2sm text-gray-700 mb-1.5">
|
||||
Enter the verification code we sent to
|
||||
</span>
|
||||
<a href="#" className="text-sm font-medium text-gray-900">
|
||||
****** 7859
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap justify-center gap-2.5">
|
||||
{codeInputs.map((value, index) => (
|
||||
<input
|
||||
key={index}
|
||||
type="text"
|
||||
maxLength={1}
|
||||
className="input focus:border-primary-clarity focus:ring focus:ring-primary-clarity size-10 shrink-0 px-0 text-center"
|
||||
value={value}
|
||||
onChange={(e) => handleInputChange(index, e.target.value)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center mb-2">
|
||||
<span className="text-xs text-gray-700 me-1.5">Didn’t receive a code? (37s)</span>
|
||||
<Link to="/auth/classic/login" className="text-xs link">
|
||||
Resend
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<button className="btn btn-primary flex justify-center grow">Continue</button>
|
||||
|
||||
<Link
|
||||
to="/auth/login"
|
||||
className="flex items-center justify-center text-sm gap-2 text-gray-700 hover:text-primary"
|
||||
>
|
||||
<KeenIcon icon="black-left" />
|
||||
Back to Login
|
||||
</Link>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { TwoFactorAuth };
|
||||
4
src/auth/pages/jwt/index.ts
Normal file
4
src/auth/pages/jwt/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from './reset-password';
|
||||
export * from './Login';
|
||||
export * from './CheckEmail';
|
||||
export * from './TwoFactorAuth';
|
||||
134
src/auth/pages/jwt/reset-password/ResetPassword.tsx
Normal file
134
src/auth/pages/jwt/reset-password/ResetPassword.tsx
Normal file
@ -0,0 +1,134 @@
|
||||
import clsx from 'clsx';
|
||||
import { useFormik } from 'formik';
|
||||
import { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import * as Yup from 'yup';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { useAuthContext } from '@/auth/useAuthContext';
|
||||
import { Alert, KeenIcon } from '@/components';
|
||||
import { useLayout } from '@/providers';
|
||||
import { AxiosError } from 'axios';
|
||||
|
||||
const initialValues = {
|
||||
email: ''
|
||||
};
|
||||
|
||||
const forgotPasswordSchema = Yup.object().shape({
|
||||
email: Yup.string()
|
||||
.email('Wrong email format')
|
||||
.min(3, 'Minimum 3 symbols')
|
||||
.max(50, 'Maximum 50 symbols')
|
||||
.required('Email is required')
|
||||
});
|
||||
|
||||
const ResetPassword = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [hasErrors, setHasErrors] = useState<boolean | undefined>(undefined);
|
||||
const { requestPasswordResetLink } = useAuthContext();
|
||||
const { currentLayout } = useLayout();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues,
|
||||
validationSchema: forgotPasswordSchema,
|
||||
onSubmit: async (values, { setStatus, setSubmitting }) => {
|
||||
setLoading(true);
|
||||
setHasErrors(undefined);
|
||||
try {
|
||||
if (!requestPasswordResetLink) {
|
||||
throw new Error('JWTProvider is required for this form.');
|
||||
}
|
||||
await requestPasswordResetLink(values.email);
|
||||
setHasErrors(false);
|
||||
setLoading(false);
|
||||
const params = new URLSearchParams();
|
||||
params.append('email', values.email);
|
||||
navigate({
|
||||
pathname:
|
||||
currentLayout?.name === 'auth-branded'
|
||||
? '/auth/reset-password/check-email'
|
||||
: '/auth/classic/reset-password/check-email',
|
||||
search: params.toString()
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError && error.response) {
|
||||
setStatus(error.response.data.message);
|
||||
} else {
|
||||
setStatus('Password reset failed. Please try again.');
|
||||
}
|
||||
setHasErrors(true);
|
||||
setLoading(false);
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
return (
|
||||
<div className="card max-w-[370px] w-full">
|
||||
<form
|
||||
className="card-body flex flex-col gap-5 p-10"
|
||||
noValidate
|
||||
onSubmit={formik.handleSubmit}
|
||||
>
|
||||
<div className="text-center">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Your Email</h3>
|
||||
<span className="text-2sm text-gray-600 font-medium">
|
||||
Enter your email to reset password
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{hasErrors && <Alert variant="danger">{formik.status}</Alert>}
|
||||
|
||||
{hasErrors === false && (
|
||||
<Alert variant="success">
|
||||
Password reset link sent. Please check your email to proceed
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="form-label text-gray-900">Email</label>
|
||||
<label className="input">
|
||||
<input
|
||||
type="email"
|
||||
placeholder="email@email.com"
|
||||
autoComplete="off"
|
||||
{...formik.getFieldProps('email')}
|
||||
className={clsx(
|
||||
'form-control bg-transparent',
|
||||
{ 'is-invalid': formik.touched.email && formik.errors.email },
|
||||
{
|
||||
'is-valid': formik.touched.email && !formik.errors.email
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</label>
|
||||
{formik.touched.email && formik.errors.email && (
|
||||
<span role="alert" className="text-danger text-xs mt-1">
|
||||
{formik.errors.email}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-5 items-stretch">
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary flex justify-center grow"
|
||||
disabled={loading || formik.isSubmitting}
|
||||
>
|
||||
{loading ? 'Please wait...' : 'Continue'}
|
||||
</button>
|
||||
|
||||
<Link
|
||||
to={currentLayout?.name === 'auth-branded' ? '/auth/login' : '/auth/classic/login'}
|
||||
className="flex items-center justify-center text-sm gap-2 text-gray-700 hover:text-primary"
|
||||
>
|
||||
<KeenIcon icon="black-left" />
|
||||
Back to Login
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { ResetPassword };
|
||||
173
src/auth/pages/jwt/reset-password/ResetPasswordChange.tsx
Normal file
173
src/auth/pages/jwt/reset-password/ResetPasswordChange.tsx
Normal file
@ -0,0 +1,173 @@
|
||||
import { useFormik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import { Alert, KeenIcon } from '@/components';
|
||||
import { useAuthContext } from '@/auth';
|
||||
import { useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useLayout } from '@/providers';
|
||||
import { AxiosError } from 'axios';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
const passwordSchema = Yup.object().shape({
|
||||
newPassword: Yup.string()
|
||||
.min(6, 'Password must be at least 6 characters')
|
||||
.required('New password is required'),
|
||||
confirmPassword: Yup.string()
|
||||
.oneOf([Yup.ref('newPassword')], 'Passwords must match')
|
||||
.required('Please confirm your new password')
|
||||
});
|
||||
|
||||
const ResetPasswordChange = () => {
|
||||
const { currentLayout } = useLayout();
|
||||
const { changePassword } = useAuthContext();
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [hasErrors, setHasErrors] = useState<boolean | undefined>(undefined);
|
||||
const [showNewPassword, setShowNewPassword] = useState(false);
|
||||
const [showNewPasswordConfirmation, setShowNewPasswordConfirmation] = useState(false);
|
||||
const { id } = useParams();
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
},
|
||||
validationSchema: passwordSchema,
|
||||
onSubmit: async (values, { setStatus, setSubmitting }) => {
|
||||
setLoading(true);
|
||||
setHasErrors(undefined);
|
||||
|
||||
const token = id;
|
||||
console.log(token);
|
||||
|
||||
if (!token) {
|
||||
setHasErrors(true);
|
||||
setStatus('Token and email properties are required');
|
||||
setLoading(false);
|
||||
setSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await changePassword(token, values.newPassword, values.confirmPassword);
|
||||
setHasErrors(false);
|
||||
navigate(
|
||||
currentLayout?.name === 'auth-branded'
|
||||
? '/auth/reset-password/changed'
|
||||
: '/auth/classic/reset-password/changed'
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError && error.response) {
|
||||
setStatus(error.response.data.message);
|
||||
} else {
|
||||
setStatus('Password reset failed. Please try again.');
|
||||
}
|
||||
setHasErrors(true);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="card max-w-[370px] w-full">
|
||||
<form
|
||||
className="card-body flex flex-col gap-5 p-10"
|
||||
onSubmit={formik.handleSubmit}
|
||||
noValidate
|
||||
>
|
||||
<div className="text-center">
|
||||
<h3 className="text-lg font-medium text-gray-900">Reset Password</h3>
|
||||
<span className="text-2sm text-gray-700">Enter your new password</span>
|
||||
</div>
|
||||
|
||||
{hasErrors && <Alert variant="danger">{formik.status}</Alert>}
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="form-label text-gray-900">New Password</label>
|
||||
<label className="input">
|
||||
<input
|
||||
type={showNewPassword ? 'text' : 'password'}
|
||||
placeholder="Enter a new password"
|
||||
autoComplete="off"
|
||||
{...formik.getFieldProps('newPassword')}
|
||||
className={clsx(
|
||||
'form-control bg-transparent',
|
||||
{ 'is-invalid': formik.touched.newPassword && formik.errors.newPassword },
|
||||
{ 'is-valid': formik.touched.newPassword && !formik.errors.newPassword }
|
||||
)}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-icon"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setShowNewPassword(!showNewPassword);
|
||||
}}
|
||||
>
|
||||
<KeenIcon icon="eye" className={clsx('text-gray-500', { hidden: showNewPassword })} />
|
||||
<KeenIcon
|
||||
icon="eye-slash"
|
||||
className={clsx('text-gray-500', { hidden: !showNewPassword })}
|
||||
/>
|
||||
</button>
|
||||
</label>
|
||||
{formik.touched.newPassword && formik.errors.newPassword && (
|
||||
<span role="alert" className="text-danger text-xs mt-1">
|
||||
{formik.errors.newPassword}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="form-label font-normal text-gray-900">Confirm New Password</label>
|
||||
<label className="input">
|
||||
<input
|
||||
type={showNewPasswordConfirmation ? 'text' : 'password'}
|
||||
placeholder="Re-enter a new Password"
|
||||
autoComplete="off"
|
||||
{...formik.getFieldProps('confirmPassword')}
|
||||
className={clsx(
|
||||
'form-control bg-transparent',
|
||||
{ 'is-invalid': formik.touched.confirmPassword && formik.errors.confirmPassword },
|
||||
{ 'is-valid': formik.touched.confirmPassword && !formik.errors.confirmPassword }
|
||||
)}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-icon"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setShowNewPasswordConfirmation(!showNewPasswordConfirmation);
|
||||
}}
|
||||
>
|
||||
<KeenIcon
|
||||
icon="eye"
|
||||
className={clsx('text-gray-500', { hidden: showNewPasswordConfirmation })}
|
||||
/>
|
||||
<KeenIcon
|
||||
icon="eye-slash"
|
||||
className={clsx('text-gray-500', { hidden: !showNewPasswordConfirmation })}
|
||||
/>
|
||||
</button>
|
||||
</label>
|
||||
{formik.touched.confirmPassword && formik.errors.confirmPassword && (
|
||||
<span role="alert" className="text-danger text-xs mt-1">
|
||||
{formik.errors.confirmPassword}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-primary flex justify-center grow"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Please wait...' : 'Submit'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { ResetPasswordChange };
|
||||
46
src/auth/pages/jwt/reset-password/ResetPasswordChanged.tsx
Normal file
46
src/auth/pages/jwt/reset-password/ResetPasswordChanged.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { toAbsoluteUrl } from '@/utils';
|
||||
import { useLayout } from '@/providers';
|
||||
|
||||
const ResetPasswordChanged = () => {
|
||||
const { currentLayout } = useLayout();
|
||||
|
||||
return (
|
||||
<div className="card max-w-[440px] w-full">
|
||||
<div className="card-body p-10">
|
||||
<div className="flex justify-center mb-5">
|
||||
<img
|
||||
src={toAbsoluteUrl('/media/illustrations/32.svg')}
|
||||
className="dark:hidden max-h-[180px]"
|
||||
alt=""
|
||||
/>
|
||||
<img
|
||||
src={toAbsoluteUrl('/media/illustrations/32-dark.svg')}
|
||||
className="light:hidden max-h-[180px]"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-medium text-gray-900 text-center mb-4">
|
||||
Your password is changed
|
||||
</h3>
|
||||
<div className="text-2sm text-center text-gray-700 mb-7.5">
|
||||
Your password has been successfully updated.
|
||||
<br />
|
||||
Your account's security is our priority.
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Link
|
||||
to={currentLayout?.name === 'auth-branded' ? '/auth/login' : '/auth/classic/login'}
|
||||
className="btn btn-primary"
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { ResetPasswordChanged };
|
||||
@ -0,0 +1,72 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { toAbsoluteUrl } from '@/utils';
|
||||
import { useLayout } from '@/providers';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const ResetPasswordCheckEmail = () => {
|
||||
const { currentLayout } = useLayout();
|
||||
const [email, setEmail] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setEmail(new URLSearchParams(window.location.search).get('email'));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="card max-w-[440px] w-full">
|
||||
<div className="card-body p-10">
|
||||
<div className="flex justify-center py-10">
|
||||
<img
|
||||
src={toAbsoluteUrl('/media/illustrations/30.svg')}
|
||||
className="dark:hidden max-h-[130px]"
|
||||
alt=""
|
||||
/>
|
||||
<img
|
||||
src={toAbsoluteUrl('/media/illustrations/30-dark.svg')}
|
||||
className="light:hidden max-h-[130px]"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg font-medium text-gray-900 text-center mb-3">Check your email</h3>
|
||||
<div className="text-2sm text-center text-gray-700 mb-7.5">
|
||||
Please click the link sent to your email{' '}
|
||||
<a href="#" className="text-2sm text-gray-800 font-medium hover:text-primary-active">
|
||||
{email}
|
||||
</a>
|
||||
<br />
|
||||
to reset your password. Thank you
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center mb-5">
|
||||
<Link
|
||||
to={
|
||||
currentLayout?.name === 'auth-branded'
|
||||
? '/auth/reset-password/changed'
|
||||
: '/auth/classic/reset-password/changed'
|
||||
}
|
||||
className="btn btn-primary flex justify-center"
|
||||
>
|
||||
Skip for now
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<span className="text-xs text-gray-600">Didn’t receive an email?</span>
|
||||
<Link
|
||||
to={
|
||||
currentLayout?.name === 'auth-branded'
|
||||
? '/auth/reset-password/enter-email'
|
||||
: '/auth/classic/reset-password/enter-email'
|
||||
}
|
||||
className="text-xs font-medium link"
|
||||
>
|
||||
Resend
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { ResetPasswordCheckEmail };
|
||||
@ -0,0 +1,45 @@
|
||||
import { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { KeenIcon } from '@/components';
|
||||
import { useLayout } from '@/providers';
|
||||
|
||||
const ResetPasswordEnterEmail = () => {
|
||||
const { currentLayout } = useLayout();
|
||||
const [searchInput, setSearchInput] = useState('');
|
||||
|
||||
return (
|
||||
<div className="card max-w-[370px] w-full">
|
||||
<form className="card-body flex flex-col gap-5 p-10">
|
||||
<div className="text-center">
|
||||
<h3 className="text-lg font-medium text-gray-900">Your Email</h3>
|
||||
<span className="text-2sm text-gray-700">Enter your email to reset password</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="form-label font-normal text-gray-900">Email</label>
|
||||
<input
|
||||
className="input"
|
||||
type="text"
|
||||
placeholder="email@email.com"
|
||||
value={searchInput}
|
||||
onChange={(e) => setSearchInput(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
to={
|
||||
currentLayout?.name === 'auth-branded'
|
||||
? '/auth/reset-password/check-email'
|
||||
: '/auth/classic/reset-password/check-email'
|
||||
}
|
||||
className="btn btn-primary flex justify-center grow"
|
||||
>
|
||||
Continue
|
||||
<KeenIcon icon="black-right" />
|
||||
</Link>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { ResetPasswordEnterEmail };
|
||||
5
src/auth/pages/jwt/reset-password/index.ts
Normal file
5
src/auth/pages/jwt/reset-password/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from './ResetPassword';
|
||||
export * from './ResetPasswordChange';
|
||||
export * from './ResetPasswordChanged';
|
||||
export * from './ResetPasswordCheckEmail';
|
||||
export * from './ResetPasswordEnterEmail';
|
||||
147
src/auth/providers/JWTProvider.tsx
Normal file
147
src/auth/providers/JWTProvider.tsx
Normal file
@ -0,0 +1,147 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
import axios, { AxiosResponse } from 'axios';
|
||||
import {
|
||||
createContext,
|
||||
type Dispatch,
|
||||
type PropsWithChildren,
|
||||
type SetStateAction,
|
||||
useEffect,
|
||||
useState
|
||||
} from 'react';
|
||||
|
||||
import * as authHelper from '../_helpers';
|
||||
import { type AuthModel, type UserModel } from '@/auth';
|
||||
import { apiConfig } from '@/config/api.config';
|
||||
import { doSaveLogActivity } from '@/actions/GlobalActions';
|
||||
|
||||
const API_URL = apiConfig.service_user;
|
||||
|
||||
export const LOGIN_URL = `${API_URL}/login`;
|
||||
export const FORGOT_PASSWORD_URL = `${API_URL}/reset_password`;
|
||||
export const RESET_PASSWORD_URL = `${API_URL}/update_password`;
|
||||
export const GET_USER_URL = `${API_URL}/user/detail`;
|
||||
|
||||
interface AuthContextProps {
|
||||
loading: boolean;
|
||||
setLoading: Dispatch<SetStateAction<boolean>>;
|
||||
auth: AuthModel | undefined;
|
||||
saveAuth: (auth: AuthModel | undefined) => void;
|
||||
currentUser: UserModel | undefined;
|
||||
setCurrentUser: Dispatch<SetStateAction<UserModel | undefined>>;
|
||||
login: (email: string, password: string, application: string) => Promise<void>;
|
||||
requestPasswordResetLink: (email: string) => Promise<void>;
|
||||
changePassword: (token: string, password: string, password_confirmation: string) => Promise<void>;
|
||||
getUser: () => Promise<AxiosResponse<any> | {}>;
|
||||
logout: () => void;
|
||||
verify: () => Promise<void>;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextProps | null>(null);
|
||||
|
||||
const AuthProvider = ({ children }: PropsWithChildren) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [auth, setAuth] = useState<AuthModel | undefined>(authHelper.getAuth());
|
||||
const [currentUser, setCurrentUser] = useState<UserModel | undefined>();
|
||||
|
||||
const verify = async () => {
|
||||
if (auth) {
|
||||
try {
|
||||
const { data: user } = await getUser();
|
||||
const createCacheUser = {
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
role_name: auth.role_name
|
||||
};
|
||||
localStorage.setItem('user', JSON.stringify(createCacheUser));
|
||||
} catch {
|
||||
saveAuth(undefined);
|
||||
setCurrentUser(undefined);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const saveAuth = (auth: AuthModel | undefined) => {
|
||||
setAuth(auth);
|
||||
if (auth) {
|
||||
authHelper.setAuth(auth);
|
||||
} else {
|
||||
authHelper.removeAuth();
|
||||
}
|
||||
};
|
||||
|
||||
const login = async (username: string, password: string, application: string) => {
|
||||
try {
|
||||
const { data: auth } = await axios
|
||||
.post(LOGIN_URL, { username, password, application })
|
||||
.then((response) => response.data);
|
||||
|
||||
// console.log('auth :', auth);
|
||||
saveAuth({ ...auth.token, id: auth.user.id, role_name: auth.role_name, user: auth.user });
|
||||
setCurrentUser(auth.user);
|
||||
const createActivity = {
|
||||
module: 'Login',
|
||||
description: `Login`,
|
||||
action: 'l'
|
||||
};
|
||||
doSaveLogActivity(createActivity);
|
||||
} catch (error: any) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const requestPasswordResetLink = async (email: string) => {
|
||||
await axios.put(FORGOT_PASSWORD_URL + '/' + email + '/credit/');
|
||||
};
|
||||
|
||||
const changePassword = async (token: string, password: string, retype_password: string) => {
|
||||
await axios.put(`${RESET_PASSWORD_URL}/${token}`, {
|
||||
password,
|
||||
retype_password
|
||||
});
|
||||
};
|
||||
|
||||
const getUser = async () => {
|
||||
let _axios = await axios
|
||||
.get(`${GET_USER_URL}/${authHelper.getAuth()?.id}`)
|
||||
.then((response) => response.data.data);
|
||||
|
||||
return { data: _axios };
|
||||
};
|
||||
|
||||
const logout = async () => {
|
||||
const createActivity = {
|
||||
module: 'Logout',
|
||||
description: `Logout`,
|
||||
action: 'O'
|
||||
};
|
||||
await doSaveLogActivity(createActivity);
|
||||
|
||||
saveAuth(undefined);
|
||||
setCurrentUser(undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
loading,
|
||||
setLoading,
|
||||
auth,
|
||||
saveAuth,
|
||||
currentUser,
|
||||
setCurrentUser,
|
||||
login,
|
||||
// register,
|
||||
requestPasswordResetLink,
|
||||
changePassword,
|
||||
getUser,
|
||||
logout,
|
||||
verify
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export { AuthContext, AuthProvider };
|
||||
10
src/auth/useAuthContext.ts
Normal file
10
src/auth/useAuthContext.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { useContext } from 'react';
|
||||
import { AuthContext } from './providers/JWTProvider';
|
||||
|
||||
export const useAuthContext = () => {
|
||||
const context = useContext(AuthContext);
|
||||
|
||||
if (!context) throw new Error('useAuthContext must be used within AuthProvider');
|
||||
|
||||
return context;
|
||||
};
|
||||
Reference in New Issue
Block a user