init
This commit is contained in:
		
						commit
						e92bf907fe
					
				
							
								
								
									
										130
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,130 @@
 | 
			
		||||
#
 | 
			
		||||
test_modules/
 | 
			
		||||
config/SSL/fullchain.pem
 | 
			
		||||
config/SSL/privkey.pem
 | 
			
		||||
logs/
 | 
			
		||||
SECRET_KEY.key
 | 
			
		||||
 | 
			
		||||
# Byte-compiled / optimized / DLL files
 | 
			
		||||
__pycache__/
 | 
			
		||||
*.py[cod]
 | 
			
		||||
*$py.class
 | 
			
		||||
 | 
			
		||||
# C extensions
 | 
			
		||||
*.so
 | 
			
		||||
 | 
			
		||||
# Distribution / packaging
 | 
			
		||||
.Python
 | 
			
		||||
build/
 | 
			
		||||
develop-eggs/
 | 
			
		||||
dist/
 | 
			
		||||
downloads/
 | 
			
		||||
eggs/
 | 
			
		||||
.eggs/
 | 
			
		||||
lib/
 | 
			
		||||
lib64/
 | 
			
		||||
parts/
 | 
			
		||||
sdist/
 | 
			
		||||
var/
 | 
			
		||||
wheels/
 | 
			
		||||
pip-wheel-metadata/
 | 
			
		||||
share/python-wheels/
 | 
			
		||||
*.egg-info/
 | 
			
		||||
.installed.cfg
 | 
			
		||||
*.egg
 | 
			
		||||
MANIFEST
 | 
			
		||||
 | 
			
		||||
# PyInstaller
 | 
			
		||||
#  Usually these files are written by a python script from a template
 | 
			
		||||
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
 | 
			
		||||
*.manifest
 | 
			
		||||
 | 
			
		||||
# Installer logs
 | 
			
		||||
pip-log.txt
 | 
			
		||||
pip-delete-this-directory.txt
 | 
			
		||||
 | 
			
		||||
# Unit test / coverage reports
 | 
			
		||||
htmlcov/
 | 
			
		||||
.tox/
 | 
			
		||||
.nox/
 | 
			
		||||
.coverage
 | 
			
		||||
.coverage.*
 | 
			
		||||
.cache
 | 
			
		||||
nosetests.xml
 | 
			
		||||
coverage.xml
 | 
			
		||||
*.cover
 | 
			
		||||
.hypothesis/
 | 
			
		||||
.pytest_cache/
 | 
			
		||||
 | 
			
		||||
# Translations
 | 
			
		||||
*.mo
 | 
			
		||||
*.pot
 | 
			
		||||
 | 
			
		||||
# Django stuff:
 | 
			
		||||
*.log
 | 
			
		||||
local_settings.py
 | 
			
		||||
db.sqlite3
 | 
			
		||||
db.sqlite3-journal
 | 
			
		||||
 | 
			
		||||
# Flask stuff:
 | 
			
		||||
instance/
 | 
			
		||||
.webassets-cache
 | 
			
		||||
 | 
			
		||||
# Scrapy stuff:
 | 
			
		||||
.scrapy
 | 
			
		||||
 | 
			
		||||
# Sphinx documentation
 | 
			
		||||
docs/_build/
 | 
			
		||||
 | 
			
		||||
# PyBuilder
 | 
			
		||||
target/
 | 
			
		||||
 | 
			
		||||
# Jupyter Notebook
 | 
			
		||||
.ipynb_checkpoints
 | 
			
		||||
 | 
			
		||||
# IPython
 | 
			
		||||
profile_default/
 | 
			
		||||
ipython_config.py
 | 
			
		||||
 | 
			
		||||
# pyenv
 | 
			
		||||
.python-version
 | 
			
		||||
 | 
			
		||||
# pipenv
 | 
			
		||||
#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
 | 
			
		||||
#   However, in case of collaboration, if having platform-specific dependencies or dependencies
 | 
			
		||||
#   having no cross-platform support, pipenv may install dependencies that don't work, or not
 | 
			
		||||
#   install all needed dependencies.
 | 
			
		||||
#Pipfile.lock
 | 
			
		||||
 | 
			
		||||
# celery beat schedule file
 | 
			
		||||
