Kaynağa Gözat

no message

master
ebeveyn
işleme
c45b22da09
10 değiştirilmiş dosya ile 508 ekleme ve 296 silme
  1. +3
    -2
      package.json
  2. +47
    -55
      src/auth/index.js
  3. +36
    -0
      src/components/TwoFAModal.js
  4. +3
    -1
      src/layout/MainLayout/Header/HeaderContent/Profile/index.js
  5. +2
    -2
      src/menu-items/index.js
  6. +8
    -0
      src/menu-items/misc.js
  7. +268
    -234
      src/pages/authentication/auth-forms/AuthLogin.js
  8. +127
    -0
      src/pages/profile/profile.js
  9. +1
    -1
      src/routes/ClientRoutes.js
  10. +13
    -1
      src/routes/SettingRoutes.js

+ 3
- 2
package.json Dosyayı Görüntüle

@@ -14,9 +14,9 @@
"@emotion/react": "^11.10.4",
"@emotion/styled": "^11.10.4",
"@mantine/core": "^7.0.2",
"@mui/icons-material": "^5.14.1",
"@mui/icons-material": "^5.18.0",
"@mui/lab": "^5.0.0-alpha.139",
"@mui/material": "^5.14.11",
"@mui/material": "^5.18.0",
"@mui/system": "^5.14.11",
"@mui/x-data-grid": "^6.11.1",
"@reduxjs/toolkit": "^1.8.5",
@@ -50,6 +50,7 @@
"mui-image": "^1.0.7",
"prop-types": "^15.8.1",
"pspdfkit": "^2024.8.2",
"qrcode.react": "^4.2.0",
"react": "^18.2.0",
"react-apexcharts": "^1.4.0",
"react-copy-to-clipboard": "^5.1.0",


+ 47
- 55
src/auth/index.js Dosyayı Görüntüle

@@ -121,66 +121,58 @@ export const SetupAxiosInterceptors = (dispatch, navigate) => {
);

