From 43c73cdbcb4cf5f34ec05da9b3524142a3fa17a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Voron?= Date: Thu, 5 May 2022 10:54:29 +0200 Subject: [PATCH] Revamp OAuth documentation --- docs/configuration/oauth.md | 158 +++++------------- .../cookbook_create_user_programmatically.py | 2 +- docs/src/db_beanie_oauth.py | 24 +++ docs/src/db_sqlalchemy_oauth.py | 20 +-- .../app/__init__.py | 0 .../app/app.py | 36 ++-- examples/beanie-oauth/app/db.py | 24 +++ examples/beanie-oauth/app/schemas.py | 14 ++ .../app/users.py | 32 ++-- examples/beanie-oauth/main.py | 4 + examples/beanie-oauth/requirements.txt | 3 + examples/mongodb-oauth/app/app.py | 37 ---- examples/mongodb-oauth/app/db.py | 17 -- examples/mongodb-oauth/app/models.py | 17 -- examples/mongodb-oauth/main.py | 4 - examples/mongodb-oauth/requirements.txt | 3 - examples/sqlalchemy-oauth/app/app.py | 20 ++- examples/sqlalchemy-oauth/app/db.py | 20 +-- examples/sqlalchemy-oauth/app/models.py | 17 -- examples/sqlalchemy-oauth/app/schemas.py | 15 ++ examples/sqlalchemy-oauth/app/users.py | 26 +-- examples/sqlalchemy-oauth/requirements.txt | 2 +- examples/tortoise-oauth/app/__init__.py | 0 examples/tortoise-oauth/app/db.py | 9 - examples/tortoise-oauth/app/models.py | 30 ---- examples/tortoise-oauth/app/users.py | 70 -------- examples/tortoise-oauth/main.py | 4 - examples/tortoise-oauth/requirements.txt | 3 - examples/tortoise/app/__init__.py | 0 examples/tortoise/app/app.py | 37 ---- examples/tortoise/app/db.py | 9 - examples/tortoise/app/models.py | 25 --- examples/tortoise/app/users.py | 62 ------- examples/tortoise/main.py | 4 - examples/tortoise/requirements.txt | 3 - 35 files changed, 202 insertions(+), 549 deletions(-) create mode 100644 docs/src/db_beanie_oauth.py rename examples/{mongodb-oauth => beanie-oauth}/app/__init__.py (100%) rename examples/{tortoise-oauth => beanie-oauth}/app/app.py (51%) create mode 100644 examples/beanie-oauth/app/db.py create mode 100644 examples/beanie-oauth/app/schemas.py rename examples/{mongodb-oauth => beanie-oauth}/app/users.py (60%) create mode 100644 examples/beanie-oauth/main.py create mode 100644 examples/beanie-oauth/requirements.txt delete mode 100644 examples/mongodb-oauth/app/app.py delete mode 100644 examples/mongodb-oauth/app/db.py delete mode 100644 examples/mongodb-oauth/app/models.py delete mode 100644 examples/mongodb-oauth/main.py delete mode 100644 examples/mongodb-oauth/requirements.txt delete mode 100644 examples/sqlalchemy-oauth/app/models.py create mode 100644 examples/sqlalchemy-oauth/app/schemas.py delete mode 100644 examples/tortoise-oauth/app/__init__.py delete mode 100644 examples/tortoise-oauth/app/db.py delete mode 100644 examples/tortoise-oauth/app/models.py delete mode 100644 examples/tortoise-oauth/app/users.py delete mode 100644 examples/tortoise-oauth/main.py delete mode 100644 examples/tortoise-oauth/requirements.txt delete mode 100644 examples/tortoise/app/__init__.py delete mode 100644 examples/tortoise/app/app.py delete mode 100644 examples/tortoise/app/db.py delete mode 100644 examples/tortoise/app/models.py delete mode 100644 examples/tortoise/app/users.py delete mode 100644 examples/tortoise/main.py delete mode 100644 examples/tortoise/requirements.txt diff --git a/docs/configuration/oauth.md b/docs/configuration/oauth.md index d10f9b98..5adf4504 100644 --- a/docs/configuration/oauth.md +++ b/docs/configuration/oauth.md @@ -7,15 +7,11 @@ FastAPI Users provides an optional OAuth2 authentication support. It relies on [ You should install the library with the optional dependencies for OAuth: ```sh -pip install 'fastapi-users[sqlalchemy2,oauth]' +pip install 'fastapi-users[sqlalchemy,oauth]' ``` ```sh -pip install 'fastapi-users[mongodb,oauth]' -``` - -```sh -pip install 'fastapi-users[tortoise-orm,oauth]' +pip install 'fastapi-users[beanie,oauth]' ``` ## Configuration @@ -30,78 +26,44 @@ from httpx_oauth.clients.google import GoogleOAuth2 google_oauth_client = GoogleOAuth2("CLIENT_ID", "CLIENT_SECRET") ``` -### Setup the models - -The user models differ a bit from the standard one as we have to have a way to store the OAuth information (access tokens, account ids...). - -```py -from fastapi_users import models - - -class User(models.BaseUser, models.BaseOAuthAccountMixin): - pass - - -class UserCreate(models.BaseUserCreate): - pass - - -class UserUpdate(models.BaseUserUpdate): - pass - - -class UserDB(User, models.BaseUserDB): - pass -``` - -Notice that we inherit from the `BaseOAuthAccountMixin`, which adds a `List` of `BaseOAuthAccount` objects. This object is structured like this: - -* `id` (`UUID4`) – Unique identifier of the OAuth account information. Defaults to a **UUID4**. -* `oauth_name` (`str`) – Name of the OAuth service. It corresponds to the `name` property of the OAuth client. -* `access_token` (`str`) – Access token. -* `expires_at` (`Optional[int]`) - Timestamp at which the access token is expired. -* `refresh_token` (`Optional[str]`) – On services that support it, a token to get a fresh access token. -* `account_id` (`str`) - Identifier of the OAuth account on the corresponding service. -* `account_email` (`str`) - Email address of the OAuth account on the corresponding service. - ### Setup the database adapter #### SQLAlchemy You'll need to define the SQLAlchemy model for storing OAuth accounts. We provide a base one for this: -```py hl_lines="19-24" +```py hl_lines="5 17-18 22 39-40" --8<-- "docs/src/db_sqlalchemy_oauth.py" ``` Notice that we also manually added a `relationship` on the `UserTable` so that SQLAlchemy can properly retrieve the OAuth accounts of the user. -When instantiating the database adapter, you should pass this SQLAlchemy model: +Besides, when instantiating the database adapter, we need pass this SQLAlchemy model as third argument. -```py hl_lines="41-42" ---8<-- "docs/src/db_sqlalchemy_oauth.py" +!!! tip "Primary key is defined as UUID" + By default, we use UUID as a primary key ID for your user. If you want to use another type, like an auto-incremented integer, you can use `SQLAlchemyBaseOAuthAccountTable` as base class and define your own `id` and `user_id` column. + + ```py + class OAuthAccount(SQLAlchemyBaseOAuthAccountTable[int], Base): + id = Column(Integer, primary_key=True) + + @declared_attr + def user_id(cls): + return Column(Integer, ForeignKey("user.id", ondelete="cascade"), nullable=False) + + ``` + + Notice that `SQLAlchemyBaseOAuthAccountTable` expects a generic type to define the actual type of ID you use. + +#### Beanie + +The advantage of MongoDB is that you can easily embed sub-objects in a single document. That's why the configuration for Beanie is quite simple. All we need to do is to define another class to structure an OAuth account object. + +```py hl_lines="5 15-16 20" +--8<-- "docs/src/db_beanie_oauth.py" ``` -#### MongoDB - -Nothing to do, the [basic configuration](./databases/mongodb.md) is enough. - -#### Tortoise ORM - -You'll need to define the Tortoise model for storing the OAuth account model. We provide a base one for this: - -```py hl_lines="29 30" ---8<-- "docs/src/db_tortoise_oauth_model.py" -``` - -!!! warning - Note that you should define the foreign key yourself, so that you can point it the user model in your namespace. - -Then, you should declare it on the database adapter: - -```py hl_lines="8 9" ---8<-- "docs/src/db_tortoise_oauth_adapter.py" -``` +It's worth to note that `OAuthAccount` is **not a Beanie document** but a Pydantic model that we'll embed inside the `User` document, through the `oauth_accounts` array. ### Generate a router @@ -109,9 +71,9 @@ Once you have a `FastAPIUsers` instance, you can make it generate a single OAuth ```py app.include_router( - fastapi_users.get_oauth_router(google_oauth_client, auth_backend, "SECRET"), - prefix="/auth/google", - tags=["auth"], + fastapi_users.get_oauth_router(google_oauth_client, auth_backend, "SECRET"), + prefix="/auth/google", + tags=["auth"], ) ``` @@ -149,10 +111,10 @@ app.include_router( --8<-- "examples/sqlalchemy-oauth/app/db.py" ``` -=== "app/models.py" +=== "app/schemas.py" ```py - --8<-- "examples/sqlalchemy-oauth/app/models.py" + --8<-- "examples/sqlalchemy-oauth/app/schemas.py" ``` === "app/users.py" @@ -161,82 +123,42 @@ app.include_router( --8<-- "examples/sqlalchemy-oauth/app/users.py" ``` -#### MongoDB +#### Beanie -[Open :material-open-in-new:](https://github.com/fastapi-users/fastapi-users/tree/master/examples/mongodb-oauth) +[Open :material-open-in-new:](https://github.com/fastapi-users/fastapi-users/tree/master/examples/beanie-oauth) === "requirements.txt" ``` - --8<-- "examples/mongodb-oauth/requirements.txt" + --8<-- "examples/beanie-oauth/requirements.txt" ``` === "main.py" ```py - --8<-- "examples/mongodb-oauth/main.py" + --8<-- "examples/beanie-oauth/main.py" ``` === "app/app.py" ```py - --8<-- "examples/mongodb-oauth/app/app.py" + --8<-- "examples/beanie-oauth/app/app.py" ``` === "app/db.py" ```py - --8<-- "examples/mongodb-oauth/app/db.py" + --8<-- "examples/beanie-oauth/app/db.py" ``` -=== "app/models.py" +=== "app/schemas.py" ```py - --8<-- "examples/mongodb-oauth/app/models.py" + --8<-- "examples/beanie-oauth/app/schemas.py" ``` === "app/users.py" ```py - --8<-- "examples/mongodb-oauth/app/users.py" - ``` - -#### Tortoise ORM - -[Open :material-open-in-new:](https://github.com/fastapi-users/fastapi-users/tree/master/examples/tortoise-oauth) - -=== "requirements.txt" - - ``` - --8<-- "examples/tortoise-oauth/requirements.txt" - ``` - -=== "main.py" - - ```py - --8<-- "examples/tortoise-oauth/main.py" - ``` - -=== "app/app.py" - - ```py - --8<-- "examples/tortoise-oauth/app/app.py" - ``` - -=== "app/db.py" - - ```py - --8<-- "examples/tortoise-oauth/app/db.py" - ``` - -=== "app/models.py" - - ```py - --8<-- "examples/tortoise-oauth/app/models.py" - ``` - -=== "app/users.py" - - ```py - --8<-- "examples/tortoise-oauth/app/users.py" + --8<-- "examples/beanie-oauth/app/users.py" ``` diff --git a/docs/src/cookbook_create_user_programmatically.py b/docs/src/cookbook_create_user_programmatically.py index 449a81ad..70724c06 100644 --- a/docs/src/cookbook_create_user_programmatically.py +++ b/docs/src/cookbook_create_user_programmatically.py @@ -1,7 +1,7 @@ import contextlib from app.db import get_async_session, get_user_db -from app.models import UserCreate +from app.schemas import UserCreate from app.users import get_user_manager from fastapi_users.manager import UserAlreadyExists diff --git a/docs/src/db_beanie_oauth.py b/docs/src/db_beanie_oauth.py new file mode 100644 index 00000000..835ddd21 --- /dev/null +++ b/docs/src/db_beanie_oauth.py @@ -0,0 +1,24 @@ +from typing import List + +import motor.motor_asyncio +from beanie import PydanticObjectId +from fastapi_users.db import BaseOAuthAccount, BeanieBaseUser, BeanieUserDatabase +from pydantic import Field + +DATABASE_URL = "mongodb://localhost:27017" +client = motor.motor_asyncio.AsyncIOMotorClient( + DATABASE_URL, uuidRepresentation="standard" +) +db = client["database_name"] + + +class OAuthAccount(BaseOAuthAccount): + pass + + +class User(BeanieBaseUser[PydanticObjectId]): + oauth_accounts: List[OAuthAccount] = Field(default_factory=list) + + +async def get_user_db(): + yield BeanieUserDatabase(User) diff --git a/docs/src/db_sqlalchemy_oauth.py b/docs/src/db_sqlalchemy_oauth.py index fc7772e2..87d6fbbd 100644 --- a/docs/src/db_sqlalchemy_oauth.py +++ b/docs/src/db_sqlalchemy_oauth.py @@ -1,29 +1,27 @@ -from typing import AsyncGenerator +from typing import AsyncGenerator, List from fastapi import Depends from fastapi_users.db import ( - SQLAlchemyBaseOAuthAccountTable, - SQLAlchemyBaseUserTable, + SQLAlchemyBaseOAuthAccountTableUUID, + SQLAlchemyBaseUserTableUUID, SQLAlchemyUserDatabase, ) from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine from sqlalchemy.ext.declarative import DeclarativeMeta, declarative_base from sqlalchemy.orm import relationship, sessionmaker -from .db import UserDB - DATABASE_URL = "sqlite+aiosqlite:///./test.db" Base: DeclarativeMeta = declarative_base() -class UserTable(Base, SQLAlchemyBaseUserTable): - oauth_accounts = relationship("OAuthAccountTable") - - -class OAuthAccountTable(SQLAlchemyBaseOAuthAccountTable, Base): +class OAuthAccount(SQLAlchemyBaseOAuthAccountTableUUID, Base): pass +class User(SQLAlchemyBaseUserTableUUID, Base): + oauth_accounts: List[OAuthAccount] = relationship("OAuthAccount", lazy="joined") + + engine = create_async_engine(DATABASE_URL) async_session_maker = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) @@ -39,4 +37,4 @@ async def get_async_session() -> AsyncGenerator[AsyncSession, None]: async def get_user_db(session: AsyncSession = Depends(get_async_session)): - yield SQLAlchemyUserDatabase(UserDB, session, UserTable, OAuthAccountTable) + yield SQLAlchemyUserDatabase(session, User, OAuthAccount) diff --git a/examples/mongodb-oauth/app/__init__.py b/examples/beanie-oauth/app/__init__.py similarity index 100% rename from examples/mongodb-oauth/app/__init__.py rename to examples/beanie-oauth/app/__init__.py diff --git a/examples/tortoise-oauth/app/app.py b/examples/beanie-oauth/app/app.py similarity index 51% rename from examples/tortoise-oauth/app/app.py rename to examples/beanie-oauth/app/app.py index 0991be46..a939f8fc 100644 --- a/examples/tortoise-oauth/app/app.py +++ b/examples/beanie-oauth/app/app.py @@ -1,8 +1,8 @@ +from beanie import init_beanie from fastapi import Depends, FastAPI -from tortoise.contrib.fastapi import register_tortoise -from app.db import DATABASE_URL -from app.models import UserDB +from app.db import User, db +from app.schemas import UserCreate, UserRead, UserUpdate from app.users import ( auth_backend, current_active_user, @@ -15,18 +15,26 @@ app = FastAPI() app.include_router( fastapi_users.get_auth_router(auth_backend), prefix="/auth/jwt", tags=["auth"] ) -app.include_router(fastapi_users.get_register_router(), prefix="/auth", tags=["auth"]) +app.include_router( + fastapi_users.get_register_router(UserRead, UserCreate), + prefix="/auth", + tags=["auth"], +) app.include_router( fastapi_users.get_reset_password_router(), prefix="/auth", tags=["auth"], ) app.include_router( - fastapi_users.get_verify_router(), + fastapi_users.get_verify_router(UserRead), prefix="/auth", tags=["auth"], ) -app.include_router(fastapi_users.get_users_router(), prefix="/users", tags=["users"]) +app.include_router( + fastapi_users.get_users_router(UserRead, UserUpdate), + prefix="/users", + tags=["users"], +) app.include_router( fastapi_users.get_oauth_router(google_oauth_client, auth_backend, "SECRET"), prefix="/auth/google", @@ -35,13 +43,15 @@ app.include_router( @app.get("/authenticated-route") -async def authenticated_route(user: UserDB = Depends(current_active_user)): +async def authenticated_route(user: User = Depends(current_active_user)): return {"message": f"Hello {user.email}!"} -register_tortoise( - app, - db_url=DATABASE_URL, - modules={"models": ["app.models"]}, - generate_schemas=True, -) +@app.on_event("startup") +async def on_startup(): + await init_beanie( + database=db, + document_models=[ + User, + ], + ) diff --git a/examples/beanie-oauth/app/db.py b/examples/beanie-oauth/app/db.py new file mode 100644 index 00000000..835ddd21 --- /dev/null +++ b/examples/beanie-oauth/app/db.py @@ -0,0 +1,24 @@ +from typing import List + +import motor.motor_asyncio +from beanie import PydanticObjectId +from fastapi_users.db import BaseOAuthAccount, BeanieBaseUser, BeanieUserDatabase +from pydantic import Field + +DATABASE_URL = "mongodb://localhost:27017" +client = motor.motor_asyncio.AsyncIOMotorClient( + DATABASE_URL, uuidRepresentation="standard" +) +db = client["database_name"] + + +class OAuthAccount(BaseOAuthAccount): + pass + + +class User(BeanieBaseUser[PydanticObjectId]): + oauth_accounts: List[OAuthAccount] = Field(default_factory=list) + + +async def get_user_db(): + yield BeanieUserDatabase(User) diff --git a/examples/beanie-oauth/app/schemas.py b/examples/beanie-oauth/app/schemas.py new file mode 100644 index 00000000..f9b2b9a1 --- /dev/null +++ b/examples/beanie-oauth/app/schemas.py @@ -0,0 +1,14 @@ +from beanie import PydanticObjectId +from fastapi_users import schemas + + +class UserRead(schemas.BaseUser[PydanticObjectId]): + pass + + +class UserCreate(schemas.BaseUserCreate): + pass + + +class UserUpdate(schemas.BaseUserUpdate): + pass diff --git a/examples/mongodb-oauth/app/users.py b/examples/beanie-oauth/app/users.py similarity index 60% rename from examples/mongodb-oauth/app/users.py rename to examples/beanie-oauth/app/users.py index 2ba395ed..81ad5ca4 100644 --- a/examples/mongodb-oauth/app/users.py +++ b/examples/beanie-oauth/app/users.py @@ -1,6 +1,7 @@ import os from typing import Optional +from beanie import PydanticObjectId from fastapi import Depends, Request from fastapi_users import BaseUserManager, FastAPIUsers from fastapi_users.authentication import ( @@ -8,41 +9,38 @@ from fastapi_users.authentication import ( BearerTransport, JWTStrategy, ) -from fastapi_users.db import MongoDBUserDatabase +from fastapi_users.db import BeanieUserDatabase, ObjectIDIDMixin from httpx_oauth.clients.google import GoogleOAuth2 -from app.db import get_user_db -from app.models import User, UserCreate, UserDB, UserUpdate +from app.db import User, get_user_db SECRET = "SECRET" - google_oauth_client = GoogleOAuth2( - os.environ["GOOGLE_OAUTH_CLIENT_ID"], - os.environ["GOOGLE_OAUTH_CLIENT_SECRET"], + os.getenv("GOOGLE_OAUTH_CLIENT_ID", ""), + os.getenv("GOOGLE_OAUTH_CLIENT_SECRET", ""), ) -class UserManager(BaseUserManager[UserCreate, UserDB]): - user_db_model = UserDB +class UserManager(ObjectIDIDMixin, BaseUserManager[User, PydanticObjectId]): reset_password_token_secret = SECRET verification_token_secret = SECRET - async def on_after_register(self, user: UserDB, request: Optional[Request] = None): + async def on_after_register(self, user: User, request: Optional[Request] = None): print(f"User {user.id} has registered.") async def on_after_forgot_password( - self, user: UserDB, token: str, request: Optional[Request] = None + self, user: User, token: str, request: Optional[Request] = None ): print(f"User {user.id} has forgot their password. Reset token: {token}") async def on_after_request_verify( - self, user: UserDB, token: str, request: Optional[Request] = None + self, user: User, token: str, request: Optional[Request] = None ): print(f"Verification requested for user {user.id}. Verification token: {token}") -async def get_user_manager(user_db: MongoDBUserDatabase = Depends(get_user_db)): +async def get_user_manager(user_db: BeanieUserDatabase = Depends(get_user_db)): yield UserManager(user_db) @@ -58,13 +56,7 @@ auth_backend = AuthenticationBackend( transport=bearer_transport, get_strategy=get_jwt_strategy, ) -fastapi_users = FastAPIUsers( - get_user_manager, - [auth_backend], - User, - UserCreate, - UserUpdate, - UserDB, -) + +fastapi_users = FastAPIUsers[User, PydanticObjectId](get_user_manager, [auth_backend]) current_active_user = fastapi_users.current_user(active=True) diff --git a/examples/beanie-oauth/main.py b/examples/beanie-oauth/main.py new file mode 100644 index 00000000..eb1edd12 --- /dev/null +++ b/examples/beanie-oauth/main.py @@ -0,0 +1,4 @@ +import uvicorn + +if __name__ == "__main__": + uvicorn.run("app.app:app", host="0.0.0.0", log_level="info") diff --git a/examples/beanie-oauth/requirements.txt b/examples/beanie-oauth/requirements.txt new file mode 100644 index 00000000..ea30f98f --- /dev/null +++ b/examples/beanie-oauth/requirements.txt @@ -0,0 +1,3 @@ +fastapi +fastapi-users[beanie] +uvicorn[standard] diff --git a/examples/mongodb-oauth/app/app.py b/examples/mongodb-oauth/app/app.py deleted file mode 100644 index 2d73c0fa..00000000 --- a/examples/mongodb-oauth/app/app.py +++ /dev/null @@ -1,37 +0,0 @@ -from fastapi import Depends, FastAPI - -from app.models import UserDB -from app.users import ( - auth_backend, - current_active_user, - fastapi_users, - google_oauth_client, -) - -app = FastAPI() - -app.include_router( - fastapi_users.get_auth_router(auth_backend), prefix="/auth/jwt", tags=["auth"] -) -app.include_router(fastapi_users.get_register_router(), prefix="/auth", tags=["auth"]) -app.include_router( - fastapi_users.get_reset_password_router(), - prefix="/auth", - tags=["auth"], -) -app.include_router( - fastapi_users.get_verify_router(), - prefix="/auth", - tags=["auth"], -) -app.include_router(fastapi_users.get_users_router(), prefix="/users", tags=["users"]) -app.include_router( - fastapi_users.get_oauth_router(google_oauth_client, auth_backend, "SECRET"), - prefix="/auth/google", - tags=["auth"], -) - - -@app.get("/authenticated-route") -async def authenticated_route(user: UserDB = Depends(current_active_user)): - return {"message": f"Hello {user.email}!"} diff --git a/examples/mongodb-oauth/app/db.py b/examples/mongodb-oauth/app/db.py deleted file mode 100644 index e9e3ce77..00000000 --- a/examples/mongodb-oauth/app/db.py +++ /dev/null @@ -1,17 +0,0 @@ -import os - -import motor.motor_asyncio -from fastapi_users.db import MongoDBUserDatabase - -from app.models import UserDB - -DATABASE_URL = os.environ["DATABASE_URL"] -client = motor.motor_asyncio.AsyncIOMotorClient( - DATABASE_URL, uuidRepresentation="standard" -) -db = client["database_name"] -collection = db["users"] - - -async def get_user_db(): - yield MongoDBUserDatabase(UserDB, collection) diff --git a/examples/mongodb-oauth/app/models.py b/examples/mongodb-oauth/app/models.py deleted file mode 100644 index 28563211..00000000 --- a/examples/mongodb-oauth/app/models.py +++ /dev/null @@ -1,17 +0,0 @@ -from fastapi_users import models, schemas - - -class User(schemas.BaseUser, schemas.BaseOAuthAccountMixin): - pass - - -class UserCreate(schemas.BaseUserCreate): - pass - - -class UserUpdate(schemas.BaseUserUpdate): - pass - - -class UserDB(User, schemas.BaseUserDB): - pass diff --git a/examples/mongodb-oauth/main.py b/examples/mongodb-oauth/main.py deleted file mode 100644 index 1b47137b..00000000 --- a/examples/mongodb-oauth/main.py +++ /dev/null @@ -1,4 +0,0 @@ -import uvicorn - -if __name__ == "__main__": - uvicorn.run("app.app:app", host="0.0.0.0", port=5000, log_level="info") diff --git a/examples/mongodb-oauth/requirements.txt b/examples/mongodb-oauth/requirements.txt deleted file mode 100644 index 8f0ff9d3..00000000 --- a/examples/mongodb-oauth/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -fastapi -fastapi-users[mongodb,oauth] -uvicorn[standard] diff --git a/examples/sqlalchemy-oauth/app/app.py b/examples/sqlalchemy-oauth/app/app.py index d9ffac76..ccd061fc 100644 --- a/examples/sqlalchemy-oauth/app/app.py +++ b/examples/sqlalchemy-oauth/app/app.py @@ -1,7 +1,7 @@ from fastapi import Depends, FastAPI -from app.db import create_db_and_tables -from app.models import UserDB +from app.db import User, create_db_and_tables +from app.schemas import UserCreate, UserRead, UserUpdate from app.users import ( auth_backend, current_active_user, @@ -14,18 +14,26 @@ app = FastAPI() app.include_router( fastapi_users.get_auth_router(auth_backend), prefix="/auth/jwt", tags=["auth"] ) -app.include_router(fastapi_users.get_register_router(), prefix="/auth", tags=["auth"]) +app.include_router( + fastapi_users.get_register_router(UserRead, UserCreate), + prefix="/auth", + tags=["auth"], +) app.include_router( fastapi_users.get_reset_password_router(), prefix="/auth", tags=["auth"], ) app.include_router( - fastapi_users.get_verify_router(), + fastapi_users.get_verify_router(UserRead), prefix="/auth", tags=["auth"], ) -app.include_router(fastapi_users.get_users_router(), prefix="/users", tags=["users"]) +app.include_router( + fastapi_users.get_users_router(UserRead, UserUpdate), + prefix="/users", + tags=["users"], +) app.include_router( fastapi_users.get_oauth_router(google_oauth_client, auth_backend, "SECRET"), prefix="/auth/google", @@ -34,7 +42,7 @@ app.include_router( @app.get("/authenticated-route") -async def authenticated_route(user: UserDB = Depends(current_active_user)): +async def authenticated_route(user: User = Depends(current_active_user)): return {"message": f"Hello {user.email}!"} diff --git a/examples/sqlalchemy-oauth/app/db.py b/examples/sqlalchemy-oauth/app/db.py index ea56bcb2..87d6fbbd 100644 --- a/examples/sqlalchemy-oauth/app/db.py +++ b/examples/sqlalchemy-oauth/app/db.py @@ -1,29 +1,27 @@ -from typing import AsyncGenerator +from typing import AsyncGenerator, List from fastapi import Depends from fastapi_users.db import ( - SQLAlchemyBaseOAuthAccountTable, - SQLAlchemyBaseUserTable, + SQLAlchemyBaseOAuthAccountTableUUID, + SQLAlchemyBaseUserTableUUID, SQLAlchemyUserDatabase, ) from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine from sqlalchemy.ext.declarative import DeclarativeMeta, declarative_base from sqlalchemy.orm import relationship, sessionmaker -from app.models import UserDB - DATABASE_URL = "sqlite+aiosqlite:///./test.db" Base: DeclarativeMeta = declarative_base() -class UserTable(Base, SQLAlchemyBaseUserTable): - oauth_accounts = relationship("OAuthAccountTable") - - -class OAuthAccountTable(SQLAlchemyBaseOAuthAccountTable, Base): +class OAuthAccount(SQLAlchemyBaseOAuthAccountTableUUID, Base): pass +class User(SQLAlchemyBaseUserTableUUID, Base): + oauth_accounts: List[OAuthAccount] = relationship("OAuthAccount", lazy="joined") + + engine = create_async_engine(DATABASE_URL) async_session_maker = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) @@ -39,4 +37,4 @@ async def get_async_session() -> AsyncGenerator[AsyncSession, None]: async def get_user_db(session: AsyncSession = Depends(get_async_session)): - yield SQLAlchemyUserDatabase(UserDB, session, UserTable, OAuthAccountTable) + yield SQLAlchemyUserDatabase(session, User, OAuthAccount) diff --git a/examples/sqlalchemy-oauth/app/models.py b/examples/sqlalchemy-oauth/app/models.py deleted file mode 100644 index 28563211..00000000 --- a/examples/sqlalchemy-oauth/app/models.py +++ /dev/null @@ -1,17 +0,0 @@ -from fastapi_users import models, schemas - - -class User(schemas.BaseUser, schemas.BaseOAuthAccountMixin): - pass - - -class UserCreate(schemas.BaseUserCreate): - pass - - -class UserUpdate(schemas.BaseUserUpdate): - pass - - -class UserDB(User, schemas.BaseUserDB): - pass diff --git a/examples/sqlalchemy-oauth/app/schemas.py b/examples/sqlalchemy-oauth/app/schemas.py new file mode 100644 index 00000000..de1169e4 --- /dev/null +++ b/examples/sqlalchemy-oauth/app/schemas.py @@ -0,0 +1,15 @@ +import uuid + +from fastapi_users import schemas + + +class UserRead(schemas.BaseUser[uuid.UUID]): + pass + + +class UserCreate(schemas.BaseUserCreate): + pass + + +class UserUpdate(schemas.BaseUserUpdate): + pass diff --git a/examples/sqlalchemy-oauth/app/users.py b/examples/sqlalchemy-oauth/app/users.py index d615d32b..0b61b2a5 100644 --- a/examples/sqlalchemy-oauth/app/users.py +++ b/examples/sqlalchemy-oauth/app/users.py @@ -1,8 +1,9 @@ import os +import uuid from typing import Optional from fastapi import Depends, Request -from fastapi_users import BaseUserManager, FastAPIUsers +from fastapi_users import BaseUserManager, FastAPIUsers, UUIDIDMixin from fastapi_users.authentication import ( AuthenticationBackend, BearerTransport, @@ -11,33 +12,30 @@ from fastapi_users.authentication import ( from fastapi_users.db import SQLAlchemyUserDatabase from httpx_oauth.clients.google import GoogleOAuth2 -from app.db import get_user_db -from app.models import User, UserCreate, UserDB, UserUpdate +from app.db import User, get_user_db SECRET = "SECRET" - google_oauth_client = GoogleOAuth2( os.getenv("GOOGLE_OAUTH_CLIENT_ID", ""), os.getenv("GOOGLE_OAUTH_CLIENT_SECRET", ""), ) -class UserManager(BaseUserManager[UserCreate, UserDB]): - user_db_model = UserDB +class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]): reset_password_token_secret = SECRET verification_token_secret = SECRET - async def on_after_register(self, user: UserDB, request: Optional[Request] = None): + async def on_after_register(self, user: User, request: Optional[Request] = None): print(f"User {user.id} has registered.") async def on_after_forgot_password( - self, user: UserDB, token: str, request: Optional[Request] = None + self, user: User, token: str, request: Optional[Request] = None ): print(f"User {user.id} has forgot their password. Reset token: {token}") async def on_after_request_verify( - self, user: UserDB, token: str, request: Optional[Request] = None + self, user: User, token: str, request: Optional[Request] = None ): print(f"Verification requested for user {user.id}. Verification token: {token}") @@ -58,13 +56,7 @@ auth_backend = AuthenticationBackend( transport=bearer_transport, get_strategy=get_jwt_strategy, ) -fastapi_users = FastAPIUsers( - get_user_manager, - [auth_backend], - User, - UserCreate, - UserUpdate, - UserDB, -) + +fastapi_users = FastAPIUsers[User, uuid.UUID](get_user_manager, [auth_backend]) current_active_user = fastapi_users.current_user(active=True) diff --git a/examples/sqlalchemy-oauth/requirements.txt b/examples/sqlalchemy-oauth/requirements.txt index 226c8ced..6407e81e 100644 --- a/examples/sqlalchemy-oauth/requirements.txt +++ b/examples/sqlalchemy-oauth/requirements.txt @@ -1,4 +1,4 @@ fastapi -fastapi-users[sqlalchemy2,oauth] +fastapi-users[sqlalchemy] uvicorn[standard] aiosqlite diff --git a/examples/tortoise-oauth/app/__init__.py b/examples/tortoise-oauth/app/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/tortoise-oauth/app/db.py b/examples/tortoise-oauth/app/db.py deleted file mode 100644 index 83b1571f..00000000 --- a/examples/tortoise-oauth/app/db.py +++ /dev/null @@ -1,9 +0,0 @@ -from fastapi_users.db import TortoiseUserDatabase - -from app.models import OAuthAccount, UserDB, UserModel - -DATABASE_URL = "sqlite://./test.db" - - -async def get_user_db(): - yield TortoiseUserDatabase(UserDB, UserModel, OAuthAccount) diff --git a/examples/tortoise-oauth/app/models.py b/examples/tortoise-oauth/app/models.py deleted file mode 100644 index c7da4abe..00000000 --- a/examples/tortoise-oauth/app/models.py +++ /dev/null @@ -1,30 +0,0 @@ -from fastapi_users import models, schemas -from fastapi_users.db import TortoiseBaseOAuthAccountModel, TortoiseBaseUserModel -from tortoise import fields -from tortoise.contrib.pydantic import PydanticModel - - -class User(schemas.BaseUser, schemas.BaseOAuthAccountMixin): - pass - - -class UserCreate(schemas.BaseUserCreate): - pass - - -class UserUpdate(schemas.BaseUserUpdate): - pass - - -class UserModel(TortoiseBaseUserModel): - pass - - -class UserDB(User, schemas.BaseUserDB, PydanticModel): - class Config: - orm_mode = True - orig_model = UserModel - - -class OAuthAccount(TortoiseBaseOAuthAccountModel): - user = fields.ForeignKeyField("models.UserModel", related_name="oauth_accounts") diff --git a/examples/tortoise-oauth/app/users.py b/examples/tortoise-oauth/app/users.py deleted file mode 100644 index 49f672f7..00000000 --- a/examples/tortoise-oauth/app/users.py +++ /dev/null @@ -1,70 +0,0 @@ -import os -from typing import Optional - -from fastapi import Depends, Request -from fastapi_users import BaseUserManager, FastAPIUsers -from fastapi_users.authentication import ( - AuthenticationBackend, - BearerTransport, - JWTStrategy, -) -from fastapi_users.db import TortoiseUserDatabase -from httpx_oauth.clients.google import GoogleOAuth2 - -from app.db import get_user_db -from app.models import User, UserCreate, UserDB, UserUpdate - -SECRET = "SECRET" - - -google_oauth_client = GoogleOAuth2( - os.environ["GOOGLE_OAUTH_CLIENT_ID"], - os.environ["GOOGLE_OAUTH_CLIENT_SECRET"], -) - - -class UserManager(BaseUserManager[UserCreate, UserDB]): - user_db_model = UserDB - reset_password_token_secret = SECRET - verification_token_secret = SECRET - - async def on_after_register(self, user: UserDB, request: Optional[Request] = None): - print(f"User {user.id} has registered.") - - async def on_after_forgot_password( - self, user: UserDB, token: str, request: Optional[Request] = None - ): - print(f"User {user.id} has forgot their password. Reset token: {token}") - - async def on_after_request_verify( - self, user: UserDB, token: str, request: Optional[Request] = None - ): - print(f"Verification requested for user {user.id}. Verification token: {token}") - - -async def get_user_manager(user_db: TortoiseUserDatabase = Depends(get_user_db)): - yield UserManager(user_db) - - -bearer_transport = BearerTransport(tokenUrl="auth/jwt/login") - - -def get_jwt_strategy() -> JWTStrategy: - return JWTStrategy(secret=SECRET, lifetime_seconds=3600) - - -auth_backend = AuthenticationBackend( - name="jwt", - transport=bearer_transport, - get_strategy=get_jwt_strategy, -) -fastapi_users = FastAPIUsers( - get_user_manager, - [auth_backend], - User, - UserCreate, - UserUpdate, - UserDB, -) - -current_active_user = fastapi_users.current_user(active=True) diff --git a/examples/tortoise-oauth/main.py b/examples/tortoise-oauth/main.py deleted file mode 100644 index 1b47137b..00000000 --- a/examples/tortoise-oauth/main.py +++ /dev/null @@ -1,4 +0,0 @@ -import uvicorn - -if __name__ == "__main__": - uvicorn.run("app.app:app", host="0.0.0.0", port=5000, log_level="info") diff --git a/examples/tortoise-oauth/requirements.txt b/examples/tortoise-oauth/requirements.txt deleted file mode 100644 index d5642ec1..00000000 --- a/examples/tortoise-oauth/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -fastapi -fastapi-users[tortoise-orm,oauth] -uvicorn[standard] diff --git a/examples/tortoise/app/__init__.py b/examples/tortoise/app/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/examples/tortoise/app/app.py b/examples/tortoise/app/app.py deleted file mode 100644 index aca846a2..00000000 --- a/examples/tortoise/app/app.py +++ /dev/null @@ -1,37 +0,0 @@ -from fastapi import Depends, FastAPI -from tortoise.contrib.fastapi import register_tortoise - -from app.db import DATABASE_URL -from app.models import UserDB -from app.users import auth_backend, current_active_user, fastapi_users - -app = FastAPI() - -app.include_router( - fastapi_users.get_auth_router(auth_backend), prefix="/auth/jwt", tags=["auth"] -) -app.include_router(fastapi_users.get_register_router(), prefix="/auth", tags=["auth"]) -app.include_router( - fastapi_users.get_reset_password_router(), - prefix="/auth", - tags=["auth"], -) -app.include_router( - fastapi_users.get_verify_router(), - prefix="/auth", - tags=["auth"], -) -app.include_router(fastapi_users.get_users_router(), prefix="/users", tags=["users"]) - - -@app.get("/authenticated-route") -async def authenticated_route(user: UserDB = Depends(current_active_user)): - return {"message": f"Hello {user.email}!"} - - -register_tortoise( - app, - db_url=DATABASE_URL, - modules={"models": ["app.models"]}, - generate_schemas=True, -) diff --git a/examples/tortoise/app/db.py b/examples/tortoise/app/db.py deleted file mode 100644 index d6fd9349..00000000 --- a/examples/tortoise/app/db.py +++ /dev/null @@ -1,9 +0,0 @@ -from fastapi_users.db import TortoiseUserDatabase - -from app.models import UserDB, UserModel - -DATABASE_URL = "sqlite://./test.db" - - -async def get_user_db(): - yield TortoiseUserDatabase(UserDB, UserModel) diff --git a/examples/tortoise/app/models.py b/examples/tortoise/app/models.py deleted file mode 100644 index 54fc0217..00000000 --- a/examples/tortoise/app/models.py +++ /dev/null @@ -1,25 +0,0 @@ -from fastapi_users import models, schemas -from fastapi_users.db import TortoiseBaseUserModel -from tortoise.contrib.pydantic import PydanticModel - - -class User(schemas.BaseUser): - pass - - -class UserCreate(schemas.BaseUserCreate): - pass - - -class UserUpdate(schemas.BaseUserUpdate): - pass - - -class UserModel(TortoiseBaseUserModel): - pass - - -class UserDB(User, schemas.BaseUserDB, PydanticModel): - class Config: - orm_mode = True - orig_model = UserModel diff --git a/examples/tortoise/app/users.py b/examples/tortoise/app/users.py deleted file mode 100644 index 20f4c565..00000000 --- a/examples/tortoise/app/users.py +++ /dev/null @@ -1,62 +0,0 @@ -from typing import Optional - -from fastapi import Depends, Request -from fastapi_users import BaseUserManager, FastAPIUsers -from fastapi_users.authentication import ( - AuthenticationBackend, - BearerTransport, - JWTStrategy, -) -from fastapi_users.db import TortoiseUserDatabase - -from app.db import get_user_db -from app.models import User, UserCreate, UserDB, UserUpdate - -SECRET = "SECRET" - - -class UserManager(BaseUserManager[UserCreate, UserDB]): - user_db_model = UserDB - reset_password_token_secret = SECRET - verification_token_secret = SECRET - - async def on_after_register(self, user: UserDB, request: Optional[Request] = None): - print(f"User {user.id} has registered.") - - async def on_after_forgot_password( - self, user: UserDB, token: str, request: Optional[Request] = None - ): - print(f"User {user.id} has forgot their password. Reset token: {token}") - - async def on_after_request_verify( - self, user: UserDB, token: str, request: Optional[Request] = None - ): - print(f"Verification requested for user {user.id}. Verification token: {token}") - - -async def get_user_manager(user_db: TortoiseUserDatabase = Depends(get_user_db)): - yield UserManager(user_db) - - -bearer_transport = BearerTransport(tokenUrl="auth/jwt/login") - - -def get_jwt_strategy() -> JWTStrategy: - return JWTStrategy(secret=SECRET, lifetime_seconds=3600) - - -auth_backend = AuthenticationBackend( - name="jwt", - transport=bearer_transport, - get_strategy=get_jwt_strategy, -) -fastapi_users = FastAPIUsers( - get_user_manager, - [auth_backend], - User, - UserCreate, - UserUpdate, - UserDB, -) - -current_active_user = fastapi_users.current_user(active=True) diff --git a/examples/tortoise/main.py b/examples/tortoise/main.py deleted file mode 100644 index 1b47137b..00000000 --- a/examples/tortoise/main.py +++ /dev/null @@ -1,4 +0,0 @@ -import uvicorn - -if __name__ == "__main__": - uvicorn.run("app.app:app", host="0.0.0.0", port=5000, log_level="info") diff --git a/examples/tortoise/requirements.txt b/examples/tortoise/requirements.txt deleted file mode 100644 index 7aec70ac..00000000 --- a/examples/tortoise/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -fastapi -fastapi-users[tortoise-orm] -uvicorn[standard]