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 (
+ <>
+ }
+ onClick={() => setIsModalOpen(true)}
+ style={{
+ display: 'flex',
+ alignItems: 'center',
+ gap: spacing.xs,
+ height: 40,
+ paddingInline: spacing.md,
+ borderRadius: 8,
+ background: 'rgba(255,255,255,0.15)',
+ color: colors.background,
+ }}
+ >
+ خروج
+
+
+ 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();
},
};