Compare commits

..

No commits in common. "develop" and "main" have entirely different histories.

31 changed files with 125 additions and 309 deletions

View file

@ -1,13 +1,19 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { Outlet, useLocation, useNavigate } from 'react-router-dom'; import { Outlet, useLocation, useNavigate } from 'react-router-dom';
import { useAppSelector } from '@/utils/hooks'; import { useAppDispatch, useAppSelector } from './utils/hooks';
import AdminLayout from '@/components/layout/AdminLayout'; import { hydrateAuth } from './features/auth/authSlice';
import AdminLayout from './components/layout/AdminLayout';
function App() { function App() {
const dispatch = useAppDispatch();
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const isAuthenticated = useAppSelector((state) => state.auth.isAuthenticated); const isAuthenticated = useAppSelector((state) => state.auth.isAuthenticated);
useEffect(() => {
dispatch(hydrateAuth());
}, [dispatch]);
useEffect(() => { useEffect(() => {
if (!isAuthenticated && location.pathname !== '/login' && location.pathname !== '/verify-otp') { if (!isAuthenticated && location.pathname !== '/login' && location.pathname !== '/verify-otp') {
navigate('/login', { replace: true }); navigate('/login', { replace: true });

View file

@ -1,4 +1,4 @@
import type { SendOtpResponse } from '@/features/auth/authTypes'; import type { SendOtpResponse } from '../features/auth/authTypes';
export const authApi = { export const authApi = {
sendOtp: async (phoneNumber: string): Promise<SendOtpResponse> => { sendOtp: async (phoneNumber: string): Promise<SendOtpResponse> => {

View file

@ -1,6 +1,6 @@
import axios, { type AxiosError } from 'axios'; import axios, { type AxiosError } from 'axios';
import { env } from '@/config/env'; import { env } from '../config/env';
import { STORAGE_KEYS } from '@/utils/storage'; import { STORAGE_KEYS } from '../utils/storage';
const api = axios.create({ const api = axios.create({
baseURL: env.apiBaseUrl, baseURL: env.apiBaseUrl,

View file

@ -1,5 +1,5 @@
import { configureStore } from '@reduxjs/toolkit'; import { configureStore } from '@reduxjs/toolkit';
import authReducer from '@/features/auth/authSlice.ts'; import authReducer from '../features/auth/authSlice';
export const store = configureStore({ export const store = configureStore({
reducer: { reducer: {

View file

@ -1,6 +1,6 @@
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { Navigate, useLocation } from 'react-router-dom'; import { Navigate, useLocation } from 'react-router-dom';
import { useAppSelector } from '@/utils/hooks'; import { useAppSelector } from '../utils/hooks';
interface ProtectedRouteProps { interface ProtectedRouteProps {
children: ReactNode; children: ReactNode;

View file

@ -2,11 +2,10 @@ import { Drawer, Layout, Menu, Typography, theme } from "antd";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { useNavigate, useLocation } from "react-router-dom"; import { useNavigate, useLocation } from "react-router-dom";
import winsooLogo from "@/assets/images/winsoo.svg"; import winsooLogo from "../../assets/images/winsoo.svg";
import { getSidebarMenuItems } from "@/constants/navigation"; import { getSidebarMenuItems } from "../../constants/navigation";
import { colors } from "@/design/tokens/colors"; import { colors } from "../../design/tokens/colors";
import { spacing } from "@/design/tokens/spacing"; import { spacing } from "../../design/tokens/spacing";
import LogoutButton from "@/features/auth/LogoutButton";
const { Header, Content, Sider } = Layout; const { Header, Content, Sider } = Layout;
const { Title } = Typography; const { Title } = Typography;
@ -138,8 +137,6 @@ const AdminLayout = ({ children }: AdminLayoutProps) => {
<Title level={4} style={{ margin: 0, color: colors.background, flex: 1, textAlign: "center" }}> <Title level={4} style={{ margin: 0, color: colors.background, flex: 1, textAlign: "center" }}>
پنل ادمین وینسو پنل ادمین وینسو
</Title> </Title>
<LogoutButton />
</Header> </Header>
<Content style={{ margin: 24 }}> <Content style={{ margin: 24 }}>

View file

@ -1,8 +1,8 @@
import { Menu } from 'antd'; import { Menu } from 'antd';
import { useNavigate, useLocation } from 'react-router-dom'; import { useNavigate, useLocation } from 'react-router-dom';
import { getSidebarMenuItems } from '@/constants/navigation'; import { getSidebarMenuItems } from '../../constants/navigation';
import { colors } from '@/design/tokens/colors'; import { colors } from '../../design/tokens/colors';
import { spacing } from '@/design/tokens/spacing'; import { spacing } from '../../design/tokens/spacing';
const Sidebar = () => { const Sidebar = () => {
const navigate = useNavigate(); const navigate = useNavigate();

View file

@ -1,9 +1,9 @@
import { Home, MonitorPlay, UserRound } from "lucide"; import { Home, MonitorPlay, UserRound } from 'lucide';
import type { ReactNode } from "react"; import type { ReactNode } from 'react';
import type { MenuProps } from "antd"; import type { MenuProps } from 'antd';
import LucideIcon from "@/components/ui/LucideIcon"; import LucideIcon from '../components/ui/LucideIcon';
export type SidebarItemKey = "/profile" | "/billboards" | "/"; export type SidebarItemKey = '/profile' | '/billboards' | '/';
export interface SidebarItem { export interface SidebarItem {
key: SidebarItemKey; key: SidebarItemKey;
@ -13,40 +13,25 @@ export interface SidebarItem {
export const sidebarItems: SidebarItem[] = [ export const sidebarItems: SidebarItem[] = [
{ {
key: "/", key: '/profile',
label: "داشبورد", label: 'پروفایل کاربر',
icon: <LucideIcon icon={Home} size={18} />,
},
{
key: "/profile",
label: "پروفایل کاربر",
icon: <LucideIcon icon={UserRound} size={18} />, icon: <LucideIcon icon={UserRound} size={18} />,
}, },
{ {
key: "/billboards", key: '/billboards',
label: "بیلبوردها", label: 'بیلبوردها',
icon: <LucideIcon icon={MonitorPlay} size={18} />, icon: <LucideIcon icon={MonitorPlay} size={18} />,
}, },
{
key: '/',
label: 'داشبورد',
icon: <LucideIcon icon={Home} size={18} />,
},
]; ];
export const getSidebarMenuItems = (): NonNullable<MenuProps["items"]> => export const getSidebarMenuItems = (): NonNullable<MenuProps['items']> =>
sidebarItems.map((item) => ({ sidebarItems.map((item) => ({
key: item.key, key: item.key,
label: ( label: <span style={{ display: 'inline-flex', alignItems: 'center', gap: 2 }}>{item.icon}<span>{item.label}</span></span>,
<span style={{ display: "inline-flex", alignItems: "center", gap: 2 }}> icon: <span style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>{item.icon}</span>,
{item.icon}
<span>{item.label}</span>
</span>
),
icon: (
<span
style={{
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
}}
>
{item.icon}
</span>
),
})); }));

View file

@ -1,6 +1,6 @@
import type { ThemeConfig } from 'antd'; import type { ThemeConfig } from 'antd';
import { colors } from '@/design/tokens/colors'; import { colors } from '../tokens/colors';
import { typography } from '@/design/tokens/typography'; import { typography } from '../tokens/typography';
export const antdTheme: ThemeConfig = { export const antdTheme: ThemeConfig = {
token: { token: {

View file

@ -1,3 +1,3 @@
export { colors } from '@/design/tokens/colors'; export { colors } from './colors';
export { spacing } from '@/design/tokens/spacing'; export { spacing } from './spacing';
export { typography } from '@/design/tokens/typography'; export { typography } from './typography';

View file

@ -1,90 +0,0 @@
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 (
<>
<Button
type="text"
icon={<LucideIcon icon={LogOut} size={16} />}
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,
}}
>
خروج
</Button>
<Modal
open={isModalOpen}
onCancel={() => setIsModalOpen(false)}
centered
footer={null}
width={380}
mask={{ closable: !isLoggingOut }}
closable={!isLoggingOut}
>
<div style={{ textAlign: 'center', padding: `${spacing.sm}px 0` }}>
<div
style={{
width: 56,
height: 56,
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
margin: `0 auto ${spacing.md}px`,
}}
>
<LucideIcon icon={LogOut} size={24} color={colors.accent} />
</div>
<Title level={4} style={{ marginBottom: spacing.md, color: colors.text }}>
خروج از حساب کاربری
</Title>
<Paragraph style={{ color: colors.textSecondary, marginTop: spacing.xs, marginBottom: 0 }}>
آیا مطمئن هستید که می‌خواهید از پنل ادمین خارج شوید؟
</Paragraph>
<div style={{ display: 'flex', gap: spacing.sm, marginTop: spacing.lg }}>
<Button block disabled={isLoggingOut} onClick={() => setIsModalOpen(false)}>
انصراف
</Button>
<Button
block
type="primary"
loading={isLoggingOut}
onClick={handleConfirm}
style={{ background: colors.accent, borderColor: colors.accent, boxShadow: 'none' }}
>
خروج
</Button>
</div>
</div>
</Modal>
</>
);
};
export default LogoutButton;

View file

@ -1,5 +1,5 @@
import { authApi } from '@/services/authApi'; import { authApi } from '../../services/authApi';
import type { AuthSessionPayload, RequestOtpResponse } from '@/types/auth'; import type { AuthSessionPayload, RequestOtpResponse } from '../../types/auth';
export const authService = { export const authService = {
sendOtp: async (phoneNumber: string): Promise<RequestOtpResponse> => { sendOtp: async (phoneNumber: string): Promise<RequestOtpResponse> => {
@ -19,8 +19,4 @@ export const authService = {
}, },
}; };
}, },
logout: async (): Promise<void> => {
await authApi.logout();
},
}; };

View file

@ -1,42 +1,17 @@
import { createSlice, type PayloadAction } from '@reduxjs/toolkit'; import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
import type { AuthState, LoginSuccessPayload } from '@/features/auth/authTypes'; import type { AuthState, LoginSuccessPayload } from './authTypes';
import { STORAGE_KEYS } from '@/utils/storage'; import { STORAGE_KEYS } from '../../utils/storage';
import { clearAuthTokens, getAccessToken, isAccessTokenExpired, storeAuthTokens } from '@/utils/authToken'; import { getAccessToken, storeAuthTokens } from '../../utils/authToken';
const clearPersistedAuthState = () => { const initialState: AuthState = {
localStorage.removeItem(STORAGE_KEYS.accessToken); user: null,
localStorage.removeItem(STORAGE_KEYS.user); accessToken: null,
localStorage.removeItem(STORAGE_KEYS.authState); isAuthenticated: false,
clearAuthTokens();
};
const loadPersistedAuthState = (): AuthState => {
const loggedOutState: AuthState = { user: null, accessToken: null, isAuthenticated: false };
const storedAuth = localStorage.getItem(STORAGE_KEYS.authState);
const storedAccessToken = getAccessToken();
if (!storedAuth) {
return loggedOutState;
}
try {
const parsed = JSON.parse(storedAuth) as AuthState;
const accessToken = parsed.accessToken || storedAccessToken;
if (accessToken && parsed.user && !isAccessTokenExpired()) {
return { user: parsed.user, accessToken, isAuthenticated: true };
}
} catch {
// fall through to the logged-out default below
}
clearPersistedAuthState();
return loggedOutState;
}; };
const authSlice = createSlice({ const authSlice = createSlice({
name: 'auth', name: 'auth',
initialState: loadPersistedAuthState(), initialState,
reducers: { reducers: {
loginSuccess: (state, action: PayloadAction<LoginSuccessPayload>) => { loginSuccess: (state, action: PayloadAction<LoginSuccessPayload>) => {
state.user = action.payload.user; state.user = action.payload.user;
@ -63,9 +38,32 @@ const authSlice = createSlice({
state.accessToken = null; state.accessToken = null;
state.isAuthenticated = false; state.isAuthenticated = false;
clearPersistedAuthState(); localStorage.removeItem(STORAGE_KEYS.accessToken);
localStorage.removeItem(STORAGE_KEYS.user);
localStorage.removeItem(STORAGE_KEYS.authState);
},
hydrateAuth: (state) => {
const storedAuth = localStorage.getItem(STORAGE_KEYS.authState);
const storedAccessToken = getAccessToken();
if (!storedAuth) {
return;
}
try {
const parsed = JSON.parse(storedAuth) as AuthState;
if ((parsed.accessToken || storedAccessToken) && parsed.user) {
state.user = parsed.user;
state.accessToken = parsed.accessToken || storedAccessToken;
state.isAuthenticated = true;
}
} catch {
state.user = null;
state.accessToken = null;
state.isAuthenticated = false;
}
}, },
hydrateAuth: () => loadPersistedAuthState(),
}, },
}); });

View file

@ -1,25 +0,0 @@
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 };
};

View file

@ -2,21 +2,19 @@ import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { RouterProvider } from 'react-router-dom'; import { RouterProvider } from 'react-router-dom';
import { App as AntdApp, ConfigProvider } from 'antd'; import { ConfigProvider } from 'antd';
import 'antd/dist/reset.css'; import 'antd/dist/reset.css';
import faIR from 'antd/locale/fa_IR'; import faIR from 'antd/locale/fa_IR';
import { store } from '@/app/store'; import { store } from './app/store';
import { router } from '@/routes/router'; import { router } from './routes/router';
import { antdTheme } from '@/design/theme/antdTheme'; import { antdTheme } from './design/theme/antdTheme';
import './index.css'; import './index.css';
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>
<Provider store={store}> <Provider store={store}>
<ConfigProvider direction="rtl" locale={faIR} theme={antdTheme}> <ConfigProvider direction="rtl" locale={faIR} theme={antdTheme}>
<AntdApp>
<RouterProvider router={router} /> <RouterProvider router={router} />
</AntdApp>
</ConfigProvider> </ConfigProvider>
</Provider> </Provider>
</StrictMode>, </StrictMode>,

View file

@ -1,6 +1,6 @@
import { Card, Typography } from 'antd'; import { Card, Typography } from 'antd';
import { colors } from '@/design/tokens/colors'; import { colors } from '../../design/tokens/colors';
import { spacing } from '@/design/tokens/spacing'; import { spacing } from '../../design/tokens/spacing';
const { Title, Paragraph } = Typography; const { Title, Paragraph } = Typography;

View file

@ -1,6 +1,6 @@
import { Card, Typography } from 'antd'; import { Card, Typography } from 'antd';
import { colors } from '@/design/tokens/colors'; import { colors } from '../../design/tokens/colors';
import { spacing } from '@/design/tokens/spacing'; import { spacing } from '../../design/tokens/spacing';
const { Title, Paragraph } = Typography; const { Title, Paragraph } = Typography;

View file

@ -1,8 +1,8 @@
import { App, Button, Card, Form, Input, Typography } from 'antd'; import { Button, Card, Form, Input, Typography, message } from 'antd';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { authService } from '@/features/auth/authService'; import { authService } from '../../features/auth/authService';
import { colors } from '@/design/tokens/colors'; import { colors } from '../../design/tokens/colors';
import { spacing } from '@/design/tokens/spacing'; import { spacing } from '../../design/tokens/spacing';
const { Title, Paragraph } = Typography; const { Title, Paragraph } = Typography;
@ -13,7 +13,6 @@ interface LoginFormValues {
const Login = () => { const Login = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const [form] = Form.useForm<LoginFormValues>(); const [form] = Form.useForm<LoginFormValues>();
const { message } = App.useApp();
const onFinish = async (values: LoginFormValues) => { const onFinish = async (values: LoginFormValues) => {
try { try {

View file

@ -1,6 +1,6 @@
import { Card, Typography } from 'antd'; import { Card, Typography } from 'antd';
import { colors } from '@/design/tokens/colors'; import { colors } from '../../design/tokens/colors';
import { spacing } from '@/design/tokens/spacing'; import { spacing } from '../../design/tokens/spacing';
const { Title, Paragraph } = Typography; const { Title, Paragraph } = Typography;

View file

@ -1,11 +1,11 @@
import { App, Button, Card, Form, Input, Typography, type InputRef } from 'antd'; import { Button, Card, Form, Input, Typography, message, type InputRef } from 'antd';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useAppDispatch } from '@/utils/hooks'; import { useAppDispatch } from '../../utils/hooks';
import { loginSuccess } from '@/features/auth/authSlice'; import { loginSuccess } from '../../features/auth/authSlice';
import { authService } from '@/features/auth/authService'; import { authService } from '../../features/auth/authService';
import { colors } from '@/design/tokens/colors'; import { colors } from '../../design/tokens/colors';
import { spacing } from '@/design/tokens/spacing'; import { spacing } from '../../design/tokens/spacing';
const { Title, Paragraph } = Typography; const { Title, Paragraph } = Typography;
@ -19,7 +19,6 @@ const VerifyOtp = () => {
const [phoneNumber] = useState(() => localStorage.getItem('pending_phone_number') ?? ''); const [phoneNumber] = useState(() => localStorage.getItem('pending_phone_number') ?? '');
const [form] = Form.useForm<VerifyOtpFormValues>(); const [form] = Form.useForm<VerifyOtpFormValues>();
const otpInputRef = useRef<InputRef>(null); const otpInputRef = useRef<InputRef>(null);
const { message } = App.useApp();
useEffect(() => { useEffect(() => {
const timeoutId = window.setTimeout(() => { const timeoutId = window.setTimeout(() => {

View file

@ -1,11 +1,11 @@
import { createBrowserRouter, Navigate } from 'react-router-dom'; import { createBrowserRouter, Navigate } from 'react-router-dom';
import App from '@/App'; import App from '../App';
import Login from '@/pages/Login/Login'; import Login from '../pages/Login/Login';
import VerifyOtp from '@/pages/VerifyOtp/VerifyOtp'; import VerifyOtp from '../pages/VerifyOtp/VerifyOtp';
import Home from '@/pages/Home/Home'; import Home from '../pages/Home/Home';
import Profile from '@/pages/Profile/Profile'; import Profile from '../pages/Profile/Profile';
import Billboards from '@/pages/Billboards/Billboards'; import Billboards from '../pages/Billboards/Billboards';
import ProtectedRoute from '@/components/ProtectedRoute'; import ProtectedRoute from '../components/ProtectedRoute';
export const router = createBrowserRouter([ export const router = createBrowserRouter([
{ {

View file

@ -1,11 +1,11 @@
import axios, { type AxiosError, type InternalAxiosRequestConfig } from 'axios'; import axios, { type AxiosError, type InternalAxiosRequestConfig } from 'axios';
import { AUTH_SCOPE } from '@/constants/auth'; import { AUTH_SCOPE } from '../constants/auth';
import { env } from '@/config/env'; import { env } from '../config/env';
import { getAccessToken, getRefreshToken, storeAuthTokens, clearAuthTokens } from '@/utils/authToken'; import { getAccessToken, getRefreshToken, storeAuthTokens, clearAuthTokens } from '../utils/authToken';
import { logout } from '@/utils/logout'; import { logout } from '../utils/logout';
import { buildAuthUrl } from '@/utils/authUrl'; import { buildAuthUrl } from '../utils/authUrl';
import { createBasicAuthHeader } from '@/config/env'; import { createBasicAuthHeader } from '../config/env';
import type { TokenResponse } from '@/types/auth'; import type { TokenResponse } from '../types/auth';
const apiBaseUrl = env.apiBaseUrl; const apiBaseUrl = env.apiBaseUrl;

View file

@ -1,10 +1,10 @@
import axios from 'axios'; import axios from 'axios';
import { AUTH_SCOPE } from '@/constants/auth'; import { AUTH_SCOPE } from '../constants/auth';
import { createBasicAuthHeader } from '@/config/env'; import { createBasicAuthHeader } from '../config/env';
import { clearAuthTokens, getAccessToken, storeAuthTokens } from '@/utils/authToken'; import { clearAuthTokens, storeAuthTokens } from '../utils/authToken';
import { normalizePhoneNumber, toE164PhoneNumber } from '@/utils/phone'; import { normalizePhoneNumber, toE164PhoneNumber } from '../utils/phone';
import { buildAuthUrl } from '@/utils/authUrl'; import { buildAuthUrl } from '../utils/authUrl';
import type { RequestOtpPayload, RequestOtpResponse, TokenResponse } from '@/types/auth'; import type { RequestOtpPayload, RequestOtpResponse, TokenResponse } from '../types/auth';
export const authApi = { export const authApi = {
requestOtp: async (phoneNumber: string): Promise<RequestOtpResponse> => { requestOtp: async (phoneNumber: string): Promise<RequestOtpResponse> => {
@ -36,7 +36,6 @@ export const authApi = {
storeAuthTokens({ storeAuthTokens({
access_token: response.data.access_token, access_token: response.data.access_token,
refresh_token: response.data.refresh_token, refresh_token: response.data.refresh_token,
expires_in: response.data.expires_in,
}); });
return response.data; return response.data;
@ -58,36 +57,12 @@ export const authApi = {
storeAuthTokens({ storeAuthTokens({
access_token: response.data.access_token, access_token: response.data.access_token,
refresh_token: response.data.refresh_token, refresh_token: response.data.refresh_token,
expires_in: response.data.expires_in,
}); });
return response.data; return response.data;
}, },
revokeToken: async (token: string): Promise<void> => { logout: () => {
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<void> => {
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(); clearAuthTokens();
}, },
}; };

View file

@ -1 +1 @@
export * from '@/features/auth/authTypes'; export * from '../features/auth/authTypes';

View file

@ -1,5 +1,5 @@
import type { AxiosError } from 'axios'; import type { AxiosError } from 'axios';
import api from '@/services/api'; import api from '../services/api';
export const attachAuthInterceptor = () => { export const attachAuthInterceptor = () => {
api.interceptors.response.use( api.interceptors.response.use(

View file

@ -1,6 +1,5 @@
const ACCESS_TOKEN_KEY = 'access_token'; const ACCESS_TOKEN_KEY = 'access_token';
const REFRESH_TOKEN_KEY = 'refresh_token'; const REFRESH_TOKEN_KEY = 'refresh_token';
const EXPIRES_AT_KEY = 'access_token_expires_at';
export const getAccessToken = (): string | null => { export const getAccessToken = (): string | null => {
return localStorage.getItem(ACCESS_TOKEN_KEY); return localStorage.getItem(ACCESS_TOKEN_KEY);
@ -10,22 +9,12 @@ export const getRefreshToken = (): string | null => {
return localStorage.getItem(REFRESH_TOKEN_KEY); return localStorage.getItem(REFRESH_TOKEN_KEY);
}; };
export const storeAuthTokens = (tokens: { access_token: string; refresh_token: string; expires_in?: number }) => { export const storeAuthTokens = (tokens: { access_token: string; refresh_token: string }) => {
localStorage.setItem(ACCESS_TOKEN_KEY, tokens.access_token); localStorage.setItem(ACCESS_TOKEN_KEY, tokens.access_token);
localStorage.setItem(REFRESH_TOKEN_KEY, tokens.refresh_token); localStorage.setItem(REFRESH_TOKEN_KEY, tokens.refresh_token);
if (tokens.expires_in) {
localStorage.setItem(EXPIRES_AT_KEY, String(Date.now() + tokens.expires_in * 1000));
}
};
export const isAccessTokenExpired = (): boolean => {
const expiresAt = localStorage.getItem(EXPIRES_AT_KEY);
return expiresAt !== null && Date.now() >= Number(expiresAt);
}; };
export const clearAuthTokens = () => { export const clearAuthTokens = () => {
localStorage.removeItem(ACCESS_TOKEN_KEY); localStorage.removeItem(ACCESS_TOKEN_KEY);
localStorage.removeItem(REFRESH_TOKEN_KEY); localStorage.removeItem(REFRESH_TOKEN_KEY);
localStorage.removeItem(EXPIRES_AT_KEY);
}; };

View file

@ -1,4 +1,4 @@
import { env } from '@/config/env'; import { env } from '../config/env';
const normalizeBaseUrl = (value: string) => value.trim().replace(/\/+$/, ''); const normalizeBaseUrl = (value: string) => value.trim().replace(/\/+$/, '');
const ensureLeadingSlash = (value: string) => (value.startsWith('/') ? value : `/${value}`); const ensureLeadingSlash = (value: string) => (value.startsWith('/') ? value : `/${value}`);

View file

@ -1,5 +1,5 @@
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import type { AppDispatch, RootState } from '@/app/store'; import type { AppDispatch, RootState } from '../app/store';
export const useAppDispatch = useDispatch.withTypes<AppDispatch>(); export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
export const useAppSelector = useSelector.withTypes<RootState>(); export const useAppSelector = useSelector.withTypes<RootState>();

View file

@ -1,4 +1,4 @@
import { clearAuthTokens } from '@/utils/authToken'; import { clearAuthTokens } from './authToken';
export const logout = () => { export const logout = () => {
clearAuthTokens(); clearAuthTokens();

View file

@ -16,11 +16,6 @@
"noEmit": true, "noEmit": true,
"jsx": "react-jsx", "jsx": "react-jsx",
/* Path aliases */
"paths": {
"@/*": ["./src/*"]
},
/* Linting */ /* Linting */
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,

View file

@ -1,4 +1,3 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import react, { reactCompilerPreset } from '@vitejs/plugin-react' import react, { reactCompilerPreset } from '@vitejs/plugin-react'
import babel from '@rolldown/plugin-babel' import babel from '@rolldown/plugin-babel'
@ -9,9 +8,4 @@ export default defineConfig({
react(), react(),
babel({ presets: [reactCompilerPreset()] }) babel({ presets: [reactCompilerPreset()] })
], ],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
}) })