celerybeat-schedule
 | 
			
		||||
 | 
			
		||||
# SageMath parsed files
 | 
			
		||||
*.sage.py
 | 
			
		||||
 | 
			
		||||
# Environments
 | 
			
		||||
.env
 | 
			
		||||
.venv
 | 
			
		||||
env/
 | 
			
		||||
venv/
 | 
			
		||||
ENV/
 | 
			
		||||
env.bak/
 | 
			
		||||
venv.bak/
 | 
			
		||||
 | 
			
		||||
# Spyder project settings
 | 
			
		||||
.spyderproject
 | 
			
		||||
.spyproject
 | 
			
		||||
 | 
			
		||||
# Rope project settings
 | 
			
		||||
.ropeproject
 | 
			
		||||
 | 
			
		||||
# mkdocs documentation
 | 
			
		||||
/site
 | 
			
		||||
 | 
			
		||||
# mypy
 | 
			
		||||
.mypy_cache/
 | 
			
		||||
.dmypy.json
 | 
			
		||||
dmypy.json
 | 
			
		||||
 | 
			
		||||
# Pyre type checker
 | 
			
		||||
.pyre/
 | 
			
		||||
							
								
								
									
										0
									
								
								common_lib/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								common_lib/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								common_lib/core/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								common_lib/core/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										4
									
								
								common_lib/core/rate_limit.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								common_lib/core/rate_limit.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
			
		||||
from slowapi import Limiter
 | 
			
		||||
from slowapi.util import get_remote_address
 | 
			
		||||
 | 
			
		||||
limiter = Limiter(key_func=get_remote_address)
 | 
			
		||||
							
								
								
									
										0
									
								
								common_lib/docs/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								common_lib/docs/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										119
									
								
								common_lib/docs/errors.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								common_lib/docs/errors.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,119 @@
 | 
			
		||||