axios.interceptors.response.use(
(response) => {
//updateLastRequestTime(Date.now());
return response;
},
(response) => response,
async (error) => {
// ** const { config, response: { status } } = error
//const {response} = error
// if (error.response.status === 401) {
// dispatch(handleLogoutFunction());
// navigate('/login');
// }
//
// // ** if (status === 401) {
// if (response.status === 401) {
// dispatch(handleLogoutFunction());
// navigate('/login');
// }
//
// if (response && response.status === 401) {
// dispatch(handleLogoutFunction());
// navigate('/login');
// }

if (error.response.status === 401 && error.config.url !== apiPath + REFRESH_TOKEN) {
// Make a request to refresh the access token
const refreshToken = localStorage.getItem('refreshToken');
if (isRefreshToken) {
return;
const originalRequest = error.config;

if (error.response) {
// === NEW: Handle 2FA required response ===
if (error.response.data && error.response.data.requires2FA && !originalRequest._retry2FA) {
originalRequest._retry2FA = true;
// This is caught in AuthLogin.js tryLogin — do NOT reject here
return Promise.reject(error);
}
isRefreshToken = true;
return axios
.post(`${apiPath}${REFRESH_TOKEN}`, {
refreshToken: refreshToken // Replace with your refresh token
})
.then((response) => {
if (response.status === 200) {
const newAccessToken = response.data.accessToken;
const newRefreshToken = response.data.refreshToken;
localStorage.setItem('accessToken', newAccessToken);
localStorage.setItem('refreshToken', newRefreshToken);
isRefreshToken = false;
// === END NEW ===

if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
const refreshToken = localStorage.getItem('refreshToken');
if (refreshToken) {
let isRefreshToken = localStorage.getItem('isRefreshToken');
if (isRefreshToken) {
return Promise.reject(error);
}
localStorage.setItem('isRefreshToken', 'true');
return axios
.post(`${apiPath}${REFRESH_TOKEN}`, { refreshToken })
.then((response) => {
if (response.status === 200) {
const newAccessToken = response.data.accessToken;
const newRefreshToken = response.data.refreshToken;
localStorage.setItem('accessToken', newAccessToken);
localStorage.setItem('refreshToken', newRefreshToken);
localStorage.setItem('axiosToken', 'Bearer ' + newAccessToken);
originalRequest.headers['Authorization'] = 'Bearer ' + newAccessToken;
localStorage.removeItem('isRefreshToken');
return axios(originalRequest);
}
})
.catch((refreshError) => {
dispatch(handleLogoutFunction());
navigate('/login');
localStorage.removeItem('isRefreshToken');
window.location.reload();
}
})
.catch((refreshError) => {
dispatch(handleLogoutFunction());
navigate('/login');
isRefreshToken = false;
window.location.reload();
throw refreshError;
});
} else {
if (error.response.status === 401) {
await dispatch(handleLogoutFunction());
await navigate('/login');
await window.location.reload();
return Promise.reject(refreshError);
});
} else {
dispatch(handleLogoutFunction());
navigate('/login');
window.location.reload();
}
}

if (error.response.status === 500){
//setIsUploading(false);
if (error.response.status === 500) {
// handle 500 if needed
}
}



+ 36
- 0
src/components/TwoFAModal.js Dosyayı Görüntüle

@@ -0,0 +1,36 @@
import React, { useState } from 'react';
import { Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField, Typography, Box } from '@mui/material';
import QRCode from 'qrcode.react';

const TwoFAModal = ({ open, mode, qrUrl, onSubmit, onClose, error }) => {
const [code, setCode] = useState('');

return (
<Dialog open={open} onClose={onClose}>
<DialogTitle>{mode === 'setup' ? 'Setup 2FA' : 'Enter 2FA Code'}</DialogTitle>
<DialogContent>
{mode === 'setup' && qrUrl && (
<Box sx={{ textAlign: 'center', my: 2 }}>
<Typography>Scan this QR code with Microsoft Authenticator:</Typography>
<QRCode value={qrUrl} size={200} />
<Typography variant="caption">Then enter a code to verify.</Typography>
</Box>
)}
<TextField
label="6-digit code"
value={code}
onChange={(e) => setCode(e.target.value)}
fullWidth
error={!!error}
helperText={error}
/>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>Cancel</Button>
<Button onClick={() => onSubmit(code)} variant="contained">Verify</Button>
</DialogActions>
</Dialog>
);
};

export default TwoFAModal;

+ 3
- 1
src/layout/MainLayout/Header/HeaderContent/Profile/index.js Dosyayı Görüntüle

@@ -136,7 +136,9 @@ const Profile = () => {
<Stack direction="row" spacing={1.25} alignItems="center">
{/*<Avatar alt="profile user" src={defaultAvatar} sx={{ width: 32, height: 32 }} />*/}
<Stack>
<Typography variant="h6" sx={{ml:3, fontWeight: 'bold'}}>{userData == null ? "" : userData.fullName}</Typography>
<Typography variant="h6" sx={{ml:3, fontWeight: 'bold'}}>
{userData ? (userData.fullname || userData.name || userData.username || '') : ''}
</Typography>
{/*<Typography variant="body2" color="textSecondary">*/}
{/* UI/UX Designer*/}
{/*</Typography>*/}


+ 2
- 2
src/menu-items/index.js Dosyayı Görüntüle

@@ -2,7 +2,7 @@
//import pages from './pages';
import dashboard from './dashboard';
import setting from "./setting";
//import misc from "./misc";
import misc from "./misc";
import award from "./award";
import client from "./client";
import appreciation from "./appreciation";
@@ -12,7 +12,7 @@ import appreciation from "./appreciation";
// ==============================|| MENU ITEMS ||============================== //

const menuItems = {
items: [client, setting]
items: [client, setting, misc]
// items: [dashboard, client, setting]
};
// pages, utilities, support, misc


+ 8
- 0
src/menu-items/misc.js Dosyayı Görüntüle

@@ -15,6 +15,14 @@ const misc = {
title: 'Miscellaneous',
type: 'group',
children: [
{
id: 'profile',
title: 'Profile & 2FA',
type: 'item',
url: '/profile',
icon: icons.ProfileOutlined,
breadcrumbs: false
},
{
id: 'logout',
title: 'Logout',


+ 268
- 234
src/pages/authentication/auth-forms/AuthLogin.js Dosyayı Görüntüle

@@ -1,283 +1,317 @@
import React, {useState} from 'react';
import {useNavigate} from 'react-router-dom';
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useDispatch } from 'react-redux';

// material-ui
import {
Button,
//Checkbox,
//Divider,
//FormControlLabel,
FormHelperText,
Grid,
//Link,
IconButton,
InputAdornment,
InputLabel,
Stack, TextField,
//Typography
Button,
FormHelperText,
Grid,
InputLabel,
Stack,
TextField,
IconButton,
InputAdornment,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Typography,
Box
} from '@mui/material';

// third party
import * as Yup from 'yup';
import { Formik } from 'formik';

// project import
//import FirebaseSocial from './FirebaseSocial';
// project imports
import AnimateButton from 'components/@extended/AnimateButton';
import { handleLogin } from 'auth/index';
import axios from 'axios';
import { apiPath } from '../../../auth/utils';
import { LOGIN_PATH } from '../../../utils/ApiPathConst';
import { LIONER_LOGIN_THEME } from '../../../themes/themeConst';
import { ThemeProvider } from '@emotion/react';

// assets
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
//import axios from "axios";
import {useDispatch} from "react-redux";
import {handleLogin} from "auth/index";
//import useJwt from "../../../auth/jwt/useJwt";
import axios from "axios";
import {apiPath} from "../../../auth/utils";
import {LOGIN_PATH} from "../../../utils/ApiPathConst";
import {LIONER_LOGIN_THEME} from "../../../themes/themeConst";
import {ThemeProvider} from "@emotion/react";
//import {AbilityContext} from "utils/context/Can"
// ============================|| FIREBASE - LOGIN ||============================ //
import { QRCodeSVG } from 'qrcode.react';

// ============================|| AUTH LOGIN WITH 2FA ||============================ //

const AuthLogin = () => {
//const ability = useContext(AbilityContext)
const dispatch = useDispatch()
const navigate = useNavigate()
//const [checked, setChecked] = useState(false);
const dispatch = useDispatch();
const navigate = useNavigate();

const [showPassword, setShowPassword] = useState(false);
const [userName, setUserName] = useState('');
const [userPassword, setUserPassword] = useState('');
const [errors, setErrors] = useState({});

// 2FA States
const [twoFAModalOpen, setTwoFAModalOpen] = useState(false);
const [twoFAMode, setTwoFAMode] = useState('login'); // 'login' or 'setup'
const [qrUrl, setQrUrl] = useState('');
const [twoFACode, setTwoFACode] = useState('');
const [twoFAError, setTwoFAError] = useState('');
const [partialToken, setPartialToken] = useState(''); // If backend uses partial session

const handleClickShowPassword = () => {
setShowPassword(!showPassword);
};

const [userName, setUserName] = useState("");
const [userPassword, setUserPassword] = useState("");

const handleMouseDownPassword = (event) => {
event.preventDefault();
};

const onUserNameChange = (e) => setUserName(e.target.value);
const onPasswordChange = (e) => setUserPassword(e.target.value);

const tryLogin = () => {
const formErrors = {};

if (!userName) formErrors.loginError = 'Username is required.';
if (!userPassword) formErrors.passwordError = 'Password is required.';

setErrors(formErrors);

if (Object.keys(formErrors).length === 0) {
axios
.post(`${apiPath}${LOGIN_PATH}`, {
username: userName,
password: userPassword
})
.then(async (response) => {
const data = response.data;

// Case 1: 2FA is required
if (data.requires2FA) {
setPartialToken(data.partialToken || ''); // optional, depending on backend
setTwoFAMode('login');
setTwoFAModalOpen(true);
return;
}

// Case 2: Login successful (no 2FA or already verified)
if (data.accessToken) {
const userData = {
id: data.id,
fullName: data.name,
email: data.email,
role: data.role,
abilities: data.abilities,
subDivisionId: data.subDivisionId,
lotusNotesUser: data.lotusNotesUser
};

await dispatch(handleLogin({ ...userData, accessToken: data.accessToken, refreshToken: data.refreshToken }));

const lastPath = localStorage.getItem('lastVisitedPath');
navigate(lastPath || '/client');
window.location.reload();
localStorage.removeItem('lastVisitedPath');

// Optional: Prompt to setup 2FA if not enabled
if (!data.twoFactorEnabled) {
setTimeout(() => {
if (window.confirm('For extra security, would you like to enable 2FA with Microsoft Authenticator?')) {
start2FASetup();
}
}, 1000);
}
}
})
.catch((error) => {
const formErrors = {};
formErrors.passwordError = error.response?.data?.message || 'Invalid credentials';
setErrors(formErrors);
});
}
};

const start2FASetup = () => {
axios
.post(`${apiPath}/2fa/setup`) // Requires user to be authenticated
.then((res) => {
setQrUrl(res.data.otpauthUrl);
setTwoFAMode('setup');
setTwoFAModalOpen(true);
setTwoFAError('');
})
.catch((err) => {
alert('Failed to start 2FA setup: ' + (err.response?.data?.message || err.message));
});
};

const handle2FASubmit = () => {
if (twoFACode.length !== 6 || !/^\d+$/.test(twoFACode)) {
setTwoFAError('Please enter a valid 6-digit code');
return;
}

const endpoint = twoFAMode === 'setup' ? '/2fa/verify-setup' : '/2fa/verify-login';

// Use the actual username from login form, NOT hard-coded '2fi'
const payload = {
code: twoFACode,
username: userName // ← This is your state from <TextField onChange={onUserNameChange} />
};

axios
.post(`${apiPath}${endpoint}`, payload)
.then(async (res) => {
if (twoFAMode === 'setup') {
alert('2FA enabled!');
setTwoFAModalOpen(false);
} else {
// Full login - use the exact same structure as normal login
await dispatch(handleLogin(res.data)); // res.data should be JwtResponse
setTwoFAModalOpen(false);
const lastPath = localStorage.getItem('lastVisitedPath');
navigate(lastPath || '/client');
window.location.reload();
}
setTwoFACode('');
})
.catch((err) => {
setTwoFAError(err.response?.data?.message || 'Invalid or expired code');
});
};

const formErrors = {};

if (userName.length===0) {
formErrors.loginError = 'Username are required.';
}

if (userPassword.length===0){
formErrors.passwordError = 'Password are required.';
}

setErrors(formErrors);

if (Object.keys(formErrors).length === 0) {
axios.post(`${apiPath}${LOGIN_PATH}`,
{username: userName, password: userPassword},
)
.then(async (response) => {
const userData = {
id: response.data.id,
fullName: response.data.name,
email: response.data.email,
role: response.data.role,
abilities: response.data.abilities,
subDivisionId: response.data.subDivisionId,
lotusNotesUser: response.data.lotusNotesUser
//avatar: require('src/assets/images/users/avatar-3.png').default,
}
const data = {
...userData,
accessToken: response.data.accessToken,
refreshToken: response.data.refreshToken
}
await dispatch(handleLogin(data))
//const abilities = response.data.abilities
//ability.update(abilities)
const lastPath = localStorage.getItem('lastVisitedPath');

await navigate(lastPath === null ? '/client' : lastPath);
// await navigate(lastPath === null ? '/lionerDashboard' : lastPath);
await window.location.reload();
await localStorage.removeItem('lastVisitedPath');

})
.catch(error => {
const formErrors = {};
formErrors.loginError = " ";
formErrors.passwordError = error.response.data.message;
setErrors(formErrors);

return false;
});

/*useJwt
.login({username: userName, password: userPassword})
.then((response) => {
const userData = {
id: response.data.id,
fullName: response.data.name,
email: response.data.email,
role: response.data.role,
abilities: response.data.abilities,
//avatar: require('src/assets/images/users/avatar-3.png').default,
}
const data = {...userData, accessToken: response.data.accessToken, refreshToken: response.data.refreshToken}
dispatch(handleLogin(data))
//const abilities = response.data.abilities
//ability.update(abilities)
navigate('/lionerDashboard');
})
.catch(error => {
console.log(error);
return Promise.reject(error);
});*/
}

}

const onUserNameChange = (event) => {
setUserName(event.target.value);
}

const onPasswordChange = (event) => {
setUserPassword(event.target.value);
}
return (
<>
<Formik
initialValues={{
userName: 'test',
email: 'info@codedthemes.com',
password: '123456',
userName: '',
password: '',
submit: null
}}
validationSchema={Yup.object().shape({
email: Yup.string().email('Must be a valid email').max(255).required('Email is required'),
password: Yup.string().max(255).required('Password is required')
userName: Yup.string().required('Username is required'),
password: Yup.string().required('Password is required')
})}
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
try {
setStatus({ success: false });
setSubmitting(false);
} catch (err) {
setStatus({ success: false });
setErrors({ submit: err.message });
setSubmitting(false);
}
}}
>
{({ handleBlur, handleSubmit, isSubmitting /*touched*/ }) => (
<form onSubmit={handleSubmit}>
<ThemeProvider theme={LIONER_LOGIN_THEME}>
{({ handleBlur }) => (
<form>
<ThemeProvider theme={LIONER_LOGIN_THEME}>
<Grid container spacing={3}>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="username">User Name</InputLabel>
<TextField
id="username"
name="username"
onBlur={handleBlur}
onChange={onUserNameChange}
placeholder="Enter user name"
fullWidth
variant="outlined"
error={!!errors.loginError}
helperText={errors.loginError}
/>
{/*{touched.email && errors.email && (*/}
{/* <FormHelperText error id="standard-weight-helper-text-email-login">*/}
{/* {errors.email}*/}
{/* </FormHelperText>*/}
{/*)}*/}
</Stack>
</Grid>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="-password-login">Password</InputLabel>
<TextField
fullWidth
variant="outlined"
error={!!errors.passwordError}
helperText={errors.passwordError}
id="-password-login"
type={showPassword ? 'text' : 'password'}
name="password"
onBlur={handleBlur}
onChange={onPasswordChange}
InputProps={{
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="username">User Name</InputLabel>
<TextField
id="username"
name="username"
value={userName}
onBlur={handleBlur}
onChange={onUserNameChange}
placeholder="Enter user name"
fullWidth
variant="outlined"
error={!!errors.loginError}
helperText={errors.loginError}
/>
</Stack>
</Grid>

<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="password-login">Password</InputLabel>
<TextField
fullWidth
variant="outlined"
error={!!errors.passwordError}
helperText={errors.passwordError}
id="password-login"
type={showPassword ? 'text' : 'password'}
value={userPassword}
name="password"
onBlur={handleBlur}
onChange={onPasswordChange}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={handleClickShowPassword}
onMouseDown={handleMouseDownPassword}
edge="end"
size="large"
>
{showPassword ? <EyeOutlined /> : <EyeInvisibleOutlined />}
</IconButton>
</InputAdornment>
),
}}
placeholder="Enter password"
/>
{/*{touched.password && errors.password && (*/}
{/* <FormHelperText error id="standard-weight-helper-text-password-login">*/}
{/* {errors.password}*/}
{/* </FormHelperText>*/}
{/*)}*/}
</Stack>
</Grid>
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={handleClickShowPassword}
onMouseDown={handleMouseDownPassword}
edge="end"
size="large"
>
{showPassword ? <EyeOutlined /> : <EyeInvisibleOutlined />}
</IconButton>
</InputAdornment>
)
}}
placeholder="Enter password"
/>
</Stack>
</Grid>

<Grid item xs={12} sx={{ mt: -1 }}>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
{/*<FormControlLabel*/}
{/* control={*/}
{/* <Checkbox*/}
{/* checked={checked}*/}
{/* onChange={(event) => setChecked(event.target.checked)}*/}
{/* name="checked"*/}
{/* color="primary"*/}
{/* size="small"*/}
{/* />*/}
{/* }*/}
{/* label={<Typography variant="h6">Keep me sign in</Typography>}*/}
{/*/>*/}
{/*<Link variant="h6" component={RouterLink} to="" color="text.primary">*/}
{/* Forgot Password?*/}
{/*</Link>*/}
</Stack>
</Grid>
{errors.submit && (
<Grid item xs={12}>
<FormHelperText error>{errors.submit}</FormHelperText>
<AnimateButton>
<Button
onClick={tryLogin}
sx={{ height: '50px' }}
fullWidth
size="large"
variant="contained"
color="primary"
>
Login
</Button>
</AnimateButton>
</Grid>
)}
<Grid item xs={12}>
<AnimateButton>
<Button disableElevation onClick={tryLogin}
sx={{height: '50px'}}
disabled={isSubmitting} fullWidth size="large" type="submit" variant="contained" color="primary">
Login
</Button>
</AnimateButton>
</Grid>
{/*<Grid item xs={12}>*/}
{/* <Divider>*/}
{/* <Typography variant="caption"> Login with</Typography>*/}
{/* </Divider>*/}
{/*</Grid>*/}
{/*<Grid item xs={12}>*/}
{/* <FirebaseSocial />*/}
{/*</Grid>*/}
</Grid>
</ThemeProvider>
</ThemeProvider>
</form>
)}
</Formik>

{/* 2FA Modal */}
<Dialog open={twoFAModalOpen} onClose={() => setTwoFAModalOpen(false)} maxWidth="sm" fullWidth>
<DialogTitle>
{twoFAMode === 'setup' ? 'Set Up Two-Factor Authentication' : 'Two-Factor Authentication Required'}
</DialogTitle>
<DialogContent>
{twoFAMode === 'setup' && qrUrl && (
<Box sx={{ textAlign: 'center', my: 3 }}>
<Typography variant="body1" gutterBottom>
Scan this QR code with <strong>Microsoft Authenticator</strong>:
</Typography>
<QRCodeSVG value={qrUrl} size={220} level="M" />
<Typography variant="caption" display="block" sx={{ mt: 2 }}>
After scanning, enter a code from the app to verify.
</Typography>
</Box>
)}

{twoFAMode === 'login' && (
<Typography variant="body1" sx={{ mb: 2 }}>
Open Microsoft Authenticator and enter the 6-digit code for your account.
</Typography>
)}

<TextField
autoFocus
fullWidth
label="6-Digit Code"
value={twoFACode}
onChange={(e) => setTwoFACode(e.target.value.replace(/\D/g, '').slice(0, 6))}
error={!!twoFAError}
helperText={twoFAError}
inputProps={{ maxLength: 6 }}
sx={{ mt: 2 }}
/>
</DialogContent>
<DialogActions>
<Button onClick={() => setTwoFAModalOpen(false)}>Cancel</Button>
<Button onClick={handle2FASubmit} variant="contained" disabled={twoFACode.length !== 6}>
Verify
</Button>
</DialogActions>
</Dialog>
</>
);
};

export default AuthLogin;
export default AuthLogin;

+ 127
- 0
src/pages/profile/profile.js Dosyayı Görüntüle

@@ -0,0 +1,127 @@
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { QRCodeSVG } from 'qrcode.react'; // Use SVG for sharp rendering
import {apiPath} from "../../auth/utils";

import {
Box,
Button,
Typography,
TextField,
Card,
CardContent,
Alert,
CircularProgress
} from '@mui/material';

const Profile = () => {
const [qrUrl, setQrUrl] = useState('');
const [code, setCode] = useState('');
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState('');
const [error, setError] = useState('');

useEffect(() => {
// Fetch current user or check Redux store for twoFactorEnabled
// Hide "Enable" button if already true
}, []);

const start2FASetup = async () => {
setLoading(true);
setMessage('');
setError('');
try {
const response = await axios.post(`${apiPath}/2fa/setup`, {}, {
headers: { Authorization: `Bearer ${localStorage.getItem('accessToken')}` } // Adjust to your auth method
});
setQrUrl(response.data.otpauthUrl);
} catch (err) {
setError('Failed to start 2FA setup. Please try again.');
} finally {
setLoading(false);
}
};

const verifyCode = async () => {
setLoading(true);
setMessage('');
setError('');
try {
await axios.post(`${apiPath}/2fa/verify-setup`, { code }, {
headers: { Authorization: `Bearer ${localStorage.getItem('accessToken')}` }
});
setMessage('2FA enabled successfully! You will now be prompted for a code on login.');
setQrUrl('');
setCode('');
} catch (err) {
setError('Invalid or expired code. Try again.');
} finally {
setLoading(false);
}
};

return (
<Box sx={{ maxWidth: 800, mx: 'auto', mt: 4 }}>
<Typography variant="h4" gutterBottom>
Profile & Security
</Typography>

<Card sx={{ mt: 3 }}>
<CardContent>
<Typography variant="h6" gutterBottom>
Two-Factor Authentication (2FA)
</Typography>

{!qrUrl ? (
<>
<Typography variant="body1" paragraph>
Add an extra layer of security to your account with 2FA using Microsoft Authenticator.
</Typography>
<Button
variant="contained"
color="primary"
onClick={start2FASetup}
disabled={loading}
>
{loading ? <CircularProgress size={24} /> : 'Enable 2FA'}
</Button>
</>
) : (
<Box sx={{ textAlign: 'center', my: 4 }}>
<Typography variant="body1" gutterBottom>
Scan this QR code with <strong>Microsoft Authenticator</strong>:
</Typography>
<QRCodeSVG value={qrUrl} size={220} level="M" />
<Typography variant="caption" display="block" sx={{ mt: 2, mb: 3 }}>
After scanning, enter the 6-digit code from the app:
</Typography>
<TextField
label="Verification Code"
value={code}
onChange={(e) => setCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
inputProps={{ maxLength: 6 }}
sx={{ width: 200 }}
/>
<Box sx={{ mt: 2 }}>
<Button
variant="contained"
onClick={verifyCode}
disabled={loading || code.length !== 6}
>
{loading ? <CircularProgress size={24} /> : 'Verify & Enable'}
</Button>
</Box>
</Box>
)}

{message && <Alert severity="success" sx={{ mt: 2 }}>{message}</Alert>}
{error && <Alert severity="error" sx={{ mt: 2 }}>{error}</Alert>}
</CardContent>
</Card>

{/* Add other profile fields here later */}
</Box>
);
};

export default Profile;

+ 1
- 1
src/routes/ClientRoutes.js Dosyayı Görüntüle

@@ -32,7 +32,7 @@ const ClientRoutes =() => {
handleRouteAbility(
ability.can('VIEW', 'CLIENT'),
<ClientSearchPage />,
<Navigate to="/userSearchview" />
<Navigate to="/" />
)
),
},


+ 13
- 1
src/routes/SettingRoutes.js Dosyayı Görüntüle

@@ -28,11 +28,13 @@ const UserActionLogPage = Loadable(lazy(() => import('pages/lionerUserActionLog'
const EmailConfigPage = Loadable(lazy(() => import('pages/lionerEmailConfig')));
const GenerateReminderPage = Loadable(lazy(() => import('pages/lionerManualButtonPage')));
const ClientDepartmentPage = Loadable(lazy(() => import('pages/lionerClientDepartmentPage')));
const ProfilePage = Loadable(lazy(() => import('pages/profile/profile')));

// ==============================|| AUTH ROUTING ||============================== //

const SettingRoutes = () => {
const ability = useContext(AbilityContext);
return {
path: '/',
element: <MainLayout />,
@@ -208,6 +210,16 @@ const SettingRoutes = () => {
)
),
},
{
path: 'profile',
element: (
handleRouteAbility(
true, // Or use a specific ability like ability.can('VIEW', 'PROFILE')
<ProfilePage />,
<Navigate to="/" />
)
),
},
{
path: 'emailConfig',
element: (


Yükleniyor…
İptal
Kaydet