From 3620ea07a3b350e93abf9a15b284b309f9c3bf90 Mon Sep 17 00:00:00 2001 From: grey-cat-1908 <61203964+grey-cat-1908@users.noreply.github.com> Date: Sun, 22 Sep 2024 08:34:48 +0000 Subject: [PATCH] cors support --- .env.example | 5 +++++ database/__init__.py | 2 +- docker-compose.yml | 2 ++ main.py | 12 +++++++++++- models/settings.py | 40 ++++++++++++++++++++++++++++++++++------ routes/admin.py | 2 +- routes/user.py | 2 +- routes/utils.py | 4 ++-- 8 files changed, 57 insertions(+), 12 deletions(-) diff --git a/.env.example b/.env.example index 4270981..90020d3 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,12 @@ DATABASE_USER=admin DATABASE_PASSWORD=password DATABASE_NAME=formaptix + PORT=3000 SECRET=super_secret + +FRONTEND_HOST="http://example.com" +BACKEND_CORS_ORIGINS="http://localhost,http://localhost:5173" + ADMIN_PASSWORD=admin_password DISABLE_ADMIN=false \ No newline at end of file diff --git a/database/__init__.py b/database/__init__.py index 127972e..8a6f4f7 100644 --- a/database/__init__.py +++ b/database/__init__.py @@ -2,7 +2,7 @@ from models import settings from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker from sqlalchemy.orm import DeclarativeBase -engine = create_async_engine(settings.database) +engine = create_async_engine(settings.DATABASE) sessions = async_sessionmaker(engine) diff --git a/docker-compose.yml b/docker-compose.yml index 08b9629..567f3af 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,8 @@ services: - PORT=${PORT} - ADMIN_PASSWORD=${ADMIN_PASSWORD} - DISABLE_ADMIN=${DISABLE_ADMIN} + - BACKEND_CORS_ORIGINS=${BACKEND_CORS_ORIGINS} + - FRONTEND_HOST=${FRONTEND_HOST} ports: - ${PORT}:${PORT} depends_on: diff --git a/main.py b/main.py index e6f6013..011c821 100644 --- a/main.py +++ b/main.py @@ -3,6 +3,7 @@ from contextlib import asynccontextmanager import uvicorn from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware import models import database @@ -22,4 +23,13 @@ async def lifespan(app: FastAPI): app = FastAPI(lifespan=lifespan) app.include_router(routes.router) -uvicorn.run(app, host="0.0.0.0", port=models.settings.port) +if models.settings.all_cors_origins: + app.add_middleware( + CORSMiddleware, + allow_origins=models.settings.all_cors_origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + +uvicorn.run(app, host="0.0.0.0", port=models.settings.PORT) diff --git a/models/settings.py b/models/settings.py index 3c14b79..be668ee 100644 --- a/models/settings.py +++ b/models/settings.py @@ -1,12 +1,40 @@ -from pydantic_settings import BaseSettings +from typing import Annotated, Any + +from pydantic import AnyUrl, BeforeValidator, computed_field +from pydantic_settings import BaseSettings, SettingsConfigDict + + +def parse_cors(v: Any) -> list[str] | str: + if isinstance(v, str) and not v.startswith("["): + return [i.strip() for i in v.split(",")] + elif isinstance(v, list | str): + return v + raise ValueError(v) class Settings(BaseSettings): - database: str - secret: str - port: int - admin_password: str - disable_admin: bool + model_config = SettingsConfigDict( + env_file=".env", env_ignore_empty=True, extra="ignore" + ) + + DATABASE: str + SECRET: str + PORT: int + + FRONTEND_HOST: str = "http://localhost:5173" + BACKEND_CORS_ORIGINS: Annotated[list[AnyUrl] | str, BeforeValidator(parse_cors)] = ( + [] + ) + + @computed_field # type: ignore[prop-decorator] + @property + def all_cors_origins(self) -> list[str]: + return [str(origin).rstrip("/") for origin in self.BACKEND_CORS_ORIGINS] + [ + self.FRONTEND_HOST + ] + + ADMIN_PASSWORD: str + DISABLE_ADMIN: bool = False settings = Settings() diff --git a/routes/admin.py b/routes/admin.py index 69423c2..95d3c75 100644 --- a/routes/admin.py +++ b/routes/admin.py @@ -16,7 +16,7 @@ async def create_user(auth: user.Auth, admin_token: Admin): raise HTTPException(400, "Username must not be empty") if len(auth.password.strip()) == 0: raise HTTPException(400, "Password must not be empty") - if settings.disable_admin: + if settings.DISABLE_ADMIN: raise HTTPException(403, "You are not admin") salt = secrets.token_hex(8) diff --git a/routes/user.py b/routes/user.py index 425f875..6dde0bc 100644 --- a/routes/user.py +++ b/routes/user.py @@ -31,7 +31,7 @@ async def login(auth: models.Auth) -> models.Token: id=user.id, username=user.username, token=jwt.encode( - {"sub": user.id}, settings.secret + user.password, "HS256" + {"sub": user.id}, settings.SECRET + user.password, "HS256" ), ) diff --git a/routes/utils.py b/routes/utils.py index fca642c..d7c555d 100644 --- a/routes/utils.py +++ b/routes/utils.py @@ -14,7 +14,7 @@ def hash_password(password: str, salt: str) -> str: def verify_admin(password: Annotated[str, Header(alias="x-token")]): - if password.strip() != settings.admin_password: + if password.strip() != settings.ADMIN_PASSWORD: raise HTTPException(401, "Unauthorized") return True @@ -39,7 +39,7 @@ async def verify_user(token: Annotated[str, Header(alias="x-token")]) -> databas raise HTTPException(401, "Invalid token") try: - jwt.decode(token, settings.secret + user.password, algorithms=["HS256"]) + jwt.decode(token, settings.SECRET + user.password, algorithms=["HS256"]) except jwt.exceptions.InvalidSignatureError: raise HTTPException(401, "Invalid token")