from fastapi import HTTPException, Request, Response, Security from fastapi.security import SecurityScopes import jwt from typing import List, Any, Dict, Optional from datetime import timedelta from urllib.parse import urlencode from bson import ObjectId import httpx from db import * from utils import * from config import * import logging import secrets from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials bearer_scheme = HTTPBearer(auto_error=False) def get_token( creds: HTTPAuthorizationCredentials = Security(bearer_scheme), ): if not creds or creds.scheme.lower() != "bearer": raise HTTPException(status_code=401, detail="Unauthorized") if creds.credentials != ATOKEN: raise HTTPException(status_code=401, detail="Unauthorized") return creds.credentials def is_browser_request(request: Request) -> bool: accept = request.headers.get("accept", "") return "text/html" in accept async def token_create(user_id: str, url: str = '/profile'): raw_token = secrets.token_urlsafe(48) token_hash = hash_token(raw_token) created_at = datetime.now(timezone.utc) await users_token_collection.insert_one({ 'token_hash': token_hash, 'url': url, 'user_id': user_id, 'created_at': created_at }) return raw_token def get_bearer_token(request: Request) -> Optional[str]: auth = request.headers.get("Authorization") if auth and auth.lower().startswith("bearer "): parts = auth.split(" ", 1) if len(parts) == 2: return parts[1].strip() return None async def get_access_token(request: Request, response: Response) -> Optional[str]: token = request.cookies.get(ACCESS_COOKIE) if token: t = token.strip() if t: return t bearer = get_bearer_token(request) if bearer: return bearer return None def get_refresh_token(request: Request) -> Optional[str]: token = request.cookies.get(REFRESH_COOKIE) if token: t = token.strip() if t: return t return None async def validate_refresh_and_rotate(refresh_token: str, response: Response, request: Request) -> Optional[Dict[str, str]]: try: payload = jwt.decode(refresh_token, SECRET_KEY, algorithms=[ALGORITHM]) except jwt.InvalidTokenError: return None if payload.get("type") != "refresh": return None user_id_str = payload.get("sub") if not user_id_str: return None refresh_hash = hash_token(refresh_token) stored = await redis_client.get(f"refresh:{refresh_hash}") if not stored or stored != user_id_str: return None await redis_client.delete(f"refresh:{refresh_hash}") user = await users_collection.find_one({"_id": ObjectId(user_id_str)}, {"hashed_password": 0}) if not user: return None scopes = get_all_user_scopes(user.get("roles", []), user.get("extra_scopes", [])) client_id = payload.get("client_id") client_ip = payload.get("ip") access_exp = timedelta(days=ACCESS_TOKEN_EXPIRE_MINUTES) refresh_exp = timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS) new_access = create_jwt_token( {"sub": user_id_str, "scopes": scopes, "client_id": client_id, "ip": client_ip}, access_exp ) new_refresh = create_jwt_token( {"sub": user_id_str, "type": "refresh", "client_id": client_id, "ip": client_ip}, refresh_exp ) new_refresh_hash = hash_token(new_refresh) await redis_client.setex(f"access:{new_access}", int(access_exp.total_seconds()), user_id_str) await redis_client.setex(f"refresh:{new_refresh_hash}", int(refresh_exp.total_seconds()), user_id_str) session_key = f"user_sessions:{user_id_str}" await redis_client.sadd(session_key, f"access:{new_access}") await redis_client.sadd(session_key, f"refresh:{new_refresh_hash}") response.set_cookie( key="access_token", value=new_access, httponly=True, secure=PROD, samesite="Strict", max_age=int(access_exp.total_seconds()) ) response.set_cookie( key="refresh_token", value=new_refresh_hash, httponly=True, secure=PROD, samesite="Strict", max_age=int(refresh_exp.total_seconds()) ) user['scopes'] = scopes user['id'] = user_id_str user = bson_clear(user) return await _build_user(user, request) async def get_current_user( request: Request, response: Response, security_scopes: SecurityScopes ) -> Dict[str, Any]: token_old = request.cookies.get('token') or request.cookies.get('v2token') access = await get_access_token(request, response) if token_old and not access: try: headers = {'Authorization': f"Bearer {token_old}"} async with httpx.AsyncClient(http2=True, verify=ctx) as client: res = await client.get(f'{FULL_PATH_DOMAIN}/api/v1/auth/me', headers=headers) email = res.json()['data']['email'] user = await users_collection.find_one({ "$or": [ {"email": email}, {"email": email.lower()} ] }) except Exception as e: logging.error(e) raise HTTPException(status_code=307, headers={"Location": '/login'}, detail="User not found") user_id_str = str(user["_id"]) scopes = get_all_user_scopes(user.get("roles", []), user.get("extra_scopes", [])) for scope in security_scopes.scopes: if scope not in scopes: raise HTTPException(status_code=403, detail="Not enough permissions") client_ip = request.client.host access_exp_delta = timedelta(days=ACCESS_TOKEN_EXPIRE_MINUTES) refresh_exp_delta = timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS) access_token = create_jwt_token( {"sub": user_id_str, "scopes": scopes, "client_id": 'old_token', "ip": client_ip}, access_exp_delta ) refresh_token = create_jwt_token( {"sub": user_id_str, "type": "refresh", "client_id": 'old_token', "ip": client_ip}, refresh_exp_delta ) await redis_client.setex(f"access:{access_token}", int(access_exp_delta.total_seconds()), user_id_str) await redis_client.setex(f"refresh:{hash_token(refresh_token)}", int(refresh_exp_delta.total_seconds()), user_id_str) session_key = f"user_sessions:{user_id_str}" await redis_client.sadd(session_key, f"access:{access_token}") await redis_client.sadd(session_key, f"refresh:{hash_token(refresh_token)}") response.set_cookie( key="access_token", value=access_token, httponly=True, secure=PROD, samesite="Strict", max_age=int(access_exp_delta.total_seconds()) ) response.set_cookie( key="refresh_token", value=refresh_token, httponly=True, secure=PROD, samesite="Strict", max_age=int(refresh_exp_delta.total_seconds()) ) if user: user['scopes'] = scopes user['id'] = user_id_str user = bson_clear(user) return await _build_user(user, request) check, scopes = await validate_access_token(access, request, security_scopes) if access and check: return await get_user_details(access, scopes, response, request) refresh = get_refresh_token(request) if refresh: rotated = await validate_refresh_and_rotate(refresh, response, request) if rotated: return rotated if is_browser_request(request): next_url = str(request.url) login_url = "/login?" + urlencode({"next": next_url}) raise HTTPException(status_code=307, headers={"Location": login_url}, detail="Redirecting to login") raise HTTPException(status_code=401, detail="Not authenticated") async def validate_access_token(access: str, request: Request, security_scopes: SecurityScopes) -> bool: key = f"access:{access}" user_id_str = await redis_client.get(key) if user_id_str: try: payload = jwt.decode(access, SECRET_KEY, algorithms=[ALGORITHM]) if payload.get("sub") != user_id_str: raise HTTPException(status_code=401, detail="Token mismatch") # Validate scopes token_scopes: List[str] = payload.get("scopes", []) if request.cookies.get("admin_access_token"): token_scopes.append('back') for scope in security_scopes.scopes: if scope not in token_scopes: raise HTTPException(status_code=403, detail="Not enough permissions") return True, token_scopes except (jwt.ExpiredSignatureError, jwt.InvalidTokenError): return False, [] return False, [] async def get_user_details(access: str, scopes, response: Response, request: Request,) -> Dict[str, Any]: key = f"access:{access}" user_id_str = await redis_client.get(key) if user_id_str: user = await users_collection.find_one( {"_id": ObjectId(user_id_str)}, {"hashed_password": 0} ) if user: if user.get("blocked"): raise HTTPException(status_code=403, detail="Аккаунт заблокирован администратором") user['scopes'] = scopes user['id'] = user_id_str user = bson_clear(user) return await _build_user(user, request) raise HTTPException(status_code=307, headers={"Location": '/login'}, detail="User not found") async def _build_user(user, request: Request): await notify_collection.update_one({'link':str(request.url).replace(str(request.base_url),'/')},{'$set':{'read':False}}) if (read_id:=request.cookies.get('read')): print(read_id) user['notify'] = await notify_collection.find({'user_id':user['id']}).limit(128).to_list() return user async def get_user( request: Request, response: Response, ) -> Optional[Dict[str, Any]]: token = await get_access_token(request, response) if not token: return None key = f"access:{token}" user_id_str = await redis_client.get(key) if not user_id_str: return None try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) if payload.get("sub") != user_id_str: return None user = await users_collection.find_one({"_id": ObjectId(user_id_str)}, {"hashed_password": 0}) if not user: return None if user.get("blocked"): return None scopes = get_all_user_scopes(user.get("roles", []), user.get("extra_scopes", [])) if request.cookies.get("admin_access_token"): scopes.append('back') user['scopes'] = scopes user['id'] = user_id_str user = bson_clear(user) return await _build_user(user, request) except jwt.ExpiredSignatureError: return None except jwt.InvalidTokenError: return None async def issue_session(user_id: str, request: Request, response: Response, client_id: str = 'web', scopes=None): """Выдаёт новую сессию: JWT access/refresh, запись в Redis и cookie. Переиспользуемый хелпер (например, для входа/регистрации через VK). Если scopes не переданы — вычисляются из ролей пользователя. """ if scopes is None: user = await users_collection.find_one({"_id": ObjectId(user_id)}) scopes = get_all_user_scopes(user.get("roles", []), user.get("extra_scopes", [])) client_ip = request.headers.get('X-Real-IP') or (request.client.host if request.client else None) access_exp = timedelta(days=ACCESS_TOKEN_EXPIRE_MINUTES) refresh_exp = timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS) access_token = create_jwt_token( {"sub": user_id, "scopes": scopes, "client_id": client_id, "ip": client_ip}, access_exp, ) refresh_token = create_jwt_token( {"sub": user_id, "type": "refresh", "client_id": client_id, "ip": client_ip}, refresh_exp, ) await redis_client.setex(f"access:{access_token}", int(access_exp.total_seconds()), user_id) await redis_client.setex(f"refresh:{hash_token(refresh_token)}", int(refresh_exp.total_seconds()), user_id) session_key = f"user_sessions:{user_id}" await redis_client.sadd(session_key, f"access:{access_token}") await redis_client.sadd(session_key, f"refresh:{hash_token(refresh_token)}") response.set_cookie( key="access_token", value=access_token, httponly=True, secure=PROD, samesite="Strict", max_age=int(access_exp.total_seconds()), ) response.set_cookie( key="refresh_token", value=refresh_token, httponly=True, secure=PROD, samesite="Strict", max_age=int(refresh_exp.total_seconds()), ) return access_token, refresh_token