From 958f00069f0093091b088567a8c61754fcf7858f Mon Sep 17 00:00:00 2001 From: Vladislav Date: Tue, 10 Jun 2025 17:05:35 +0500 Subject: [PATCH] test --- api/api/__main__.py | 3 +- api/api/config/default.py | 3 +- api/api/db/logic/auth.py | 3 +- api/api/endpoints/auth.py | 40 +++++++++-------- api/api/services/middleware.py | 58 ++++++++++++------------ client/package-lock.json | 25 +++++++++++ client/package.json | 1 + client/src/App.tsx | 3 +- client/src/api/api.ts | 70 ++++++++++++++++++++++++++--- client/src/pages/MainLayout.tsx | 19 ++++++++ client/src/pages/ProtectedRoute.tsx | 15 ++++++- client/src/store/user.ts | 2 + 12 files changed, 179 insertions(+), 63 deletions(-) diff --git a/api/api/__main__.py b/api/api/__main__.py index 6e57ec2..f00b342 100644 --- a/api/api/__main__.py +++ b/api/api/__main__.py @@ -73,6 +73,7 @@ if __name__ == "__main__": log_level="info", ) +app.add_middleware(MiddlewareAccessTokenValidadtion) app.add_middleware( CORSMiddleware, allow_origins=origins, @@ -80,5 +81,3 @@ app.add_middleware( allow_methods=["GET", "POST", "OPTIONS", "DELETE", "PUT"], allow_headers=["*"], ) - -app.add_middleware(MiddlewareAccessTokenValidadtion) diff --git a/api/api/config/default.py b/api/api/config/default.py index 8821f2e..c8b126e 100644 --- a/api/api/config/default.py +++ b/api/api/config/default.py @@ -18,7 +18,8 @@ class DbCredentialsSchema(BaseModel): class DefaultSettings(BaseSettings): ENV: str = environ.get("ENV", "local") PATH_PREFIX: str = environ.get("PATH_PREFIX", "/api/v1") - APP_HOST: str = environ.get("APP_HOST", "http://127.0.0.1") + # APP_HOST: str = environ.get("APP_HOST", "http://127.0.0.1") + APP_HOST: str = environ.get("APP_HOST", "http://localhost") APP_PORT: int = int(environ.get("APP_PORT", 8000)) APP_ID: uuid.UUID = environ.get("APP_ID", uuid.uuid4()) LOGS_STORAGE_PATH: str = environ.get("LOGS_STORAGE_PATH", "storage/logs") diff --git a/api/api/db/logic/auth.py b/api/api/db/logic/auth.py index 393598d..20e9573 100644 --- a/api/api/db/logic/auth.py +++ b/api/api/db/logic/auth.py @@ -50,13 +50,12 @@ async def get_user(connection: AsyncConnection, login: str) -> Optional[User]: return user, password -async def upgrade_old_refresh_token(connection: AsyncConnection, user, refresh_token) -> Optional[User]: +async def upgrade_old_refresh_token(connection: AsyncConnection, refresh_token) -> Optional[User]: new_status = KeyStatus.EXPIRED update_query = ( update(account_keyring_table) .where( - account_table.c.id == user.id, account_keyring_table.c.status == KeyStatus.ACTIVE, account_keyring_table.c.key_type == KeyType.REFRESH_TOKEN, account_keyring_table.c.key_value == refresh_token, diff --git a/api/api/endpoints/auth.py b/api/api/endpoints/auth.py index bd0cfe8..cc3f07f 100644 --- a/api/api/endpoints/auth.py +++ b/api/api/endpoints/auth.py @@ -1,5 +1,6 @@ from datetime import datetime, timedelta, timezone +import jwt from fastapi import ( APIRouter, Depends, @@ -8,7 +9,6 @@ from fastapi import ( Response, status, ) - from loguru import logger from fastapi_jwt_auth import AuthJWT @@ -30,11 +30,21 @@ api_router = APIRouter( ) +def get_login_from_jwt(token: str): + payload = jwt.decode( + token, + get_settings().SECRET_KEY, + algorithms=[get_settings().ALGORITHM], + ) + return payload.get("sub") + + class Settings(BaseModel): authjwt_secret_key: str = get_settings().SECRET_KEY # Configure application to store and get JWT from cookies authjwt_token_location: set = {"headers", "cookies"} authjwt_cookie_domain: str = get_settings().DOMAIN + authjwt_refresh_cookie_name: str = "refresh_token_cookie" # Only allow JWT cookies to be sent over https authjwt_cookie_secure: bool = get_settings().ENV == "prod" @@ -68,7 +78,8 @@ async def login_for_access_token( # headers={"WWW-Authenticate": "Bearer"}, ) - access_token_expires = timedelta(minutes=get_settings().ACCESS_TOKEN_EXPIRE_MINUTES) + # access_token_expires = timedelta(minutes=get_settings().ACCESS_TOKEN_EXPIRE_MINUTES) + access_token_expires = timedelta(seconds=5) refresh_token_expires = timedelta(days=get_settings().REFRESH_TOKEN_EXPIRE_DAYS) @@ -88,28 +99,19 @@ async def login_for_access_token( @api_router.post("/refresh", response_model=Access) async def refresh( - request: Request, connection: AsyncConnection = Depends(get_connection_dep), Authorize: AuthJWT = Depends() + request: Request, + connection: AsyncConnection = Depends(get_connection_dep), + Authorize: AuthJWT = Depends(), ): refresh_token = request.cookies.get("refresh_token_cookie") - # print("Refresh Token:", refresh_token) if not refresh_token: raise HTTPException(status_code=401, detail="Refresh token is missing") - - try: - Authorize.jwt_refresh_token_required() - current_user = Authorize.get_jwt_subject() - - except Exception as e: - await upgrade_old_refresh_token(connection, current_user, refresh_token) - - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid refresh token", - ) - - access_token_expires = timedelta(minutes=get_settings().ACCESS_TOKEN_EXPIRE_MINUTES) - + Authorize.jwt_refresh_token_required(refresh_token) + current_user = Authorize.get_jwt_subject() + # try: + # access_token_expires = timedelta(minutes=get_settings().ACCESS_TOKEN_EXPIRE_MINUTES) + access_token_expires = timedelta(seconds=5) new_access_token = Authorize.create_access_token(subject=current_user, expires_time=access_token_expires) return Access(access_token=new_access_token) diff --git a/api/api/services/middleware.py b/api/api/services/middleware.py index a44070d..215a5f2 100644 --- a/api/api/services/middleware.py +++ b/api/api/services/middleware.py @@ -22,40 +22,38 @@ class MiddlewareAccessTokenValidadtion(BaseHTTPMiddleware): self.excluded_routes = [ re.compile(r"^" + re.escape(self.prefix) + r"/auth/refresh/?$"), re.compile(r"^" + re.escape(self.prefix) + r"/auth/?$"), + re.compile(r"^" + r"/swagger"), + re.compile(r"^" + r"/openapi"), ] async def dispatch(self, request: Request, call_next): - if request.method in ["GET", "POST", "PUT", "DELETE"]: - if any(pattern.match(request.url.path) for pattern in self.excluded_routes): - return await call_next(request) - else: - auth_header = request.headers.get("Authorization") - if not auth_header: - return JSONResponse( - status_code=status.HTTP_401_UNAUTHORIZED, - content={"detail": "Missing authorization header."}, - headers={"WWW-Authenticate": "Bearer"}, - ) + if request.method not in ["GET", "POST", "PUT", "DELETE"]: + return JSONResponse( + status_code=status.HTTP_405_METHOD_NOT_ALLOWED, + content={"detail": "Method not allowed"}, + ) - token = auth_header.split(" ")[1] - Authorize = AuthJWT(request) + if any(pattern.match(request.url.path) for pattern in self.excluded_routes): + return await call_next(request) - try: - current_user = Authorize.get_jwt_subject() - request.state.current_user = current_user - return await call_next(request) + auth_header = request.headers.get("Authorization") + if not auth_header: + return JSONResponse( + status_code=status.HTTP_401_UNAUTHORIZED, + content={"detail": "Missing authorization header."}, + headers={"WWW-Authenticate": "Bearer"}, + ) - except Exception: - return JSONResponse( - status_code=status.HTTP_401_UNAUTHORIZED, - content={"detail": "The access token is invalid or expired."}, - headers={"WWW-Authenticate": "Bearer"}, - ) + try: + token = auth_header.split(" ")[1] + Authorize = AuthJWT(request) + current_user = Authorize.get_jwt_subject() + request.state.current_user = current_user + except Exception: + return JSONResponse( + status_code=status.HTTP_401_UNAUTHORIZED, + content={"detail": "The access token is invalid or expired."}, + headers={"WWW-Authenticate": "Bearer"}, + ) - # async with get_connection() as connection: - # authorize_user = await get_user_login(connection, current_user) - # print(authorize_user) - # if authorize_user is None : - # return JSONResponse( - # status_code=status.HTTP_404_NOT_FOUND , - # detail="User not found.") + return await call_next(request) diff --git a/client/package-lock.json b/client/package-lock.json index 3aedc28..4f0bc7c 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -19,6 +19,7 @@ "@types/react-dom": "^19.0.4", "antd": "^5.24.7", "axios": "^1.9.0", + "axios-retry": "^4.5.0", "i18next": "^25.0.1", "i18next-browser-languagedetector": "^8.0.5", "react": "^18.3.1", @@ -5275,6 +5276,18 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/axios-retry": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.5.0.tgz", + "integrity": "sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==", + "license": "Apache-2.0", + "dependencies": { + "is-retry-allowed": "^2.2.0" + }, + "peerDependencies": { + "axios": "0.x || 1.x" + } + }, "node_modules/axios/node_modules/form-data": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", @@ -10092,6 +10105,18 @@ "node": ">=0.10.0" } }, + "node_modules/is-retry-allowed": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", + "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-root": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz", diff --git a/client/package.json b/client/package.json index 74e16cf..cc6e3a8 100644 --- a/client/package.json +++ b/client/package.json @@ -14,6 +14,7 @@ "@types/react-dom": "^19.0.4", "antd": "^5.24.7", "axios": "^1.9.0", + "axios-retry": "^4.5.0", "i18next": "^25.0.1", "i18next-browser-languagedetector": "^8.0.5", "react": "^18.3.1", diff --git a/client/src/App.tsx b/client/src/App.tsx index 7fc96c3..3d98dae 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -2,12 +2,13 @@ import React from 'react'; import { Route, Routes } from 'react-router-dom'; import MainLayout from './pages/MainLayout'; import ProtectedRoute from './pages/ProtectedRoute'; +import LoginPage from './pages/LoginPage'; function App() { return (
- login
} /> + } /> }> }> diff --git a/client/src/api/api.ts b/client/src/api/api.ts index 0fee327..4ba6165 100644 --- a/client/src/api/api.ts +++ b/client/src/api/api.ts @@ -1,5 +1,8 @@ import axios from 'axios'; import { Access, Auth } from '../types/auth'; +import { User } from '../types/user'; +import { AuthService } from '../services/auth'; +import axiosRetry from 'axios-retry'; const baseURL = `${process.env.REACT_APP_HTTP_PROTOCOL}://${process.env.REACT_APP_API_URL}/api/v1`; @@ -11,20 +14,75 @@ const base = axios.create({ }, }); -// base.interceptors.request.use((config) => { -// const token = localStorage.getItem('accessToken'); -// if (token) { -// config.headers.Authorization = `Bearer ${token}`; -// } -// return config; +base.interceptors.request.use((config) => { + const token = localStorage.getItem('accessToken'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}); + +// axiosRetry(base, { +// retries: 3, +// retryDelay: (retryCount: number) => { +// console.log(`retry attempt: ${retryCount}`); +// return retryCount * 2000; +// }, +// retryCondition: async (error: any) => { +// if (error.code === 'ERR_CANCELED') { +// return true; +// } +// return false; +// }, // }); +base.interceptors.response.use( + (response) => { + return response; + }, + async function (error) { + console.log('error', error); + const originalRequest = error.response.config; + console.log('originalRequest._retry', originalRequest); + const urlTokens = error?.request?.responseURL.split('/'); + const url = urlTokens[urlTokens.length - 1]; + console.log('url', url); + if ( + error.response.status === 401 && + !(originalRequest?._retry != null) && + url !== 'login' && + url !== 'refresh' && + url !== 'logout' + ) { + originalRequest._retry = true; + const res = await AuthService.refresh().catch(async () => { + await AuthService.logout(); + }); + console.log('res', res); + return await base(originalRequest); + } + return await Promise.reject(error); + } +); + const api = { + // auth async login(auth: Auth): Promise { console.log(auth); const response = await base.post('/auth', auth); return response.data; }, + + async refreshToken(): Promise { + const response = await base.post('/auth/refresh'); + return response.data; + }, + + // user + async getProfile(): Promise { + const response = await base.get('/profile'); + return response.data; + }, }; export default api; diff --git a/client/src/pages/MainLayout.tsx b/client/src/pages/MainLayout.tsx index 2933a89..b6c75b3 100644 --- a/client/src/pages/MainLayout.tsx +++ b/client/src/pages/MainLayout.tsx @@ -9,6 +9,8 @@ import RunningProcessesPage from './RunningProcessesPage'; import AccountsPage from './AccountsPage'; import EventsListPage from './EventsListPage'; import ConfigurationPage from './ConfigurationPage'; +import { useSetUserSelector } from '../store/user'; +import { UserService } from '../services/user'; export default function MainLayout() { const navigate = useNavigate(); @@ -19,6 +21,8 @@ export default function MainLayout() { const [width, setWidth] = useState('15%'); const [collapsedWidth, setCollapsedWidth] = useState(50); + const setUser = useSetUserSelector() + const calculateWidths = () => { const windowWidth = window.innerWidth; const expanded = Math.min(Math.max(windowWidth * 0.15, 180), 240); @@ -54,6 +58,21 @@ export default function MainLayout() { navigate(key); } + useEffect(() => { + const token = localStorage.getItem('accessToken'); + if (!token) { + navigate('/login'); + } else { + if (localStorage.getItem('user')) { + setUser(JSON.parse(localStorage.getItem('user') as string)) + } else { + UserService.getProfile().then((user) => { + setUser(user); + }); + } + } + }, []) + return ( { + const user = useUserSelector(); + const navigate = useNavigate(); + + useEffect(() => { + if (user.id === null) { + navigate('/login'); + } + }, [user]); + return ; }; export default ProtectedRoute; diff --git a/client/src/store/user.ts b/client/src/store/user.ts index f9c9517..137d3e8 100644 --- a/client/src/store/user.ts +++ b/client/src/store/user.ts @@ -11,6 +11,7 @@ type UserStoreState = { type UserStoreActions = { setUser: (user: User) => void; + removeUser: () => void; }; type UserStore = UserStoreState & UserStoreActions; @@ -22,6 +23,7 @@ export const useUserStore = create()( user: userInfo != null ? JSON.parse(userInfo) : ({} as User), loading: false, setUser: (user: User) => set({ user }), + removeUser: () => set({ user: {} as User }), }), { name: 'userInfo' } )