chore: connect recent logins to back

This commit is contained in:
Koosha Lahouti
2025-08-12 22:14:52 +03:30
parent 782ef2a2de
commit cd4113b75a
3 changed files with 188 additions and 85 deletions

View File

@@ -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 () => {
setLoading(true); try {
setTimeout(() => { setLoading(true);
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);
if (!changePassword) setChangePassword(true);
handleClose();
setPassword('');
setConfirmPassword('');
setCurrentPassword('');
} 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); setLoading(false);
setShowPasswordAlert(true); }
setChangePassword(true);
handleClose();
setPassword('');
setConfirmPassword('');
setCurrentPassword('');
}, 1500);
}; };
return ( return (

View File

@@ -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')}
> >
<Box {isLoading ? (
sx={{ <Box
py: 2, sx={{
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': { <CircularProgress />
borderBottom: 'none', </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>
<Box sx={{ color: 'divider', borderBottom: '1px solid' }} /> {isXsup && (
<Box sx={{ color: 'divider', borderBottom: '1px solid' }} />
)}
</React.Fragment> </React.Fragment>
))} ))}
</Box> </Box>
</Box> )}
</CardContainer> </CardContainer>
</PageWrapper> </PageWrapper>
); );

View File

@@ -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'
} }
/> />