COMMON_ERROR_RESPONSES = {
 | 
			
		||||
    400: {
 | 
			
		||||
        "description": "Bad Request",
 | 
			
		||||
        "content": {
 | 
			
		||||
            "application/json": {
 | 
			
		||||
                "example": {
 | 
			
		||||
                    "status": "error",
 | 
			
		||||
                    "errors": [
 | 
			
		||||
                        {"field": "general", "message": "Bad request syntax or invalid parameters"}
 | 
			
		||||
                    ]
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    401: {
 | 
			
		||||
        "description": "Unauthorized",
 | 
			
		||||
        "content": {
 | 
			
		||||
            "application/json": {
 | 
			
		||||
                "example": {
 | 
			
		||||
                    "status": "error",
 | 
			
		||||
                    "errors": [
 | 
			
		||||
                        {"field": "login", "message": "Invalid login or password"}
 | 
			
		||||
                    ]
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    403: {
 | 
			
		||||
        "description": "Forbidden",
 | 
			
		||||
        "content": {
 | 
			
		||||
            "application/json": {
 | 
			
		||||
                "example": {
 | 
			
		||||
                    "status": "error",
 | 
			
		||||
                    "errors": [
 | 
			
		||||
                        {"field": "permission", "message": "You don't have access to this resource"}
 | 
			
		||||
                    ]
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    404: {
 | 
			
		||||
        "description": "Not Found",
 | 
			
		||||
        "content": {
 | 
			
		||||
            "application/json": {
 | 
			
		||||
                "example": {
 | 
			
		||||
                    "status": "error",
 | 
			
		||||
                    "errors": [
 | 
			
		||||
                        {"field": "resource", "message": "Requested resource not found"}
 | 
			
		||||
                    ]
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    405: {
 | 
			
		||||
        "description": "Method Not Allowed",
 | 
			
		||||
        "content": {
 | 
			
		||||
            "application/json": {
 | 
			
		||||
                "example": {
 | 
			
		||||
                    "status": "error",
 | 
			
		||||
                    "errors": [
 | 
			
		||||
                        {"field": "method", "message": "Method not allowed on this endpoint"}
 | 
			
		||||
                    ]
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    409: {
 | 
			
		||||
        "description": "Conflict",
 | 
			
		||||
        "content": {
 | 
			
		||||
            "application/json": {
 | 
			
		||||
                "example": {
 | 
			
		||||
                    "status": "error",
 | 
			
		||||
                    "errors": [
 | 
			
		||||
                        {"field": "conflict", "message": "Resource already exists or conflict occurred"}
 | 
			
		||||
                    ]
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    418: {
 | 
			
		||||
        "description": "I'm a teapot (In Development)",
 | 
			
		||||
        "content": {
 | 
			
		||||
            "application/json": {
 | 
			
		||||
                "example": {
 | 
			
		||||
                    "status": "error",
 | 
			
		||||
                    "errors": [
 | 
			
		||||
                        {"field": "debug", "message": "This feature is under development"}
 | 
			
		||||
                    ]
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    422: {
 | 
			
		||||
        "description": "Validation Error",
 | 
			
		||||
        "content": {
 | 
			
		||||
            "application/json": {
 | 
			
		||||
                "example": {
 | 
			
		||||
                    "status": "error",
 | 
			
		||||
                    "errors": [
 | 
			
		||||
                        {"field": "login", "message": "Login must not contain whitespace characters"}
 | 
			
		||||
                    ]
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    500: {
 | 
			
		||||
        "description": "Internal Server Error",
 | 
			
		||||
        "content": {
 | 
			
		||||
            "application/json": {
 | 
			
		||||
                "example": {
 | 
			
		||||
                    "status": "error",
 | 
			
		||||
                    "errors": [
 | 
			
		||||
                        {"field": "server", "message": "An unexpected error occurred. Please try again later."}
 | 
			
		||||
                    ]
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										0
									
								
								common_lib/middlewares/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								common_lib/middlewares/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										168
									
								
								common_lib/middlewares/error_handler.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										168
									
								
								common_lib/middlewares/error_handler.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,168 @@
 | 
			
		||||
import os
 | 
			
		||||
import uuid
 | 
			
		||||
import logging
 | 
			
		||||
from fastapi import FastAPI, Request, HTTPException
 | 
			
		||||
from fastapi.responses import JSONResponse
 | 
			
		||||
from fastapi.exceptions import RequestValidationError
 | 
			
		||||
from starlette.status import (
 | 
			
		||||
    HTTP_400_BAD_REQUEST,
 | 
			
		||||
    HTTP_401_UNAUTHORIZED,
 | 
			
		||||
    HTTP_403_FORBIDDEN,
 | 
			
		||||
    HTTP_404_NOT_FOUND,
 | 
			
		||||
    HTTP_405_METHOD_NOT_ALLOWED,
 | 
			
		||||
    HTTP_409_CONFLICT,
 | 
			
		||||
    HTTP_418_IM_A_TEAPOT,
 | 
			
		||||
    HTTP_422_UNPROCESSABLE_ENTITY,
 | 
			
		||||
)
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
from config import settings
 | 
			
		||||
 | 
			
		||||
# Настраиваем логирование
 | 
			
		||||
logging.basicConfig(level=logging.ERROR)
 | 
			
		||||
logger = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def register_error_handlers(app: FastAPI):
 | 
			
		||||
 | 
			
		||||
    # Кастомный обработчик для 400 ошибки (BAD_REQUEST)
 | 
			
		||||
    @app.exception_handler(HTTP_400_BAD_REQUEST)
 | 
			
		||||
    async def bad_request_handler(request: Request, exc: HTTPException):
 | 
			
		||||
        return JSONResponse(
 | 
			
		||||
            status_code=HTTP_400_BAD_REQUEST,
 | 
			
		||||
            content={
 | 
			
		||||
                "status": "error",
 | 
			
		||||
                "errors": [{"field": "general", "message": exc.detail or "Bad request syntax or invalid parameters"}]
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # Кастомный обработчик для 401 ошибки (UNAUTHORIZED)
 | 
			
		||||
    @app.exception_handler(HTTP_401_UNAUTHORIZED)
 | 
			
		||||
    async def unauthorized_handler(request: Request, exc: HTTPException):
 | 
			
		||||
        return JSONResponse(
 | 
			
		||||
            status_code=HTTP_401_UNAUTHORIZED,
 | 
			
		||||
            content={
 | 
			
		||||
                "status": "error",
 | 
			
		||||
                "errors": [{"field": "login", "message": exc.detail or "Unauthorized"}]
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # Кастомный обработчик для 403 ошибки (FORBIDDEN)
 | 
			
		||||
    @app.exception_handler(HTTP_403_FORBIDDEN)
 | 
			
		||||
    async def forbidden_handler(request: Request, exc: HTTPException):
 | 
			
		||||
        return JSONResponse(
 | 
			
		||||
            status_code=HTTP_403_FORBIDDEN,
 | 
			
		||||
            content={
 | 
			
		||||
                "status": "error",
 | 
			
		||||
                "errors": [{"field": "permission", "message": exc.detail or "Forbidden"}]
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # Кастомный обработчик для 404 ошибки (NOT_FOUND)
 | 
			
		||||
    @app.exception_handler(404)
 | 
			
		||||
    async def custom_404_handler(request: Request, exc: HTTPException):
 | 
			
		||||
        return JSONResponse(
 | 
			
		||||
            status_code=HTTP_404_NOT_FOUND,
 | 
			
		||||
            content={
 | 
			
		||||
                "status": "error",
 | 
			
		||||
                "errors": [{"field": "resource", "message": exc.detail or "Resource not found."}]
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # Кастомный обработчик для 405 ошибки
 | 
			
		||||
    @app.exception_handler(405)
 | 
			
		||||
    async def custom_405_handler(request: Request, exc: HTTPException):
 | 
			
		||||
        return JSONResponse(
 | 
			
		||||
            status_code=HTTP_405_METHOD_NOT_ALLOWED,
 | 
			
		||||
            content={
 | 
			
		||||
                "status": "error",
 | 
			
		||||
                "errors": [{"field": "request", "message": exc.detail or "Method not allowed."}]
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # Кастомный обработчик для 409 ошибки (CONFLICT) (message: already exists)
 | 
			
		||||
    @app.exception_handler(HTTP_409_CONFLICT)
 | 
			
		||||
    async def conflict_handler(request: Request, exc: HTTPException):
 | 
			
		||||
        return JSONResponse(
 | 
			
		||||
            status_code=HTTP_409_CONFLICT,
 | 
			
		||||
            content={
 | 
			
		||||
                "status": "error",
 | 
			
		||||
                "errors": [{"field": "conflict", "message": exc.detail or "Conflict"}]
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # Кастомный обработчик для 418 ошибки (IM_A_TEAPOT) (message: в разработке)
 | 
			
		||||
    @app.exception_handler(HTTP_418_IM_A_TEAPOT)
 | 
			
		||||
    async def teapot_handler(request: Request, exc: HTTPException):
 | 
			
		||||
        return JSONResponse(
 | 
			
		||||
            status_code=HTTP_418_IM_A_TEAPOT,
 | 
			
		||||
            content={
 | 
			
		||||
                "status": "error",
 | 
			
		||||
                "errors": [{"field": "debug", "message": exc.detail or "This feature is under development"}]
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # Кастомный обработчик валидатора 422
 | 
			
		||||
    @app.exception_handler(RequestValidationError)
 | 
			
		||||
    async def validation_exception_handler(request: Request, exc: RequestValidationError):
 | 
			
		||||
        errors = exc.errors()
 | 
			
		||||
        formatted_errors = []
 | 
			
		||||
 | 
			
		||||
        for err in errors:
 | 
			
		||||
            field_path = ".".join(str(loc) for loc in err.get("loc", []) if isinstance(loc, str))
 | 
			
		||||
            formatted_errors.append({
 | 
			
		||||
                "field": field_path,
 | 
			
		||||
                "message": err.get("msg", "Validation error")
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
        return JSONResponse(
 | 
			
		||||
            status_code=HTTP_422_UNPROCESSABLE_ENTITY,
 | 
			
		||||
            content={
 | 
			
		||||
                "status": "error",
 | 
			
		||||
                "errors": formatted_errors
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # Универсальный обработчик HTTP ошибок
 | 
			
		||||
    @app.exception_handler(HTTPException)
 | 
			
		||||
    async def http_exception_handler(request: Request, exc: HTTPException):
 | 
			
		||||
        return JSONResponse(
 | 
			
		||||
            status_code=exc.status_code,
 | 
			
		||||
            content={
 | 
			
		||||
                "status": "error",
 | 
			
		||||
                "errors": [{"field": "request", "message": exc.detail or "HTTP error"}]
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # Обработчик Internal Server Error
 | 
			
		||||
    @app.exception_handler(Exception)
 | 
			
		||||
    async def general_exception_handler(request: Request, exc: Exception):
 | 
			
		||||
        # logger.error(f"Unhandled error: {exc}")
 | 
			
		||||
 | 
			
		||||
        LOG_DIR = settings.LOG_DIR
 | 
			
		||||
        os.makedirs(LOG_DIR, exist_ok=True)
 | 
			
		||||
        # Уникальный код ошибки (первые 8 символов UUID)
 | 
			
		||||
        error_id = str(uuid.uuid4())[:8]
 | 
			
		||||
        date_str = datetime.now().strftime("%Y-%m-%d")  # Текущая дата (YYYY-MM-DD)
 | 
			
		||||
        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")  # Полное время ошибки
 | 
			
		||||
 | 
			
		||||
        log_filename = os.path.join(LOG_DIR, f"{error_id}_{date_str}.log")
 | 
			
		||||
 | 
			
		||||
        full_log_message = (
 | 
			
		||||
            f"{'-' * 50}\n"
 | 
			
		||||
            f"Timestamp: {timestamp}\n"
 | 
			
		||||
            f"Error ID: {error_id}\n"
 | 
			
		||||
            f"Message: {exc}\n"
 | 
			
		||||
            f"{'-' * 50}\n"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        with open(log_filename, "w", encoding="utf-8") as log_file:
 | 
			
		||||
            log_file.write(full_log_message)
 | 
			
		||||
 | 
			
		||||
        print(f"critical error: {LOG_DIR}/{error_id}_{date_str}.log")
 | 
			
		||||
        return JSONResponse(
 | 
			
		||||
            status_code=500,
 | 
			
		||||
            content={
 | 
			
		||||
                "status": "error",
 | 
			
		||||
                "errors": [{"field": "server", "message": "Internal Server Error"}]
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
							
								
								
									
										1
									
								
								common_lib/models/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								common_lib/models/__init__.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										26
									
								
								common_lib/models/db.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								common_lib/models/db.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,26 @@
 | 
			
		||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
 | 
			
		||||
from sqlalchemy.orm import declarative_base
 | 
			
		||||
from config import settings  # поправил импорт — без .config
 | 
			
		||||
 | 
			
		||||
# Создаем асинхронный движок
 | 
			
		||||
engine = create_async_engine(settings.DATABASE_URL, echo=False, future=True)
 | 
			
		||||
 | 
			
		||||
# Создаем фабрику сессий
 | 
			
		||||
AsyncSessionLocal = async_sessionmaker(
 | 
			
		||||
    bind=engine,
 | 
			
		||||
    class_=AsyncSession,
 | 
			
		||||
    expire_on_commit=False
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
# Базовый класс для моделей
 | 
			
		||||
Base = declarative_base()
 | 
			
		||||
 | 
			
		||||
# Зависимость для FastAPI
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def get_db():
 | 
			
		||||
    async with AsyncSessionLocal() as session:
 | 
			
		||||
        try:
 | 
			
		||||
            yield session
 | 
			
		||||
        finally:
 | 
			
		||||
            await session.close()
 | 
			
		||||
							
								
								
									
										0
									
								
								common_lib/utils/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								common_lib/utils/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										160
									
								
								common_lib/utils/auth.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								common_lib/utils/auth.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,160 @@
 | 
			
		||||
import httpx
 | 
			
		||||
import re
 | 
			
		||||
from fastapi import Depends, Request, HTTPException, status
 | 
			
		||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
 | 
			
		||||
from typing import List
 | 
			
		||||
 | 
			
		||||
from dataclasses import dataclass
 | 
			
		||||
from config import settings
 | 
			
		||||
 | 
			
		||||
auth_scheme = HTTPBearer()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class CurrentUser:
 | 
			
		||||
    user_id: str
 | 
			
		||||
    session_id: str
 | 
			
		||||
    permissions: List[str]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def get_current_user(
 | 
			
		||||
    request: Request,
 | 
			
		||||
    credentials: HTTPAuthorizationCredentials = Depends(auth_scheme)
 | 
			
		||||
):
 | 
			
		||||
    token = credentials.credentials
 | 
			
		||||
    ip = request.client.host or "(unknown)"
 | 
			
		||||
    user_agent = request.headers.get("User-Agent", "(unknown)")
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        async with httpx.AsyncClient(verify=False) as client:
 | 
			
		||||
            response = await client.post(
 | 
			
		||||
                f"{settings.TOKEN_SERVICE}/decode",
 | 
			
		||||
                json={
 | 
			
		||||
                    "token": token,
 | 
			
		||||
                    "ip": ip,
 | 
			
		||||
                    "user_agent": user_agent
 | 
			
		||||
                },
 | 
			
		||||
                timeout=5.0
 | 
			
		||||
            )
 | 
			
		||||
    except httpx.RequestError:
 | 
			
		||||
        raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Token service unavailable")
 | 
			
		||||
 | 
			
		||||
    if response.status_code != 200:
 | 
			
		||||
        # raise HTTPException(status_code=response.status_code, detail=response.json().get("detail", "Invalid token"))
 | 
			
		||||
        raise HTTPException(
 | 
			
		||||
            status_code=response.status_code,
 | 
			
		||||
            detail=f"token_service: {response.text}"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    data = response.json()
 | 
			
		||||
 | 
			
		||||
    return CurrentUser(
 | 
			
		||||
        user_id=data["user_id"],
 | 
			
		||||
        session_id=data["session_id"],
 | 
			
		||||
        permissions=data["permissions"]
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_username(value: str,
 | 
			
		||||
                      field_name: str = "login",
 | 
			
		||||
                      with_httpexception=False,
 | 
			
		||||
                      need_back=False,
 | 
			
		||||
                      min_length=3,
 | 
			
		||||
                      max_length=32,
 | 
			
		||||
                      error_status_code: int = status.HTTP_401_UNAUTHORIZED,
 | 
			
		||||
                      error_detail: str = "Invalid login or password") -> str:
 | 
			
		||||
    """
 | 
			
		||||
    Username validator:
 | 
			
		||||
    - checks for length
 | 
			
		||||
    - no spaces
 | 
			
		||||
    - no leading underscore
 | 
			
		||||
    - no consecutive underscores
 | 
			
		||||
    - only [A-Za-z0-9_]
 | 
			
		||||
    """
 | 
			
		||||
    # Проверка типа и длины
 | 
			
		||||
    if not isinstance(value, str) or not (min_length <= len(value) <= max_length):
 | 
			
		||||
        msg = f"{field_name.capitalize()} must be between {min_length} and {max_length} characters long"
 | 
			
		||||
        if with_httpexception:
 | 
			
		||||
            raise HTTPException(status_code=error_status_code, detail=error_detail)
 | 
			
		||||
        if need_back:
 | 
			
		||||
            return False, msg
 | 
			
		||||
        raise ValueError(msg)
 | 
			
		||||
 | 
			
		||||
    # Пробелы
 | 
			
		||||
    if any(c.isspace() for c in value):
 | 
			
		||||
        msg = f"{field_name.capitalize()} must not contain whitespace characters"
 | 
			
		||||
        if with_httpexception:
 | 
			
		||||
            raise HTTPException(status_code=error_status_code, detail=error_detail)
 | 
			
		||||
        if need_back:
 | 
			
		||||
            return False, msg
 | 
			
		||||
        raise ValueError(msg)
 | 
			
		||||
 | 
			
		||||
    # Начинается с подчеркивания
 | 
			
		||||
    if value.startswith('_'):
 | 
			
		||||
        msg = f"{field_name.capitalize()} must not start with an underscore"
 | 
			
		||||
        if with_httpexception:
 | 
			
		||||
            raise HTTPException(status_code=error_status_code, detail=error_detail)
 | 
			
		||||
        if need_back:
 | 
			
		||||
            return False, msg
 | 
			
		||||
        raise ValueError(msg)
 | 
			
		||||
 | 
			
		||||
    # Двойные подчеркивания
 | 
			
		||||
    if '__' in value:
 | 
			
		||||
        msg = f"{field_name.capitalize()} must not contain consecutive underscores"
 | 
			
		||||
        if with_httpexception:
 | 
			
		||||
            raise HTTPException(status_code=error_status_code, detail=error_detail)
 | 
			
		||||
        if need_back:
 | 
			
		||||
            return False, msg
 | 
			
		||||
        raise ValueError(msg)
 | 
			
		||||
 | 
			
		||||
    # Только допустимые символы
 | 
			
		||||
    if not re.fullmatch(r'[A-Za-z0-9_]+', value):
 | 
			
		||||
        msg = f"{field_name.capitalize()} must contain only English letters, digits, and underscores"
 | 
			
		||||
        if with_httpexception:
 | 
			
		||||
            raise HTTPException(status_code=error_status_code, detail=error_detail)
 | 
			
		||||
        if need_back:
 | 
			
		||||
            return False, msg
 | 
			
		||||
        raise ValueError(msg)
 | 
			
		||||
 | 
			
		||||
    if need_back:
 | 
			
		||||
        return True, value.lower()
 | 
			
		||||
    return value.lower()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_password(value: str,
 | 
			
		||||
                      field_name: str = "password",
 | 
			
		||||
                      with_httpexception=False,
 | 
			
		||||
                      need_back=False,
 | 
			
		||||
                      min_length=8,
 | 
			
		||||
                      max_length=128,
 | 
			
		||||
                      error_status_code: int = status.HTTP_401_UNAUTHORIZED,
 | 
			
		||||
                      error_detail: str = "Invalid login or password") -> str:
 | 
			
		||||
    """
 | 
			
		||||
    Validates password length and (optionally) other rules.
 | 
			
		||||
    Supports HTTPException raising if `with_httpexception=True`.
 | 
			
		||||
    Returns (True, value) or raises ValueError.
 | 
			
		||||
    """
 | 
			
		||||
    # Проверка типа и длины
 | 
			
		||||
    if not isinstance(value, str) or not (min_length <= len(value) <= max_length):
 | 
			
		||||
        msg = f"{field_name.capitalize()} must be between {min_length} and {max_length} characters long"
 | 
			
		||||
        if with_httpexception:
 | 
			
		||||
            raise HTTPException(status_code=error_status_code, detail=error_detail)
 | 
			
		||||
        if need_back:
 | 
			
		||||
            return False, msg
 | 
			
		||||
        raise ValueError(msg)
 | 
			
		||||
 | 
			
		||||
    # if any(c.isspace() for c in value):
 | 
			
		||||
    #     raise ValueError(f"{field_name.capitalize()} must not contain whitespace characters")
 | 
			
		||||
 | 
			
		||||
    # if not re.search(r'[A-Z]', value):
 | 
			
		||||
    #     raise ValueError(f"{field_name.capitalize()} must contain at least one uppercase letter")
 | 
			
		||||
 | 
			
		||||
    # if not re.search(r'\d', value):
 | 
			
		||||
    #     raise ValueError(f"{field_name.capitalize()} must contain at least one digit")
 | 
			
		||||
 | 
			
		||||
    # if not re.search(r'[!@#$%^&*()\-_=+\[\]{};:\'",.<>?/|\\]', value):
 | 
			
		||||
    #     raise ValueError(f"{field_name.capitalize()} must contain at least one special character")
 | 
			
		||||
 | 
			
		||||
    if need_back:
 | 
			
		||||
        return True, value
 | 
			
		||||
    return value
 | 
			
		||||
							
								
								
									
										16
									
								
								pyproject.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								pyproject.toml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
			
		||||
[project]
 | 
			
		||||
name = "common-lib"
 | 
			
		||||
version = "0.0.1"
 | 
			
		||||
description = "Библиотека общих компонентов для микросервисов yobble"
 | 
			
		||||
authors = [{ name = "cheykrym", email = "you@example.com" }]
 | 
			
		||||
license = "CHEYKRYM"
 | 
			
		||||
dependencies = [
 | 
			
		||||
    "fastapi",
 | 
			
		||||
    "sqlalchemy",
 | 
			
		||||
    "httpx",
 | 
			
		||||
    "slowapi"
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[build-system]
 | 
			
		||||
requires = ["setuptools", "wheel"]
 | 
			
		||||
build-backend = "setuptools.build_meta"
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user