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