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 { Toast } from '@/components/Toast';
import { regex } from '@/utils/regex';
import apiClient from '@/lib/apiClient';
export function PasswordSecurity() {
const theme = useTheme();
@@ -53,17 +54,48 @@ export function PasswordSecurity() {
const handleOpen = () => setOpen(true);
const handleClose = () => setOpen(false);
const handleShowAlert = () => {
setLoading(true);
setTimeout(() => {
const handleShowAlert = async () => {
try {
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);
setShowPasswordAlert(true);
setChangePassword(true);
handleClose();
setPassword('');
setConfirmPassword('');
setCurrentPassword('');
}, 1500);
}
};
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 type { TFunction } from 'i18next';
import { CardContainer } from '@/components/CardContainer';
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() {
const { t } = useTranslation('setting');
const data = [
{
id: 0,
time: 'دقایقی پیش',
device: 'asus i5 24i',
ip: '192.168.1.1',
current: true,
},
{
id: 1,
time: '۲۲:۱۳ - ۱۴۰۴/۰۹/۰۹',
device: 'samsung s23 ultra',
ip: '192.220.1.1',
current: false,
},
];
const { t, i18n } = useTranslation('setting');
const token = localStorage.getItem('authToken');
const [logs, setLogs] = useState<LoginLog[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [fetchError, setFetchError] = useState<string | null>(null);
const theme = useTheme();
const isXsup = useMediaQuery(theme.breakpoints.up('xs'));
useEffect(() => {
const fetchLoginLogs = async () => {
setIsLoading(true);
setFetchError(null);
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 (
<PageWrapper>
@@ -29,30 +119,33 @@ export function RecentLogins() {
title={t('securityForm.recentLogins')}
subtitle={t('securityForm.description')}
>
<Box
sx={{
py: 2,
display: 'flex',
flexDirection: 'column',
gap: 2,
bgcolor: 'background.paper',
flex: 1,
borderBottom: '1px solid',
borderColor: 'divider',
'&:last-child': {
borderBottom: 'none',
},
}}
>
{isLoading ? (
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
p: 4,
minHeight: '200px',
}}
>
<CircularProgress />
</Box>
) : fetchError ? (
<Box sx={{ textAlign: 'center', p: 4 }}>
<Typography color="error.main">{fetchError}</Typography>
</Box>
) : (
<Box sx={{ px: 4 }}>
{data.map((d) => (
<React.Fragment key={d.id}>
{logs.map((log, index) => (
<React.Fragment key={index}>
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
alignItems: { xs: 'flex-start', sm: 'center' },
minHeight: 50,
py: 1.5,
}}
>
<Typography
@@ -61,10 +154,9 @@ export function RecentLogins() {
flexBasis: { xs: '100%', sm: 'auto' },
mb: { xs: 1, sm: 0 },
minWidth: { sm: '172.5px' },
order: { xs: 1, sm: 1 },
}}
>
{d.time}
{formatLoginDate(log.loginDateTime, i18n.language, t)}
</Typography>
<Typography
variant="body2"
@@ -72,10 +164,9 @@ export function RecentLogins() {
flexBasis: { xs: '100%', sm: 'auto' },
mb: { xs: 1, sm: 0 },
minWidth: { sm: '172.5px' },
order: { xs: 1, sm: 1 },
}}
>
{d.device}
{`${log.deviceOs} ${log.deviceName}`}
</Typography>
<Typography
variant="body2"
@@ -83,43 +174,18 @@ export function RecentLogins() {
flexBasis: { xs: '100%', sm: 'auto' },
mb: { xs: 1, sm: 0 },
minWidth: { sm: '172.5px' },
order: { xs: 1, sm: 1 },
}}
>
{d.ip}
{log.ipAddress}
</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 sx={{ color: 'divider', borderBottom: '1px solid' }} />
{isXsup && (
<Box sx={{ color: 'divider', borderBottom: '1px solid' }} />
)}
</React.Fragment>
))}
</Box>
</Box>
)}
</CardContainer>
</PageWrapper>
);

View File

@@ -75,13 +75,16 @@ export function Setting() {
const [isFetching, setIsFetching] = useState(true);
const [fetchError, setFetchError] = useState<string | null>(null);
const notLoggedIn = t('settings.notLoggedIn');
const failRetrieve = t('settings.failRetrieve');
const errorFetch = t('settings.errorFetch');
useEffect(() => {
const fetchUserSettings = async () => {
setIsFetching(true);
setFetchError(null);
if (!token) {
setIsFetching(false);
setFetchError(t('settings.notLoggedIn'));
setFetchError(notLoggedIn);
return;
}
try {
@@ -114,12 +117,13 @@ export function Setting() {
setSavedSettings(newSettings);
setDraftSettings(newSettings);
setMode(themeMode);
i18n.changeLanguage(languageCode);
} else {
throw new Error(res.data.message || t('settings.failRetrieve'));
throw new Error(res.data.message || failRetrieve);
}
} catch (e: unknown) {
let message = t('settings.errorFetch');
let message = errorFetch;
if (e instanceof Error) {
message = e.message;
}
@@ -128,8 +132,9 @@ export function Setting() {
setIsFetching(false);
}
};
fetchUserSettings();
}, [token, setMode, i18n, t]);
}, [token, setMode, i18n, notLoggedIn, failRetrieve, errorFetch]);
useEffect(() => {
if (isEditing) {
@@ -313,7 +318,7 @@ export function Setting() {
variant="Bold"
color={
savedSettings.theme === 'light'
? 'action.hover'
? 'text.primary'
: 'primary.main'
}
/>