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 (