diff --git a/src/features/profile/components/security/PasswordSecurity.tsx b/src/features/profile/components/security/PasswordSecurity.tsx index 060c08d..072e210 100644 --- a/src/features/profile/components/security/PasswordSecurity.tsx +++ b/src/features/profile/components/security/PasswordSecurity.tsx @@ -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 ( diff --git a/src/features/profile/components/security/RecentLogins.tsx b/src/features/profile/components/security/RecentLogins.tsx index 3e83f34..44c5edd 100644 --- a/src/features/profile/components/security/RecentLogins.tsx +++ b/src/features/profile/components/security/RecentLogins.tsx @@ -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([]); + const [isLoading, setIsLoading] = useState(true); + const [fetchError, setFetchError] = useState(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( + '/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 ( @@ -29,30 +119,33 @@ export function RecentLogins() { title={t('securityForm.recentLogins')} subtitle={t('securityForm.description')} > - + {isLoading ? ( + + + + ) : fetchError ? ( + + {fetchError} + + ) : ( - {data.map((d) => ( - + {logs.map((log, index) => ( + - {d.time} + {formatLoginDate(log.loginDateTime, i18n.language, t)} - {d.device} + {`${log.deviceOs} ${log.deviceName}`} - {d.ip} + {log.ipAddress} - - {d.current && ( - - )} - - + {isXsup && ( + + )} ))} - + )} ); diff --git a/src/features/profile/components/setting/Setting.tsx b/src/features/profile/components/setting/Setting.tsx index af2fc3f..41144e9 100644 --- a/src/features/profile/components/setting/Setting.tsx +++ b/src/features/profile/components/setting/Setting.tsx @@ -75,13 +75,16 @@ export function Setting() { const [isFetching, setIsFetching] = useState(true); const [fetchError, setFetchError] = useState(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' } />