diff --git a/src/components/layout/AdminLayout.tsx b/src/components/layout/AdminLayout.tsx index 0f6148b..f8b4bd2 100644 --- a/src/components/layout/AdminLayout.tsx +++ b/src/components/layout/AdminLayout.tsx @@ -6,6 +6,7 @@ import winsooLogo from "@/assets/images/winsoo.svg"; import { getSidebarMenuItems } from "@/constants/navigation"; import { colors } from "@/design/tokens/colors"; import { spacing } from "@/design/tokens/spacing"; +import LogoutButton from "@/features/auth/LogoutButton"; const { Header, Content, Sider } = Layout; const { Title } = Typography; @@ -137,6 +138,8 @@ const AdminLayout = ({ children }: AdminLayoutProps) => { پنل ادمین وینسو + + diff --git a/src/features/auth/LogoutButton.tsx b/src/features/auth/LogoutButton.tsx new file mode 100644 index 0000000..ea9582d --- /dev/null +++ b/src/features/auth/LogoutButton.tsx @@ -0,0 +1,91 @@ +import { Button, Modal, Typography } from 'antd'; +import { LogOut } from 'lucide'; +import { useState } from 'react'; +import LucideIcon from '@/components/ui/LucideIcon'; +import { colors } from '@/design/tokens/colors'; +import { spacing } from '@/design/tokens/spacing'; +import { useLogout } from '@/features/auth/useLogout'; + +const { Title, Paragraph } = Typography; + +const LogoutButton = () => { + const { logout, isLoggingOut } = useLogout(); + const [isModalOpen, setIsModalOpen] = useState(false); + + const handleConfirm = () => { + void logout(); + }; + + return ( + <> + + + setIsModalOpen(false)} + centered + footer={null} + width={380} + mask={{ closable: !isLoggingOut }} + closable={!isLoggingOut} + > +
+
+ +
+ + + خروج از حساب کاربری + + + آیا مطمئن هستید که می‌خواهید از پنل ادمین خارج شوید؟ + + +
+ + +
+
+
+ + ); +}; + +export default LogoutButton; diff --git a/src/features/auth/authService.ts b/src/features/auth/authService.ts index 1be9093..65839d1 100644 --- a/src/features/auth/authService.ts +++ b/src/features/auth/authService.ts @@ -19,4 +19,8 @@ export const authService = { }, }; }, + + logout: async (): Promise => { + await authApi.logout(); + }, }; diff --git a/src/features/auth/useLogout.ts b/src/features/auth/useLogout.ts new file mode 100644 index 0000000..6693b1b --- /dev/null +++ b/src/features/auth/useLogout.ts @@ -0,0 +1,25 @@ +import { useCallback, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useAppDispatch } from '@/utils/hooks'; +import { authService } from '@/features/auth/authService'; +import { logout as logoutAction } from '@/features/auth/authSlice'; + +export const useLogout = () => { + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + const [isLoggingOut, setIsLoggingOut] = useState(false); + + const logout = useCallback(async () => { + setIsLoggingOut(true); + + try { + await authService.logout(); + } finally { + dispatch(logoutAction()); + setIsLoggingOut(false); + navigate('/login', { replace: true }); + } + }, [dispatch, navigate]); + + return { logout, isLoggingOut }; +}; diff --git a/src/services/authApi.ts b/src/services/authApi.ts index ff31db4..418cfc5 100644 --- a/src/services/authApi.ts +++ b/src/services/authApi.ts @@ -1,7 +1,7 @@ import axios from 'axios'; import { AUTH_SCOPE } from '@/constants/auth'; import { createBasicAuthHeader } from '@/config/env'; -import { clearAuthTokens, storeAuthTokens } from '@/utils/authToken'; +import { clearAuthTokens, getAccessToken, storeAuthTokens } from '@/utils/authToken'; import { normalizePhoneNumber, toE164PhoneNumber } from '@/utils/phone'; import { buildAuthUrl } from '@/utils/authUrl'; import type { RequestOtpPayload, RequestOtpResponse, TokenResponse } from '@/types/auth'; @@ -62,7 +62,30 @@ export const authApi = { return response.data; }, - logout: () => { + revokeToken: async (token: string): Promise => { + const params = new URLSearchParams(); + params.append('token', token); + params.append('token_type_hint', 'access_token'); + + await axios.post(buildAuthUrl('/oauth2/revoke_token/'), params, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: createBasicAuthHeader(), + }, + }); + }, + + logout: async (): Promise => { + const accessToken = getAccessToken(); + + if (accessToken) { + try { + await authApi.revokeToken(accessToken); + } catch { + // Token may already be expired/invalid server-side; still clear the local session. + } + } + clearAuthTokens(); }, };