diff --git a/README.md b/README.md index f09b45f..aa2d438 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ # zoo -An asynchronous zoo API, powered by FastAPI and SQLModel. +An asynchronous zoo API, powered by [FastAPI](https://fastapi.tiangolo.com/), +[SQLAlchemy](https://www.sqlalchemy.org/), [Pydantic](https://docs.pydantic.dev/latest/), +and [Alembic](https://alembic.sqlalchemy.org/en/latest/). diff --git a/tests/backend/test_animals.py b/tests/api/test_animals_api.py similarity index 100% rename from tests/backend/test_animals.py rename to tests/api/test_animals_api.py diff --git a/tests/backend/test_failures.py b/tests/api/test_failures.py similarity index 100% rename from tests/backend/test_failures.py rename to tests/api/test_failures.py diff --git a/tests/models/test_animals_models.py b/tests/schema/test_animals_schema.py similarity index 100% rename from tests/models/test_animals_models.py rename to tests/schema/test_animals_schema.py diff --git a/zoo/__main__.py b/zoo/__main__.py index 9f27e1d..9153fb8 100644 --- a/zoo/__main__.py +++ b/zoo/__main__.py @@ -2,6 +2,7 @@ Zoo CLI """ +import asyncio import json import logging import pathlib @@ -9,10 +10,13 @@ import click import uvicorn +from click import Context +from fastapi_users.exceptions import UserAlreadyExists from zoo._version import __application__, __version__ from zoo.app import ZooFastAPI, app from zoo.config import ZooSettings, app_config +from zoo.models.users import create_user logger = logging.getLogger(__name__) @@ -58,6 +62,26 @@ def openapi(context: ZooContext) -> None: json_file.write_text(json.dumps(openapi_body, indent=2)) +@cli.command() +@click.option("-e", "--email", help="User email to create", type=str, required=True) +@click.option( + "-p", "--password", default="admin", help="Password to create", type=str, required=True +) +@click.pass_context +def users(context: Context, email: str, password: str) -> None: + """ + Create users + """ + _ = context + logger.info("Creating user: %s", email) + try: + user = asyncio.run(create_user(email=email, password=password)) + logger.info("Created user: %s", user.id) + except UserAlreadyExists: + logger.info("User already exists: %s", email) + context.exit(1) + + if __name__ == "__main__": logging.basicConfig(level=logging.INFO) if not app_config.DOCKER: diff --git a/zoo/models/users.py b/zoo/models/users.py index 7f1bb2b..a85c837 100644 --- a/zoo/models/users.py +++ b/zoo/models/users.py @@ -9,7 +9,12 @@ from fastapi import Depends, FastAPI from fastapi_users import BaseUserManager, FastAPIUsers, UUIDIDMixin -from fastapi_users.authentication import AuthenticationBackend, BearerTransport +from fastapi_users.authentication import ( + AuthenticationBackend, + BearerTransport, + CookieTransport, + JWTStrategy, +) from fastapi_users.authentication.strategy import AccessTokenDatabase, DatabaseStrategy from fastapi_users_db_sqlalchemy import SQLAlchemyBaseUserTableUUID, SQLAlchemyUserDatabase from fastapi_users_db_sqlalchemy.access_token import ( @@ -18,6 +23,7 @@ ) from sqlalchemy.ext.asyncio import AsyncSession +from zoo._version import __application__ from zoo.config import app_config from zoo.db import get_async_session from zoo.models.base import Base, CreatedUpdatedMixin, UpdatedAtMixin @@ -25,6 +31,7 @@ auth_endpoint = "auth" jwt_endpoint = "jwt" +cookie_endpoint = "cookie" class User(SQLAlchemyBaseUserTableUUID, CreatedUpdatedMixin, Base): @@ -72,6 +79,15 @@ async def get_user_manager(user_db=Depends(get_user_db)) -> AsyncGenerator[UserM yield UserManager(user_db=user_db) +def get_jwt_strategy() -> JWTStrategy: + """ + Get a DatabaseStrategy using the AccessTokenDatabase + """ + return JWTStrategy( + secret=app_config.DATABASE_SECRET, lifetime_seconds=app_config.JWT_EXPIRATION + ) + + def get_database_strategy( access_token_db: AccessTokenDatabase = Depends(get_access_token_db), ) -> DatabaseStrategy: @@ -82,14 +98,23 @@ def get_database_strategy( bearer_transport = BearerTransport(tokenUrl=f"{auth_endpoint}/{jwt_endpoint}/login") +cookie_transport = CookieTransport(cookie_name=f"{__application__}-auth", cookie_max_age=3600) + + auth_backend = AuthenticationBackend( name="jwt", transport=bearer_transport, get_strategy=get_database_strategy, ) +cookie_auth_backend = AuthenticationBackend( + name="cookie", + transport=cookie_transport, + get_strategy=get_jwt_strategy, +) fastapi_users = FastAPIUsers[User, uuid.UUID]( - get_user_manager=get_user_manager, auth_backends=[auth_backend] + get_user_manager=get_user_manager, + auth_backends=[auth_backend, cookie_auth_backend], ) current_active_user = fastapi_users.current_user(active=True) @@ -148,6 +173,11 @@ def bootstrap_fastapi_users(app: FastAPI) -> None: prefix=f"/{auth_endpoint}/{jwt_endpoint}", tags=[auth_endpoint], ) + app.include_router( + fastapi_users.get_auth_router(cookie_auth_backend), + prefix=f"/{auth_endpoint}/{cookie_endpoint}", + tags=[auth_endpoint], + ) app.include_router( fastapi_users.get_register_router(UserRead, UserCreate), prefix=f"/{auth_endpoint}",