chore: connect recent logins to back
This commit is contained in:
@@ -12,6 +12,7 @@ import { PageWrapper } from '../PageWrapper';
|
|||||||
import { PasswordDialog } from './PasswordDialog';
|
import { PasswordDialog } from './PasswordDialog';
|
||||||
import { Toast } from '@/components/Toast';
|
import { Toast } from '@/components/Toast';
|
||||||
import { regex } from '@/utils/regex';
|
import { regex } from '@/utils/regex';
|
||||||
|
import apiClient from '@/lib/apiClient';
|
||||||
|
|
||||||
export function PasswordSecurity() {
|
export function PasswordSecurity() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@@ -53,17 +54,48 @@ export function PasswordSecurity() {
|
|||||||
const handleOpen = () => setOpen(true);
|
const handleOpen = () => setOpen(true);
|
||||||
const handleClose = () => setOpen(false);
|
const handleClose = () => setOpen(false);
|
||||||
|
|
||||||
const handleShowAlert = () => {
|
const handleShowAlert = async () => {
|
||||||
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setTimeout(() => {
|
|
||||||
setLoading(false);
|
const url = changePassword
|
||||||
|
? 'Profile/ChangePassword'
|
||||||
|
: 'Profile/AddPassword';
|
||||||
|
|
||||||
|
const payload = changePassword
|
||||||
|
? {
|
||||||
|
oldPassword: currentPassword,
|
||||||
|
newPassword: password,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
password,
|
||||||
|
confirmPassword,
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await apiClient.post(url, payload, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem('authToken')}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.data?.success) {
|
||||||
setShowPasswordAlert(true);
|
setShowPasswordAlert(true);
|
||||||
setChangePassword(true);
|
if (!changePassword) setChangePassword(true);
|
||||||
handleClose();
|
handleClose();
|
||||||
setPassword('');
|
setPassword('');
|
||||||
setConfirmPassword('');
|
setConfirmPassword('');
|
||||||
setCurrentPassword('');
|
setCurrentPassword('');
|
||||||
}, 1500);
|
} else {
|
||||||
|
console.error('Password update failed:', res.data?.message);
|
||||||
|
// optionally show error toast
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error updating password:', err);
|
||||||
|
// optionally show error toast
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,27 +1,117 @@
|
|||||||
import { Box, Typography, Button } from '@mui/material';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
Button,
|
||||||
|
CircularProgress,
|
||||||
|
useTheme,
|
||||||
|
useMediaQuery,
|
||||||
|
} from '@mui/material';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import type { TFunction } from 'i18next';
|
||||||
import { CardContainer } from '@/components/CardContainer';
|
import { CardContainer } from '@/components/CardContainer';
|
||||||
import { PageWrapper } from '../PageWrapper';
|
import { PageWrapper } from '../PageWrapper';
|
||||||
import React from 'react';
|
import apiClient from '@/lib/apiClient';
|
||||||
|
|
||||||
|
function formatLoginDate(isoDate: string, lang: string, t: TFunction): string {
|
||||||
|
const date = new Date(isoDate);
|
||||||
|
const now = new Date();
|
||||||
|
const diffInMinutes = Math.floor(
|
||||||
|
(now.getTime() - date.getTime()) / (1000 * 60),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (diffInMinutes < 1) return t('active.justNow');
|
||||||
|
if (diffInMinutes < 60)
|
||||||
|
return t('active.minutesAgo', { count: diffInMinutes });
|
||||||
|
|
||||||
|
let displayLocale: string;
|
||||||
|
let options: Intl.DateTimeFormatOptions;
|
||||||
|
|
||||||
|
if (lang.startsWith('fa')) {
|
||||||
|
displayLocale = 'fa-IR';
|
||||||
|
options = {
|
||||||
|
calendar: 'persian',
|
||||||
|
numberingSystem: 'arab',
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
displayLocale = 'en-US';
|
||||||
|
options = {
|
||||||
|
calendar: 'gregory',
|
||||||
|
numberingSystem: 'latn',
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Intl.DateTimeFormat(displayLocale, options).format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LoginLog {
|
||||||
|
loginDateTime: string;
|
||||||
|
ipAddress: string;
|
||||||
|
deviceName: string;
|
||||||
|
deviceOs: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProfileApiResponse {
|
||||||
|
success: boolean;
|
||||||
|
loginLogs?: LoginLog[];
|
||||||
|
}
|
||||||
|
|
||||||
export function RecentLogins() {
|
export function RecentLogins() {
|
||||||
const { t } = useTranslation('setting');
|
const { t, i18n } = useTranslation('setting');
|
||||||
const data = [
|
const token = localStorage.getItem('authToken');
|
||||||
{
|
|
||||||
id: 0,
|
const [logs, setLogs] = useState<LoginLog[]>([]);
|
||||||
time: 'دقایقی پیش',
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
device: 'asus i5 24i',
|
const [fetchError, setFetchError] = useState<string | null>(null);
|
||||||
ip: '192.168.1.1',
|
|
||||||
current: true,
|
const theme = useTheme();
|
||||||
},
|
const isXsup = useMediaQuery(theme.breakpoints.up('xs'));
|
||||||
{
|
|
||||||
id: 1,
|
useEffect(() => {
|
||||||
time: '۲۲:۱۳ - ۱۴۰۴/۰۹/۰۹',
|
const fetchLoginLogs = async () => {
|
||||||
device: 'samsung s23 ultra',
|
setIsLoading(true);
|
||||||
ip: '192.220.1.1',
|
setFetchError(null);
|
||||||
current: false,
|
|
||||||
},
|
if (!token) {
|
||||||
];
|
setIsLoading(false);
|
||||||
|
setFetchError(t('active.notLoggedIn'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await apiClient.post<ProfileApiResponse>(
|
||||||
|
'/Profile/GetProfile',
|
||||||
|
{},
|
||||||
|
{ headers: { Authorization: `Bearer ${token}` } },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.data.success && Array.isArray(res.data.loginLogs)) {
|
||||||
|
setLogs(res.data.loginLogs);
|
||||||
|
} else {
|
||||||
|
throw new Error(t('active.failFetchActiveSessions'));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(t('active.failFetchActiveSessions'), err);
|
||||||
|
setFetchError(t('active.errorFetch'));
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchLoginLogs();
|
||||||
|
}, [token, i18n.language, t]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageWrapper>
|
<PageWrapper>
|
||||||
@@ -29,30 +119,33 @@ export function RecentLogins() {
|
|||||||
title={t('securityForm.recentLogins')}
|
title={t('securityForm.recentLogins')}
|
||||||
subtitle={t('securityForm.description')}
|
subtitle={t('securityForm.description')}
|
||||||
>
|
>
|
||||||
|
{isLoading ? (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
py: 2,
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
justifyContent: 'center',
|
||||||
gap: 2,
|
alignItems: 'center',
|
||||||
bgcolor: 'background.paper',
|
p: 4,
|
||||||
flex: 1,
|
minHeight: '200px',
|
||||||
borderBottom: '1px solid',
|
|
||||||
borderColor: 'divider',
|
|
||||||
'&:last-child': {
|
|
||||||
borderBottom: 'none',
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<CircularProgress />
|
||||||
|
</Box>
|
||||||
|
) : fetchError ? (
|
||||||
|
<Box sx={{ textAlign: 'center', p: 4 }}>
|
||||||
|
<Typography color="error.main">{fetchError}</Typography>
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
<Box sx={{ px: 4 }}>
|
<Box sx={{ px: 4 }}>
|
||||||
{data.map((d) => (
|
{logs.map((log, index) => (
|
||||||
<React.Fragment key={d.id}>
|
<React.Fragment key={index}>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: { xs: 'column', sm: 'row' },
|
flexDirection: { xs: 'column', sm: 'row' },
|
||||||
alignItems: { xs: 'flex-start', sm: 'center' },
|
alignItems: { xs: 'flex-start', sm: 'center' },
|
||||||
minHeight: 50,
|
minHeight: 50,
|
||||||
|
py: 1.5,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography
|
||||||
@@ -61,10 +154,9 @@ export function RecentLogins() {
|
|||||||
flexBasis: { xs: '100%', sm: 'auto' },
|
flexBasis: { xs: '100%', sm: 'auto' },
|
||||||
mb: { xs: 1, sm: 0 },
|
mb: { xs: 1, sm: 0 },
|
||||||
minWidth: { sm: '172.5px' },
|
minWidth: { sm: '172.5px' },
|
||||||
order: { xs: 1, sm: 1 },
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{d.time}
|
{formatLoginDate(log.loginDateTime, i18n.language, t)}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
@@ -72,10 +164,9 @@ export function RecentLogins() {
|
|||||||
flexBasis: { xs: '100%', sm: 'auto' },
|
flexBasis: { xs: '100%', sm: 'auto' },
|
||||||
mb: { xs: 1, sm: 0 },
|
mb: { xs: 1, sm: 0 },
|
||||||
minWidth: { sm: '172.5px' },
|
minWidth: { sm: '172.5px' },
|
||||||
order: { xs: 1, sm: 1 },
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{d.device}
|
{`${log.deviceOs} ${log.deviceName}`}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
@@ -83,43 +174,18 @@ export function RecentLogins() {
|
|||||||
flexBasis: { xs: '100%', sm: 'auto' },
|
flexBasis: { xs: '100%', sm: 'auto' },
|
||||||
mb: { xs: 1, sm: 0 },
|
mb: { xs: 1, sm: 0 },
|
||||||
minWidth: { sm: '172.5px' },
|
minWidth: { sm: '172.5px' },
|
||||||
order: { xs: 1, sm: 1 },
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{d.ip}
|
{log.ipAddress}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
flexBasis: { xs: '100%', sm: 'auto' },
|
|
||||||
mb: { xs: 1, sm: 0 },
|
|
||||||
textAlign: { xs: 'left', sm: 'center' },
|
|
||||||
minWidth: { sm: '172.5px' },
|
|
||||||
order: { xs: 4, sm: 4 },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{d.current && (
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
sx={{
|
|
||||||
borderRadius: 2,
|
|
||||||
border: '2px solid',
|
|
||||||
borderColor: 'success.main',
|
|
||||||
height: '30px',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
color: 'success.main',
|
|
||||||
textTransform: 'none',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('securityForm.currentDevice')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
{isXsup && (
|
||||||
<Box sx={{ color: 'divider', borderBottom: '1px solid' }} />
|
<Box sx={{ color: 'divider', borderBottom: '1px solid' }} />
|
||||||
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
)}
|
||||||
</CardContainer>
|
</CardContainer>
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -75,13 +75,16 @@ export function Setting() {
|
|||||||
const [isFetching, setIsFetching] = useState(true);
|
const [isFetching, setIsFetching] = useState(true);
|
||||||
const [fetchError, setFetchError] = useState<string | null>(null);
|
const [fetchError, setFetchError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const notLoggedIn = t('settings.notLoggedIn');
|
||||||
|
const failRetrieve = t('settings.failRetrieve');
|
||||||
|
const errorFetch = t('settings.errorFetch');
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchUserSettings = async () => {
|
const fetchUserSettings = async () => {
|
||||||
setIsFetching(true);
|
setIsFetching(true);
|
||||||
setFetchError(null);
|
setFetchError(null);
|
||||||
if (!token) {
|
if (!token) {
|
||||||
setIsFetching(false);
|
setIsFetching(false);
|
||||||
setFetchError(t('settings.notLoggedIn'));
|
setFetchError(notLoggedIn);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -114,12 +117,13 @@ export function Setting() {
|
|||||||
setSavedSettings(newSettings);
|
setSavedSettings(newSettings);
|
||||||
setDraftSettings(newSettings);
|
setDraftSettings(newSettings);
|
||||||
setMode(themeMode);
|
setMode(themeMode);
|
||||||
|
|
||||||
i18n.changeLanguage(languageCode);
|
i18n.changeLanguage(languageCode);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(res.data.message || t('settings.failRetrieve'));
|
throw new Error(res.data.message || failRetrieve);
|
||||||
}
|
}
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
let message = t('settings.errorFetch');
|
let message = errorFetch;
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
message = e.message;
|
message = e.message;
|
||||||
}
|
}
|
||||||
@@ -128,8 +132,9 @@ export function Setting() {
|
|||||||
setIsFetching(false);
|
setIsFetching(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchUserSettings();
|
fetchUserSettings();
|
||||||
}, [token, setMode, i18n, t]);
|
}, [token, setMode, i18n, notLoggedIn, failRetrieve, errorFetch]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
@@ -313,7 +318,7 @@ export function Setting() {
|
|||||||
variant="Bold"
|
variant="Bold"
|
||||||
color={
|
color={
|
||||||
savedSettings.theme === 'light'
|
savedSettings.theme === 'light'
|
||||||
? 'action.hover'
|
? 'text.primary'
|
||||||
: 'primary.main'
|
: 'primary.main'
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user