Native model and generic ID (#971)

* Use a generic Protocol model for User instead of Pydantic

* Remove UserDB Pydantic schema

* Harmonize schema variable naming to avoid confusions

* Revamp OAuth account model management

* Revamp AccessToken DB strategy to adopt generic model approach

* Make ID a generic instead of forcing UUIDs

* Improve generic typing

* Improve Strategy typing

* Tweak base DB typing

* Don't set Pydantic schemas on FastAPIUsers class: pass it directly on router creation

* Add IntegerIdMixin and export related classes

* Start to revamp doc for V10

* Revamp OAuth documentation

* Fix code highlights

* Write the 9.x.x ➡️ 10.x.x migration doc

* Fix pyproject.toml
This commit is contained in:
François Voron
2022-05-05 14:51:19 +02:00
committed by GitHub
parent b7734fc8b0
commit 72aa68c462
124 changed files with 2144 additions and 2114 deletions

View File

@@ -37,10 +37,8 @@ Add quickly a registration and authentication system to your [FastAPI](https://f
* [X] Dependency callables to inject current user in route
* [X] Pluggable password validation
* [X] Customizable database backend
* [X] [SQLAlchemy ORM async](https://docs.sqlalchemy.org/en/14/orm/extensions/asyncio.html) backend included
* [X] MongoDB async backend included thanks to [mongodb/motor](https://github.com/mongodb/motor)
* [X] [Tortoise ORM](https://tortoise-orm.readthedocs.io/en/latest/) backend included
* [X] [ormar](https://collerek.github.io/ormar/) backend included
* [X] [SQLAlchemy ORM async](https://docs.sqlalchemy.org/en/14/orm/extensions/asyncio.html) included
* [X] [MongoDB with Beanie ODM](https://github.com/roman-right/beanie/) included
* [X] Multiple customizable authentication backends
* [X] Transports: Authorization header, Cookie
* [X] Strategies: JWT, Database, Redis

View File

@@ -4,68 +4,72 @@ The most natural way for storing tokens is of course the very same database you'
## Configuration
The configuration of this strategy is a bit more complex than the others as it requires you to configure models and a database adapter, [exactly like we did for users](../../overview.md#database-adapters).
The configuration of this strategy is a bit more complex than the others as it requires you to configure models and a database adapter, [exactly like we did for users](../../overview.md#user-model-and-database-adapters).
### Model
### Database adapters
You should define an `AccessToken` Pydantic model inheriting from `BaseAccessToken`.
```py
from fastapi_users.authentication.strategy.db import BaseAccessToken
class AccessToken(BaseAccessToken):
pass
```
It is structured like this:
An access token will be structured like this in your database:
* `token` (`str`) Unique identifier of the token. It's generated automatically upon login by the strategy.
* `user_id` (`UUID4`) User id. of the user associated to this token.
* `user_id` (`ID`) User id. of the user associated to this token.
* `created_at` (`datetime`) Date and time of creation of the token. It's used to determine if the token is expired or not.
### Database adapter
We are providing a base model with those fields for each database we are supporting.
=== "SQLAlchemy"
#### SQLAlchemy
```py hl_lines="5-8 13 23-24 45-46"
--8<-- "docs/src/db_sqlalchemy_access_tokens.py"
We'll expand from the basic SQLAlchemy configuration.
```py hl_lines="5-8 21-22 43-46"
--8<-- "docs/src/db_sqlalchemy_access_tokens.py"
```
1. We define an `AccessToken` ORM model inheriting from `SQLAlchemyBaseAccessTokenTableUUID`.
2. We define a dependency to instantiate the `SQLAlchemyAccessTokenDatabase` class. Just like the user database adapter, it expects a fresh SQLAlchemy session and the `AccessToken` model class we defined above.
!!! tip "`user_id` foreign key is defined as UUID"
By default, we use UUID as a primary key ID for your user, so we follow the same convention to define the foreign key pointing to the user.
If you want to use another type, like an auto-incremented integer, you can use `SQLAlchemyBaseAccessTokenTable` as base class and define your own `user_id` column.
```py
class AccessToken(SQLAlchemyBaseAccessTokenTable[int], Base):
@declared_attr
def user_id(cls):
return Column(Integer, ForeignKey("user.id", ondelete="cascade"), nullable=False)
```
=== "Tortoise ORM"
Notice that `SQLAlchemyBaseAccessTokenTable` expects a generic type to define the actual type of ID you use.
With Tortoise ORM, you need to define a proper Tortoise model for `AccessToken` and manually specify the user foreign key. Besides, you need to modify the Pydantic model a bit so that it works well with this Tortoise model.
#### Beanie
=== "model.py"
```py hl_lines="2 4 31-38"
--8<-- "docs/src/db_tortoise_access_tokens_model.py"
```
We'll expand from the basic Beanie configuration.
=== "adapter.py"
```py hl_lines="2 4 13-14"
--8<-- "docs/src/db_tortoise_access_tokens_adapter.py"
```
```py hl_lines="4-7 20-21 28-29"
--8<-- "docs/src/db_beanie_access_tokens.py"
```
=== "MongoDB"
1. We define an `AccessToken` ODM model inheriting from `BeanieBaseAccessToken`. Notice that we set a generic type to define the type of the `user_id` reference. By default, it's a standard MongoDB ObjectID.
```py hl_lines="3 5 13 20-21"
--8<-- "docs/src/db_mongodb_access_tokens.py"
```
2. We define a dependency to instantiate the `BeanieAccessTokenDatabase` class. Just like the user database adapter, it expects the `AccessToken` model class we defined above.
### Strategy
```py
import uuid
from fastapi import Depends
from fastapi_users.authentication.strategy.db import AccessTokenDatabase, DatabaseStrategy
from .models import AccessToken, UserCreate, UserDB
from .db import AccessToken, User
def get_database_strategy(
access_token_db: AccessTokenDatabase[AccessToken] = Depends(get_access_token_db),
) -> DatabaseStrategy[UserCreate, UserDB, AccessToken]:
) -> DatabaseStrategy:
return DatabaseStrategy(access_token_db, lifetime_seconds=3600)
```

View File

@@ -0,0 +1,75 @@
# Beanie
**FastAPI Users** provides the necessary tools to work with MongoDB databases using the [Beanie ODM](https://github.com/roman-right/beanie).
## Setup database connection and collection
The first thing to do is to create a MongoDB connection using [mongodb/motor](https://github.com/mongodb/motor) (automatically installed with Beanie).
```py hl_lines="5-9"
--8<-- "docs/src/db_beanie.py"
```
You can choose any name for the database.
## Create the User model
As for any Beanie ODM model, we'll create a `User` model.
```py hl_lines="12-13"
--8<-- "docs/src/db_beanie.py"
```
As you can see, **FastAPI Users** provides a base class that will include base fields for our `User` table. You can of course add you own fields there to fit to your needs!
!!! tip "Document ID is a MongoDB ObjectID"
Beanie [automatically manages document ID](https://roman-right.github.io/beanie/tutorial/defining-a-document/#id) by encoding/decoding MongoDB ObjectID.
If you want to use another type, like UUID, you can override the `id` field:
```py
import uuid
from pydantic import Field
class User(BeanieBaseUser[uuid.UUID]):
id: uuid.UUID = Field(default_factory=uuid.uuid4)
```
Notice that `BeanieBaseUser` expects a generic type to define the actual type of ID you use.
!!! info
The base class is configured to automatically create a [unique index](https://roman-right.github.io/beanie/tutorial/defining-a-document/#indexes) on `id` and `email`.
## Create the database adapter
The database adapter of **FastAPI Users** makes the link between your database configuration and the users logic. It should be generated by a FastAPI dependency.
```py hl_lines="16-17"
--8<-- "docs/src/db_beanie.py"
```
Notice that we pass a reference to the `User` model we defined above.
## Initialize Beanie
When initializing your FastAPI app, it's important that you [**initialize Beanie**](https://roman-right.github.io/beanie/tutorial/initialization/) so it can discover your models. We can achieve this using a startup event handler on the FastAPI app:
```py
from beanie import init_beanie
@app.on_event("startup")
async def on_startup():
await init_beanie(
database=db, # (1)!
document_models=[
User, # (2)!
],
)
```
1. This is the `db` Motor database instance we defined above.
2. This is the Beanie `User` model we defined above. Don't forget to also add your very own models!

View File

@@ -1,32 +0,0 @@
# MongoDB
**FastAPI Users** provides the necessary tools to work with MongoDB databases thanks to [mongodb/motor](https://github.com/mongodb/motor) package for full async support.
## Setup database connection and collection
Let's create a MongoDB connection and instantiate a collection.
```py hl_lines="6 7 8 9 10 11"
--8<-- "docs/src/db_mongodb.py"
```
You can choose any name for the database and the collection.
!!! warning
You may have noticed the `uuidRepresentation` parameter. It controls how the UUID values will be encoded in the database. By default, it's set to `pythonLegacy` but new applications should consider setting this to `standard` for cross language compatibility. [Read more about this](https://pymongo.readthedocs.io/en/stable/api/pymongo/mongo_client.html#pymongo.mongo_client.MongoClient).
## Create the database adapter
The database adapter of **FastAPI Users** makes the link between your database configuration and the users logic. It should be generated by a FastAPI dependency.
```py hl_lines="14 15"
--8<-- "docs/src/db_mongodb.py"
```
Notice that we pass a reference to your [`UserDB` model](../models.md).
!!! info
The database adapter will automatically create a [unique index](https://docs.mongodb.com/manual/core/index-unique/) on `id` and `email`.
!!! warning
**FastAPI Users** will use its defined [`id` UUID](../models.md) as unique identifier for the user, rather than the builtin MongoDB `_id`.

View File

@@ -1,49 +0,0 @@
# Ormar
**FastAPI Users** provides the necessary tools to work with ormar.
## Installation
Install the database driver that corresponds to your DBMS:
```sh
pip install asyncpg psycopg2
```
```sh
pip install aiomysql pymysql
```
```sh
pip install aiosqlite
```
For the sake of this tutorial from now on, we'll use a simple SQLite database.
## Setup User table
Let's declare our User ORM model.
```py hl_lines="12-16"
--8<-- "docs/src/db_ormar.py"
```
As you can see, **FastAPI Users** provides an abstract model that will
include base fields for our User table. You can of course add you own fields
there to fit to your needs!
## Create the database adapter
The database adapter of **FastAPI Users** makes the link between your
database configuration and the users logic. It should be generated by a FastAPI dependency.
```py hl_lines="23-24"
--8<-- "docs/src/db_ormar.py"
```
Notice that we pass a reference to your [`UserDB` model](../models.md).
!!! warning
In production, it's strongly recommended to setup a migration system to
update your SQL schemas. See
[Alembic](https://alembic.sqlalchemy.org/en/latest/).

View File

@@ -2,9 +2,6 @@
**FastAPI Users** provides the necessary tools to work with SQL databases thanks to [SQLAlchemy ORM with asyncio](https://docs.sqlalchemy.org/en/14/orm/extensions/asyncio.html).
!!! warning
The previous adapter using `encode/databases` is now deprecated but can still be installed using `fastapi-users[sqlalchemy]`.
## Asynchronous driver
To work with your DBMS, you'll need to install the corresponding asyncio driver. The common choices are:
@@ -14,21 +11,31 @@ To work with your DBMS, you'll need to install the corresponding asyncio driver.
For the sake of this tutorial from now on, we'll use a simple SQLite databse.
## Setup User table
## Create the User model
Let's declare our SQLAlchemy `User` table.
As for any SQLAlchemy ORM model, we'll create a `User` model.
```py hl_lines="15 16"
```py hl_lines="13-14"
--8<-- "docs/src/db_sqlalchemy.py"
```
As you can see, **FastAPI Users** provides a mixin that will include base fields for our `User` table. You can of course add you own fields there to fit to your needs!
As you can see, **FastAPI Users** provides a base class that will include base fields for our `User` table. You can of course add you own fields there to fit to your needs!
!!! 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 `SQLAlchemyBaseUserTable` as base class and define your own `id` column.
```py
class User(SQLAlchemyBaseUserTable[int], Base):
id = Column(Integer, primary_key=True)
```
Notice that `SQLAlchemyBaseUserTable` expects a generic type to define the actual type of ID you use.
## Implement a function to create the tables
We'll now create an utility function to create all the defined tables.
```py hl_lines="23-25"
```py hl_lines="21-23"
--8<-- "docs/src/db_sqlalchemy.py"
```
@@ -41,14 +48,13 @@ This function can be called, for example, during the initialization of your Fast
The database adapter of **FastAPI Users** makes the link between your database configuration and the users logic. It should be generated by a FastAPI dependency.
```py hl_lines="28-34"
```py hl_lines="26-33"
--8<-- "docs/src/db_sqlalchemy.py"
```
Notice that we define first a `get_async_session` dependency returning us a fresh SQLAlchemy session to interact with the database.
It's then used inside the `get_user_db` dependency to generate our adapter. Notice that we pass it three things:
It's then used inside the `get_user_db` dependency to generate our adapter. Notice that we pass it two things:
* A reference to your [`UserDB` model](../models.md).
* The `session` instance we just injected.
* The `UserTable` variable, which is the actual SQLAlchemy model.
* The `User` class, which is the actual SQLAlchemy model.

View File

@@ -1,69 +0,0 @@
# Tortoise ORM
**FastAPI Users** provides the necessary tools to work with Tortoise ORM.
## Installation
Install the database driver that corresponds to your DBMS:
```sh
pip install asyncpg
```
```sh
pip install aiomysql
```
```sh
pip install aiosqlite
```
For the sake of this tutorial from now on, we'll use a simple SQLite databse.
## Setup User table and model
Let's declare our User ORM model.
```py hl_lines="18 19"
--8<-- "docs/src/db_tortoise_model.py"
```
As you can see, **FastAPI Users** provides an abstract model that will include base fields for our User table. You can of course add you own fields there to fit to your needs!
In order to make the Pydantic model and the Tortoise ORM model working well together, you'll have to add a mixin and some configuration options to your `UserDB` model. Tortoise ORM provides [utilities to ease the integration with Pydantic](https://tortoise-orm.readthedocs.io/en/latest/contrib/pydantic.html) and we'll use them here.
```py hl_lines="22 23 24 25 26"
--8<-- "docs/src/db_tortoise_model.py"
```
The `PydanticModel` mixin adds methods used internally by Tortoise ORM to the Pydantic model so that it can easily transform it back to an ORM model. It expects then that you provide the property `orig_model` which should point to the **User ORM model we defined just above**.
## Create the database adapter
The database adapter of **FastAPI Users** makes the link between your database configuration and the users logic. It should be generated by a FastAPI dependency.
```py hl_lines="8 9"
--8<-- "docs/src/db_tortoise_adapter.py"
```
Notice that we pass a reference to your [`UserDB` model](../models.md).
## Register Tortoise
For using Tortoise ORM we must register our models and database.
Tortoise ORM supports integration with FastAPI out-of-the-box. It will automatically bind startup and shutdown events.
```py
from tortoise.contrib.fastapi import register_tortoise
register_tortoise(
app,
db_url=DATABASE_URL,
modules={"models": ["models"]},
generate_schemas=True,
)
```
!!! warning
In production, it's strongly recommended to setup a migration system to update your SQL schemas. See [https://tortoise-orm.readthedocs.io/en/latest/migration.html](https://tortoise-orm.readthedocs.io/en/latest/migration.html).

View File

@@ -34,10 +34,10 @@ Here is a full working example with JWT authentication to help get you started.
--8<-- "examples/sqlalchemy/app/db.py"
```
=== "app/models.py"
=== "app/schemas.py"
```py
--8<-- "examples/sqlalchemy/app/models.py"
--8<-- "examples/sqlalchemy/app/schemas.py"
```
=== "app/users.py"
@@ -46,84 +46,44 @@ Here is a full working example with JWT authentication to help get you started.
--8<-- "examples/sqlalchemy/app/users.py"
```
## MongoDB
## Beanie
[Open :material-open-in-new:](https://github.com/fastapi-users/fastapi-users/tree/master/examples/mongodb)
[Open :material-open-in-new:](https://github.com/fastapi-users/fastapi-users/tree/master/examples/beanie)
=== "requirements.txt"
```
--8<-- "examples/mongodb/requirements.txt"
--8<-- "examples/beanie/requirements.txt"
```
=== "main.py"
```py
--8<-- "examples/mongodb/main.py"
--8<-- "examples/beanie/main.py"
```
=== "app/app.py"
```py
--8<-- "examples/mongodb/app/app.py"
--8<-- "examples/beanie/app/app.py"
```
=== "app/db.py"
```py
--8<-- "examples/mongodb/app/db.py"
--8<-- "examples/beanie/app/db.py"
```
=== "app/models.py"
=== "app/schemas.py"
```py
--8<-- "examples/mongodb/app/models.py"
--8<-- "examples/beanie/app/schemas.py"
```
=== "app/users.py"
```py
--8<-- "examples/mongodb/app/users.py"
```
## Tortoise ORM
[Open :material-open-in-new:](https://github.com/fastapi-users/fastapi-users/tree/master/examples/tortoise)
=== "requirements.txt"
```
--8<-- "examples/tortoise/requirements.txt"
```
=== "main.py"
```py
--8<-- "examples/tortoise/main.py"
```
=== "app/app.py"
```py
--8<-- "examples/tortoise/app/app.py"
```
=== "app/db.py"
```py
--8<-- "examples/tortoise/app/db.py"
```
=== "app/models.py"
```py
--8<-- "examples/tortoise/app/models.py"
```
=== "app/users.py"
```py
--8<-- "examples/tortoise/app/users.py"
--8<-- "examples/beanie/app/users.py"
```
## What now?

View File

@@ -1,69 +0,0 @@
# Models
**FastAPI Users** defines a minimal User model for authentication purposes. It is structured like this:
* `id` (`UUID4`) Unique identifier of the user. Defaults to a **UUID4**.
* `email` (`str`) Email of the user. Validated by [`email-validator`](https://github.com/JoshData/python-email-validator).
* `is_active` (`bool`) Whether or not the user is active. If not, login and forgot password requests will be denied. Defaults to `True`.
* `is_verified` (`bool`) Whether or not the user is verified. Optional but helpful with the [`verify` router](./routers/verify.md) logic. Defaults to `False`.
* `is_superuser` (`bool`) Whether or not the user is a superuser. Useful to implement administration logic. Defaults to `False`.
## Define your models
There are four Pydantic models variations provided as mixins:
* `BaseUser`, which provides the basic fields and validation ;
* `BaseCreateUser`, dedicated to user registration, which consists of compulsory `email` and `password` fields ;
* `BaseUpdateUser`, dedicated to user profile update, which adds an optional `password` field ;
* `BaseUserDB`, which is a representation of the user in database, adding a `hashed_password` field.
You should define each of those variations, inheriting from each mixin:
```py
from fastapi_users import models
class User(models.BaseUser):
pass
class UserCreate(models.BaseUserCreate):
pass
class UserUpdate(models.BaseUserUpdate):
pass
class UserDB(User, models.BaseUserDB):
pass
```
### Adding your own fields
You can of course add your own properties there to fit to your needs. In the example below, we add a required string property, `first_name`, and an optional date property, `birthdate`.
```py
import datetime
from fastapi_users import models
class User(models.BaseUser):
first_name: str
birthdate: Optional[datetime.date]
class UserCreate(models.BaseUserCreate):
first_name: str
birthdate: Optional[datetime.date]
class UserUpdate(models.BaseUserUpdate):
first_name: Optional[str]
birthdate: Optional[datetime.date]
class UserDB(User, models.BaseUserDB):
pass
```

View File

@@ -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"
```

View File

@@ -4,28 +4,23 @@ The schema below shows you how the library is structured and how each part fit t
```mermaid
flowchart LR
flowchart TB
FASTAPI_USERS{FastAPIUsers}
USER_MANAGER{UserManager}
USER_MODEL{User model}
DATABASE_DEPENDENCY[[get_user_db]]
USER_MANAGER_DEPENDENCY[[get_user_manager]]
CURRENT_USER[[current_user]]
subgraph MODELS[Models]
direction RL
subgraph SCHEMAS[Schemas]
USER[User]
USER_CREATE[UserCreate]
USER_UPDATE[UserUpdate]
USER_DB[UserDB]
end
subgraph DATABASE[Database adapters]
direction RL
SQLALCHEMY[SQLAlchemy]
MONGODB[MongoDB]
TORTOISE[Tortoise ORM]
ORMAR[Ormar]
BEANIE[Beanie]
end
subgraph ROUTERS[Routers]
direction RL
AUTH[[get_auth_router]]
OAUTH[[get_oauth_router]]
REGISTER[[get_register_router]]
@@ -34,24 +29,21 @@ flowchart LR
USERS[[get_users_router]]
end
subgraph AUTH_BACKENDS[Authentication]
direction RL
subgraph TRANSPORTS[Transports]
direction RL
COOKIE[CookieTransport]
BEARER[BearerTransport]
end
subgraph STRATEGIES[Strategies]
direction RL
DB[DatabaseStrategy]
JWT[JWTStrategy]
REDIS[RedisStrategy]
end
AUTH_BACKEND{AuthenticationBackend}
end
DATABASE --> DATABASE_DEPENDENCY
USER_MODEL --> DATABASE_DEPENDENCY
DATABASE_DEPENDENCY --> USER_MANAGER
MODELS --> USER_MANAGER
MODELS --> FASTAPI_USERS
USER_MANAGER --> USER_MANAGER_DEPENDENCY
USER_MANAGER_DEPENDENCY --> FASTAPI_USERS
@@ -60,30 +52,21 @@ flowchart LR
TRANSPORTS --> AUTH_BACKEND
STRATEGIES --> AUTH_BACKEND
AUTH_BACKEND --> AUTH
AUTH_BACKEND --> OAUTH
AUTH_BACKEND --> ROUTERS
AUTH_BACKEND --> FASTAPI_USERS
FASTAPI_USERS --> CURRENT_USER
SCHEMAS --> ROUTERS
```
## Models
## User model and database adapters
Pydantic models representing the data structure of a user. Base classes are provided with the required fields to make authentication work. You should sub-class each of them and add your own fields there.
➡️ [Configure the models](./models.md)
## Database adapters
FastAPI Users is compatible with various databases and ORM. To build the interface between those database tools and the library, we provide database adapters classes that you need to instantiate and configure.
FastAPI Users is compatible with various **databases and ORM**. To build the interface between those database tools and the library, we provide database adapters classes that you need to instantiate and configure.
➡️ [I'm using SQLAlchemy](databases/sqlalchemy.md)
➡️ [I'm using MongoDB](databases/mongodb.md)
➡️ [I'm using Tortoise ORM](databases/tortoise.md)
➡️ [I'm using ormar](databases/ormar.md)
➡️ [I'm using Beanie](databases/beanie.md)
## Authentication backends
@@ -101,6 +84,12 @@ This `UserManager` object should be provided through a FastAPI dependency, `get_
➡️ [Configure `UserManager`](./user-manager.md)
## Schemas
FastAPI is heavily using [Pydantic models](https://pydantic-docs.helpmanual.io/) to validate request payloads and serialize responses. **FastAPI Users** is no exception and will expect you to provide Pydantic schemas representing a user when it's read, created and updated.
➡️ [Configure schemas](./schemas.md)
## `FastAPIUsers` and routers
Finally, `FastAPIUsers` object is the main class from which you'll be able to generate routers for classic routes like registration or login, but also get the `current_user` dependency factory to inject the authenticated user in your own routes.

View File

@@ -7,16 +7,16 @@ Check the [routes usage](../../usage/routes.md) to learn how to use them.
## Setup
```py
import uuid
from fastapi import FastAPI
from fastapi_users import FastAPIUsers
fastapi_users = FastAPIUsers(
from .db import User
fastapi_users = FastAPIUsers[User, uuid.UUID](
get_user_manager,
[auth_backend],
User,
UserCreate,
UserUpdate,
UserDB,
)
app = FastAPI()

View File

@@ -4,29 +4,33 @@ We're almost there! The last step is to configure the `FastAPIUsers` object that
## Configure `FastAPIUsers`
Configure `FastAPIUsers` object with all the elements we defined before. More precisely:
Configure `FastAPIUsers` object with the elements we defined before. More precisely:
* `get_user_manager`: Dependency callable getter to inject the
user manager class instance. See [UserManager](../user-manager.md).
* `auth_backends`: List of authentication backends. See [Authentication](../authentication/index.md).
* `user_model`: Pydantic model of a user.
* `user_create_model`: Pydantic model for creating a user.
* `user_update_model`: Pydantic model for updating a user.
* `user_db_model`: Pydantic model of a DB representation of a user.
```py
import uuid
from fastapi_users import FastAPIUsers
fastapi_users = FastAPIUsers(
from .db import User
fastapi_users = FastAPIUsers[User, uuid.UUID](
get_user_manager,
[auth_backend],
User,
UserCreate,
UserUpdate,
UserDB,
)
```
!!! note "Typing: User and ID generic types are expected"
You can see that we define two generic types when instantiating:
* `User`, which is the user model we defined in the database part
* The ID, which should correspond to the type of ID you use on your model. Here, we chose UUID, but it can be anything, like an integer or a MongoDB ObjectID.
It'll help you to have **good type-checking and auto-completion**.
## Available routers
This helper class will let you generate useful routers to setup the authentication system. Each of them is **optional**, so you can pick only the one that you are interested in! Here are the routers provided:

View File

@@ -7,23 +7,22 @@ Check the [routes usage](../../usage/routes.md) to learn how to use them.
## Setup
```py
import uuid
from fastapi import FastAPI
from fastapi_users import FastAPIUsers
SECRET = "SECRET"
from .db import User
from .schemas import UserCreate, UserRead
fastapi_users = FastAPIUsers(
fastapi_users = FastAPIUsers[User, uuid.UUID](
get_user_manager,
[auth_backend],
User,
UserCreate,
UserUpdate,
UserDB,
)
app = FastAPI()
app.include_router(
fastapi_users.get_register_router(),
fastapi_users.get_register_router(UserRead, UserCreate),
prefix="/auth",
tags=["auth"],
)

View File

@@ -7,18 +7,16 @@ Check the [routes usage](../../usage/routes.md) to learn how to use them.
## Setup
```py
import uuid
from fastapi import FastAPI
from fastapi_users import FastAPIUsers
SECRET = "SECRET"
from .db import User
fastapi_users = FastAPIUsers(
fastapi_users = FastAPIUsers[User, uuid.UUID](
get_user_manager,
[auth_backend],
User,
UserCreate,
UserUpdate,
UserDB,
)
app = FastAPI()

View File

@@ -5,23 +5,22 @@ This router provides routes to manage users. Check the [routes usage](../../usag
## Setup
```py
import uuid
from fastapi import FastAPI
from fastapi_users import FastAPIUsers
SECRET = "SECRET"
from .db import User
from .schemas import UserRead, UserUpdate
fastapi_users = FastAPIUsers(
fastapi_users = FastAPIUsers[User, uuid.UUID](
get_user_manager,
[auth_backend],
User,
UserCreate,
UserUpdate,
UserDB,
)
app = FastAPI()
app.include_router(
fastapi_users.get_users_router(),
fastapi_users.get_users_router(UserRead, UserUpdate),
prefix="/users",
tags=["users"],
)
@@ -33,7 +32,7 @@ You can require the user to be **verified** (i.e. `is_verified` property set to
```py
app.include_router(
fastapi_users.get_users_router(requires_verification=True),
fastapi_users.get_users_router(UserRead, UserUpdate, requires_verification=True),
prefix="/users",
tags=["users"],
)

View File

@@ -8,23 +8,22 @@ This router provides routes to manage user email verification. Check the [routes
## Setup
```py
import uuid
from fastapi import FastAPI
from fastapi_users import FastAPIUsers
SECRET = "SECRET"
from .db import User
from .schemas import UserRead
fastapi_users = FastAPIUsers(
fastapi_users = FastAPIUsers[User, uuid.UUID](
get_user_manager,
[auth_backend],
User,
UserCreate,
UserUpdate,
UserDB,
)
app = FastAPI()
app.include_router(
fastapi_users.get_verify_router(),
fastapi_users.get_verify_router(UserRead),
prefix="/auth",
tags=["auth"],
)

View File

@@ -0,0 +1,73 @@
# Schemas
FastAPI is heavily using [Pydantic models](https://pydantic-docs.helpmanual.io/) to validate request payloads and serialize responses. **FastAPI Users** is no exception and will expect you to provide Pydantic schemas representing a user when it's read, created and updated.
It's **different from your `User` model**, which is an object that actually interacts with the database. Those schemas on the other hand are here to validate data and serialize correct it in the API.
**FastAPI Users** provides a base structure to cover its needs. It is structured like this:
* `id` (`ID`) Unique identifier of the user. It matches the type of your ID, like UUID or integer.
* `email` (`str`) Email of the user. Validated by [`email-validator`](https://github.com/JoshData/python-email-validator).
* `is_active` (`bool`) Whether or not the user is active. If not, login and forgot password requests will be denied. Defaults to `True`.
* `is_verified` (`bool`) Whether or not the user is verified. Optional but helpful with the [`verify` router](./routers/verify.md) logic. Defaults to `False`.
* `is_superuser` (`bool`) Whether or not the user is a superuser. Useful to implement administration logic. Defaults to `False`.
## Define your schemas
There are four Pydantic models variations provided as mixins:
* `BaseUser`, which provides the basic fields and validation;
* `BaseCreateUser`, dedicated to user registration, which consists of compulsory `email` and `password` fields;
* `BaseUpdateUser`, dedicated to user profile update, which adds an optional `password` field;
You should define each of those variations, inheriting from each mixin:
```py
import uuid
from fastapi_users import schemas
class UserRead(schemas.BaseUser[uuid.UUID]):
pass
class UserCreate(schemas.BaseUserCreate):
pass
class UserUpdate(schemas.BaseUserUpdate):
pass
```
!!! note "Typing: ID generic type is expected"
You can see that we define a generic type when extending the `BaseUser` class. It should correspond to the type of ID you use on your model. Here, we chose UUID, but it can be anything, like an integer or a MongoDB ObjectID.
### Adding your own fields
You can of course add your own properties there to fit to your needs. In the example below, we add a required string property, `first_name`, and an optional date property, `birthdate`.
```py
import datetime
import uuid
from fastapi_users import schemas
class UserRead(schemas.BaseUser[uuid.UUID]):
first_name: str
birthdate: Optional[datetime.date]
class UserCreate(schemas.BaseUserCreate):
first_name: str
birthdate: Optional[datetime.date]
class UserUpdate(schemas.BaseUserUpdate):
first_name: Optional[str]
birthdate: Optional[datetime.date]
```
!!! warning "Make sure to mirror this in your database model"
The `User` model you defined earlier for your specific database will be the central object that will actually store the data. Therefore, you need to define the very same fields in it so the data can be actually stored.

View File

@@ -2,23 +2,62 @@
The `UserManager` class is the core logic of FastAPI Users. We provide the `BaseUserManager` class which you should extend to set some parameters and define logic, for example when a user just registered or forgot its password.
It's designed to be easily extensible and customizable so that you can integrate less generic logic.
It's designed to be easily extensible and customizable so that you can integrate your very own logic.
## Create your `UserManager` class
You should define your own version of the `UserManager` class to set various parameters.
```py hl_lines="12-28"
```py hl_lines="12-27"
--8<-- "docs/src/user_manager.py"
```
As you can see, you have to define here various attributes and methods. You can find the complete list of those below.
!!! note "Typing: User and ID generic types are expected"
You can see that we define two generic types when extending the base class:
* `User`, which is the user model we defined in the database part
* The ID, which should correspond to the type of ID you use on your model. Here, we chose UUID, but it can be anything, like an integer or a MongoDB ObjectID.
It'll help you to have **good type-checking and auto-completion** when implementing the custom methods.
### The ID parser mixin
Since the user ID is fully generic, we need a way to **parse it reliably when it'll come from API requests**, typically as URL path attributes.
That's why we added the `UUIDIDMixin` in the example above. It implements the `parse_id` method, ensuring UUID are valid and correctly parsed.
Of course, it's important that this logic **matches the type of your ID**. To help you with this, we provide mixins for the most common cases:
* `UUIDIDMixin`, for UUID ID.
* `IntegerIDMixin`, for integer ID.
* `ObjectIDIDMixin` (provided by `fastapi_users_db_beanie`), for MongoDB ObjectID.
!!! tip "Inheritance order matters"
Notice in your example that **the mixin comes first in our `UserManager` inheritance**. Because of the Method-Resolution-Order (MRO) of Python, the left-most element takes precedence.
If you need another type of ID, you can simply overload the `parse_id` method on your `UserManager` class:
```py
from fastapi_users import BaseUserManager, InvalidID
class UserManager(BaseUserManager[User, MyCustomID]):
def parse_id(self, value: Any) -> MyCustomID:
try:
return MyCustomID(value)
except ValueError as e:
raise InvalidID() from e # (1)!
```
1. If the ID can't be parsed into the desired type, you'll need to raise an `InvalidID` exception.
## Create `get_user_manager` dependency
The `UserManager` class will be injected at runtime using a FastAPI dependency. This way, you can run it in a database session or swap it with a mock during testing.
```py hl_lines="31-32"
```py hl_lines="30-31"
--8<-- "docs/src/user_manager.py"
```
@@ -28,7 +67,6 @@ Notice that we use the `get_user_db` dependency we defined earlier to inject the
### Attributes
* `user_db_model`: Pydantic model of a DB representation of a user.
* `reset_password_token_secret`: Secret to encode reset password token. **Use a strong passphrase and keep it secure.**
* `reset_password_token_lifetime_seconds`: Lifetime of reset password token. Defaults to 3600.
* `reset_password_token_audience`: JWT audience of reset password token. Defaults to `fastapi-users:reset`.
@@ -54,15 +92,15 @@ This function should return `None` if the password is valid or raise `InvalidPas
**Example**
```py
from fastapi_users import BaseUserManager, InvalidPasswordException
from fastapi_users import BaseUserManager, InvalidPasswordException, UUIDIDMixin
class UserManager(BaseUserManager[UserCreate, UserDB]):
class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
# ...
async def validate_password(
self,
password: str,
user: Union[UserCreate, UserDB],
user: Union[UserCreate, User],
) -> None:
if len(password) < 8:
raise InvalidPasswordException(
@@ -82,18 +120,18 @@ Typically, you'll want to **send a welcome e-mail** or add it to your marketing
**Arguments**
* `user` (`UserDB`): the registered user.
* `user` (`User`): the registered user.
* `request` (`Optional[Request]`): optional FastAPI request object that triggered the operation. Defaults to None.
**Example**
```py
from fastapi_users import BaseUserManager
from fastapi_users import BaseUserManager, UUIDIDMixin
class UserManager(BaseUserManager[UserCreate, UserDB]):
class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
# ...
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.")
```
@@ -105,21 +143,21 @@ It may be useful, for example, if you wish to update your user in a data analyti
**Arguments**
* `user` (`UserDB`): the updated user.
* `user` (`User`): the updated user.
* `update_dict` (`Dict[str, Any]`): dictionary with the updated user fields.
* `request` (`Optional[Request]`): optional FastAPI request object that triggered the operation. Defaults to None.
**Example**
```py
from fastapi_users import BaseUserManager
from fastapi_users import BaseUserManager, UUIDIDMixin
class UserManager(BaseUserManager[UserCreate, UserDB]):
class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
# ...
async def on_after_update(
self,
user: UserDB,
user: User,
update_dict: Dict[str, Any],
request: Optional[Request] = None,
):
@@ -134,20 +172,20 @@ Typically, you'll want to **send an e-mail** with the link (and the token) that
**Arguments**
* `user` (`UserDB`): the user to verify.
* `user` (`User`): the user to verify.
* `token` (`str`): the verification token.
* `request` (`Optional[Request]`): optional FastAPI request object that triggered the operation. Defaults to None.
**Example**
```py
from fastapi_users import BaseUserManager
from fastapi_users import BaseUserManager, UUIDIDMixin
class UserManager(BaseUserManager[UserCreate, UserDB]):
class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
# ...
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}")
```
@@ -160,19 +198,19 @@ This may be useful if you wish to send another e-mail or store this information
**Arguments**
* `user` (`UserDB`): the verified user.
* `user` (`User`): the verified user.
* `request` (`Optional[Request]`): optional FastAPI request object that triggered the operation. Defaults to None.
**Example**
```py
from fastapi_users import BaseUserManager
from fastapi_users import BaseUserManager, UUIDIDMixin
class UserManager(BaseUserManager[UserCreate, UserDB]):
class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
# ...
async def on_after_verify(
self, user: UserDB, request: Optional[Request] = None
self, user: User, request: Optional[Request] = None
):
print(f"User {user.id} has been verified")
```
@@ -185,20 +223,20 @@ Typically, you'll want to **send an e-mail** with the link (and the token) that
**Arguments**
* `user` (`UserDB`): the user that forgot its password.
* `user` (`User`): the user that forgot its password.
* `token` (`str`): the forgot password token
* `request` (`Optional[Request]`): optional FastAPI request object that triggered the operation. Defaults to None.
**Example**
```py
from fastapi_users import BaseUserManager
from fastapi_users import BaseUserManager, UUIDIDMixin
class UserManager(BaseUserManager[UserCreate, UserDB]):
class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
# ...
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}")
```
@@ -211,17 +249,17 @@ For example, you may want to **send an e-mail** to the concerned user to warn hi
**Arguments**
* `user` (`UserDB`): the user that reset its password.
* `user` (`User`): the user that reset its password.
* `request` (`Optional[Request]`): optional FastAPI request object that triggered the operation. Defaults to None.
**Example**
```py
from fastapi_users import BaseUserManager
from fastapi_users import BaseUserManager, UUIDIDMixin
class UserManager(BaseUserManager[UserCreate, UserDB]):
class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
# ...
async def on_after_reset_password(self, user: UserDB, request: Optional[Request] = None):
async def on_after_reset_password(self, user: User, request: Optional[Request] = None):
print(f"User {user.id} has reset their password.")
```

View File

@@ -5,27 +5,15 @@ You can add **FastAPI Users** to your FastAPI project in a few easy steps. First
## With SQLAlchemy support
```sh
pip install 'fastapi-users[sqlalchemy2]'
pip install 'fastapi-users[sqlalchemy]'
```
## With MongoDB support
## With Beanie support
```sh
pip install 'fastapi-users[mongodb]'
```
## With Tortoise ORM support
```sh
pip install 'fastapi-users[tortoise-orm]'
```
## With ormar support
```sh
pip install 'fastapi-users[ormar]'
```
---
That's it! Now, let's have a look at our [User model](./configuration/models.md).
That's it! In the next section, we'll have an [overview](./configuration/overview.md) of how things work.

361
docs/migration/9x_to_10x.md Normal file
View File

@@ -0,0 +1,361 @@
# 9.x.x ➡️ 10.x.x
Version 10 marks important changes in how we manage User models and their ID.
Before, we were relying only on Pydantic models to work with users. In particular the [`current_user` dependency](../usage/current-user.md) would return you an instance of `UserDB`, a Pydantic model. This proved to be quite problematic with some ORM if you ever needed to **retrieve relationship data** or make specific requests.
Now, FastAPI Users is designed to always return you a **native object for your ORM model**, whether it's an SQLAlchemy model or a Beanie document. Pydantic models are now only used for validation and serialization inside the API.
Before, we were forcing the use of UUID as primary key ID; a consequence of the design above. This proved to be quite problematic on some databases, like MongoDB which uses a special ObjectID format by default. Some SQL folks also prefer to use traditional auto-increment integers.
Now, FastAPI Users is designed to use **generic ID type**. It means that you can use any type you want for your user's ID. By default, SQLAlchemy adapter still use UUID; but you can quite easily switch to another thing, like an integer. Beanie adapter for MongoDB will use native ObjectID by default, but it also can be overriden.
As you may have guessed, those changes imply quite a lot of **breaking changes**.
## User models and database adapter
### SQLAlchemy ORM
We've removed the old SQLAlchemy dependency support, so the dependency is now `fastapi-users[sqlalchemy]`.
=== "Before"
```txt
fastapi
fastapi-users[sqlalchemy2]
uvicorn[standard]
aiosqlite
```
=== "After"
```txt
fastapi
fastapi-users[sqlalchemy]
uvicorn[standard]
aiosqlite
```
The User model base class for SQLAlchemy slightly changed to support UUID by default.
We changed the name of the class from `UserTable` to `User`: it's not a compulsory change, but since there is no risk of confusion with Pydantic models anymore, it's probably a more idiomatic naming.
=== "Before"
```py
class UserTable(Base, SQLAlchemyBaseUserTable):
pass
```
=== "After"
```py
class User(SQLAlchemyBaseUserTableUUID, Base):
pass
```
Instantiating the `SQLAlchemyUserDatabase` adapter now only expects this `User` model. `UserDB` is removed.
=== "Before"
```py
async def get_user_db(session: AsyncSession = Depends(get_async_session)):
yield SQLAlchemyUserDatabase(UserDB, session, UserTable)
```
=== "After"
```py
async def get_user_db(session: AsyncSession = Depends(get_async_session)):
yield SQLAlchemyUserDatabase(session, User)
```
### MongoDB
MongoDB support is now only provided through [Beanie ODM](https://github.com/roman-right/beanie/). Even if you don't use it for the rest of your project, it's a very light addition that shouldn't interfere much.
=== "Before"
```txt
fastapi
fastapi-users[mongodb]
uvicorn[standard]
aiosqlite
```
=== "After"
```txt
fastapi
fastapi-users[beanie]
uvicorn[standard]
aiosqlite
```
You now need to define a proper User model using Beanie.
=== "Before"
```py
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)
```
=== "After"
```py
import motor.motor_asyncio
from beanie import PydanticObjectId
from fastapi_users.db import BeanieBaseUser, BeanieUserDatabase
DATABASE_URL = "mongodb://localhost:27017"
client = motor.motor_asyncio.AsyncIOMotorClient(
DATABASE_URL, uuidRepresentation="standard"
)
db = client["database_name"]
class User(BeanieBaseUser[PydanticObjectId]):
pass
async def get_user_db():
yield BeanieUserDatabase(User)
```
!!! danger "ID are now ObjectID by default"
By default, User ID will now be native MongoDB ObjectID. If you don't want to make the transition and keep UUID you can do so by overriding the `id` field:
```py
import uuid
from pydantic import Field
class User(BeanieBaseUser[uuid.UUID]):
id: uuid.UUID = Field(default_factory=uuid.uuid4)
```
Beanie also needs to be initialized in a startup event handler of your FastAPI app:
```py
from beanie import init_beanie
@app.on_event("startup")
async def on_startup():
await init_beanie(
database=db,
document_models=[
User,
],
)
```
### Tortoise ORM and ormar
Unfortunately, we sometimes need to make difficult choices to keep things sustainable. That's why we decided to **not support Tortoise ORM and ormar** anymore. It appeared they were not widely used.
You can still add support for those ORM yourself by implementing the necessary adapter. You can take inspiration from [the SQLAlchemy one](https://github.com/fastapi-users/fastapi-users-db-sqlalchemy).
## `UserManager`
There is some slight changes on the `UserManager` class. In particular, it now needs a `parse_id` method that can be provided through built-in mixins.
Generic typing now expects your **native User model class** and the **type of ID**.
The `user_db_model` class property is **removed**.
=== "Before"
```py
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}")
```
=== "After"
```py
class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
reset_password_token_secret = SECRET
verification_token_secret = SECRET
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: 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: User, token: str, request: Optional[Request] = None
):
print(f"Verification requested for user {user.id}. Verification token: {token}")
```
If you need to support other types of ID, you can read more about it [in the dedicated section](../configuration/user-manager.md#the-id-parser-mixin).
## Pydantic models
To better distinguish them from the ORM models, Pydantic models are now called **schemas**.
**`UserDB` has been removed** in favor of native models.
We changed the name of `User` to `UserRead`: it's not a compulsory change, but since there is a **risk of confusion** with the native model, it's highly recommended.
Besides, the `BaseUser` schema now accepts a generic type to specify the type of ID you use.
=== "Before"
```py
from fastapi_users import models
class User(models.BaseUser):
pass
class UserCreate(models.BaseUserCreate):
pass
class UserUpdate(models.BaseUserUpdate):
pass
class UserDB(User, models.BaseUserDB):
pass
```
=== "After"
```py
import uuid
from fastapi_users import schemas
class UserRead(schemas.BaseUser[uuid.UUID]):
pass
class UserCreate(schemas.BaseUserCreate):
pass
class UserUpdate(schemas.BaseUserUpdate):
pass
```
## FastAPI Users and routers
Pydantic schemas are now way less important in this new design. As such, you don't need to pass them when initializing the `FastAPIUsers` class:
=== "Before"
```py
fastapi_users = FastAPIUsers(
get_user_manager,
[auth_backend],
User,
UserCreate,
UserUpdate,
UserDB,
)
```
=== "After"
```py
fastapi_users = FastAPIUsers[User, uuid.UUID](
get_user_manager,
[auth_backend],
)
```
As a consequence, those schemas need to be passed when initializing the router that needs them: `get_register_router`, `get_verify_router` and `get_users_router`.
=== "Before"
```py
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"])
```
=== "After"
```py
app.include_router(
fastapi_users.get_auth_router(auth_backend), prefix="/auth/jwt", 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(UserRead),
prefix="/auth",
tags=["auth"],
)
app.include_router(
fastapi_users.get_users_router(UserRead, UserUpdate),
prefix="/users",
tags=["users"],
)
```
## Lost?
If you're unsure or a bit lost, make sure to check the [full working examples](../configuration/full-example.md).

View File

@@ -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

View File

@@ -1,15 +1,17 @@
import motor.motor_asyncio
from fastapi_users.db import MongoDBUserDatabase
from .models import UserDB
from beanie import PydanticObjectId
from fastapi_users.db import BeanieBaseUser, BeanieUserDatabase
DATABASE_URL = "mongodb://localhost:27017"
client = motor.motor_asyncio.AsyncIOMotorClient(
DATABASE_URL, uuidRepresentation="standard"
)
db = client["database_name"]
collection = db["users"]
class User(BeanieBaseUser[PydanticObjectId]):
pass
async def get_user_db():
yield MongoDBUserDatabase(UserDB, collection)
yield BeanieUserDatabase(User)

View File

@@ -0,0 +1,29 @@
import motor.motor_asyncio
from beanie import PydanticObjectId
from fastapi_users.db import BeanieBaseUser, BeanieUserDatabase
from fastapi_users_db_beanie.access_token import (
BeanieAccessTokenDatabase,
BeanieBaseAccessToken,
)
DATABASE_URL = "mongodb://localhost:27017"
client = motor.motor_asyncio.AsyncIOMotorClient(
DATABASE_URL, uuidRepresentation="standard"
)
db = client["database_name"]
class User(BeanieBaseUser):
pass
class AccessToken(BeanieBaseAccessToken[PydanticObjectId]): # (1)!
pass
async def get_user_db():
yield BeanieUserDatabase(User)
async def get_access_token_db(): # (2)!
yield BeanieAccessTokenDatabase(AccessToken)

View File

@@ -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)

View File

@@ -1,21 +0,0 @@
import motor.motor_asyncio
from fastapi_users.db import MongoDBUserDatabase
from fastapi_users_db_mongodb.access_token import MongoDBAccessTokenDatabase
from .models import AccessToken, UserDB
DATABASE_URL = "mongodb://localhost:27017"
client = motor.motor_asyncio.AsyncIOMotorClient(
DATABASE_URL, uuidRepresentation="standard"
)
db = client["database_name"]
users_collection = db["users"]
access_tokens_collection = db["access_tokens"]
async def get_user_db():
yield MongoDBUserDatabase(UserDB, users_collection)
async def get_access_token_db():
yield MongoDBAccessTokenDatabase(AccessToken, access_tokens_collection)

View File

@@ -1,24 +0,0 @@
import databases
import sqlalchemy
from fastapi_users.db import OrmarBaseUserModel, OrmarUserDatabase
from .models import UserDB
DATABASE_URL = "sqlite:///test.db"
metadata = sqlalchemy.MetaData()
database = databases.Database(DATABASE_URL)
class UserModel(OrmarBaseUserModel):
class Meta:
tablename = "users"
metadata = metadata
database = database
engine = sqlalchemy.create_engine(DATABASE_URL)
metadata.create_all(engine)
async def get_user_db():
yield OrmarUserDatabase(UserDB, UserModel)

View File

@@ -1,18 +1,16 @@
from typing import AsyncGenerator
from fastapi import Depends
from fastapi_users.db import SQLAlchemyBaseUserTable, SQLAlchemyUserDatabase
from fastapi_users.db import SQLAlchemyBaseUserTableUUID, SQLAlchemyUserDatabase
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.ext.declarative import DeclarativeMeta, declarative_base
from sqlalchemy.orm import sessionmaker
from .models import UserDB
DATABASE_URL = "sqlite+aiosqlite:///./test.db"
Base: DeclarativeMeta = declarative_base()
class UserTable(Base, SQLAlchemyBaseUserTable):
class User(SQLAlchemyBaseUserTableUUID, Base):
pass
@@ -31,4 +29,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)
yield SQLAlchemyUserDatabase(session, User)

View File

@@ -1,26 +1,24 @@
from typing import AsyncGenerator
from fastapi import Depends
from fastapi_users.db import SQLAlchemyBaseUserTable, SQLAlchemyUserDatabase
from fastapi_users.db import SQLAlchemyBaseUserTableUUID, SQLAlchemyUserDatabase
from fastapi_users_db_sqlalchemy.access_token import (
SQLAlchemyAccessTokenDatabase,
SQLAlchemyBaseAccessTokenTable,
SQLAlchemyBaseAccessTokenTableUUID,
)
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.ext.declarative import DeclarativeMeta, declarative_base
from sqlalchemy.orm import sessionmaker
from .models import AccessToken, UserDB
DATABASE_URL = "sqlite+aiosqlite:///./test.db"
Base: DeclarativeMeta = declarative_base()
class UserTable(Base, SQLAlchemyBaseUserTable):
class User(SQLAlchemyBaseUserTableUUID, Base):
pass
class AccessTokenTable(SQLAlchemyBaseAccessTokenTable, Base):
class AccessToken(SQLAlchemyBaseAccessTokenTableUUID, Base): # (1)!
pass
@@ -39,8 +37,10 @@ async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
async def get_user_db(session: AsyncSession = Depends(get_async_session)):
yield SQLAlchemyUserDatabase(UserDB, session, UserTable)
yield SQLAlchemyUserDatabase(session, User)
async def get_access_token_db(session: AsyncSession = Depends(get_async_session)):
yield SQLAlchemyAccessTokenDatabase(AccessToken, session, AccessTokenTable)
async def get_access_token_db(
session: AsyncSession = Depends(get_async_session),
): # (2)!
yield SQLAlchemyAccessTokenDatabase(session, AccessToken)

View File

@@ -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 .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)

View File

@@ -1,14 +0,0 @@
from fastapi_users.db import TortoiseUserDatabase
from fastapi_users_db_tortoise.access_token import TortoiseAccessTokenDatabase
from .models import AccessToken, AccessTokenModel, UserDB, UserModel
DATABASE_URL = "sqlite://./test.db"
async def get_user_db():
yield TortoiseUserDatabase(UserDB, UserModel)
async def get_access_token_db():
yield TortoiseAccessTokenDatabase(AccessToken, AccessTokenModel)

View File

@@ -1,38 +0,0 @@
from fastapi_users import models
from fastapi_users.authentication.strategy.db.models import BaseAccessToken
from fastapi_users.db import TortoiseBaseUserModel
from fastapi_users_db_tortoise.access_token import TortoiseBaseAccessTokenModel
from tortoise import fields
from tortoise.contrib.pydantic import PydanticModel
class User(models.BaseUser):
pass
class UserCreate(models.BaseUserCreate):
pass
class UserUpdate(models.BaseUserUpdate):
pass
class UserModel(TortoiseBaseUserModel):
pass
class UserDB(User, models.BaseUserDB, PydanticModel):
class Config:
orm_mode = True
orig_model = UserModel
class AccessTokenModel(TortoiseBaseAccessTokenModel):
user = fields.ForeignKeyField("models.UserModel", related_name="access_tokens")
class AccessToken(BaseAccessToken, PydanticModel):
class Config:
orm_mode = True
orig_model = AccessTokenModel

View File

@@ -1,9 +0,0 @@
from fastapi_users.db import TortoiseUserDatabase
from .models import UserDB, UserModel
DATABASE_URL = "sqlite://./test.db"
async def get_user_db():
yield TortoiseUserDatabase(UserDB, UserModel)

View File

@@ -1,25 +0,0 @@
from fastapi_users import models
from fastapi_users.db import TortoiseBaseUserModel
from tortoise.contrib.pydantic import PydanticModel
class User(models.BaseUser):
pass
class UserCreate(models.BaseUserCreate):
pass
class UserUpdate(models.BaseUserUpdate):
pass
class UserModel(TortoiseBaseUserModel):
pass
class UserDB(User, models.BaseUserDB, PydanticModel):
class Config:
orm_mode = True
orig_model = UserModel

View File

@@ -1,9 +0,0 @@
from fastapi_users.db import TortoiseUserDatabase
from .models import OAuthAccount, UserDB, UserModel
DATABASE_URL = "sqlite://./test.db"
async def get_user_db():
yield TortoiseUserDatabase(UserDB, UserModel, OAuthAccount)

View File

@@ -1,30 +0,0 @@
from fastapi_users import models
from fastapi_users.db import TortoiseBaseOAuthAccountModel, TortoiseBaseUserModel
from tortoise import fields
from tortoise.contrib.pydantic import PydanticModel
class User(models.BaseUser, models.BaseOAuthAccountMixin):
pass
class UserCreate(models.BaseUserCreate):
pass
class UserUpdate(models.BaseUserUpdate):
pass
class UserModel(TortoiseBaseUserModel):
pass
class UserDB(User, models.BaseUserDB, PydanticModel):
class Config:
orm_mode = True
orig_model = UserModel
class OAuthAccount(TortoiseBaseOAuthAccountModel):
user = fields.ForeignKeyField("models.UserModel", related_name="oauth_accounts")

View File

@@ -1,29 +1,28 @@
import uuid
from typing import Optional
from fastapi import Depends, Request
from fastapi_users import BaseUserManager
from fastapi_users import BaseUserManager, UUIDIDMixin
from .db import get_user_db
from .models import UserCreate, UserDB
from .db import User, get_user_db
SECRET = "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}")

View File

@@ -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,
],
)

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -0,0 +1,4 @@
import uvicorn
if __name__ == "__main__":
uvicorn.run("app.app:app", host="0.0.0.0", log_level="info")

View File

@@ -1,3 +1,3 @@
fastapi
fastapi-users[mongodb]
fastapi-users[beanie]
uvicorn[standard]

View File

@@ -0,0 +1,47 @@
from beanie import init_beanie
from fastapi import Depends, FastAPI
from app.db import User, db
from app.schemas import UserCreate, UserRead, UserUpdate
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(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(UserRead),
prefix="/auth",
tags=["auth"],
)
app.include_router(
fastapi_users.get_users_router(UserRead, UserUpdate),
prefix="/users",
tags=["users"],
)
@app.get("/authenticated-route")
async def authenticated_route(user: User = Depends(current_active_user)):
return {"message": f"Hello {user.email}!"}
@app.on_event("startup")
async def on_startup():
await init_beanie(
database=db,
document_models=[
User,
],
)

17
examples/beanie/app/db.py Normal file
View File

@@ -0,0 +1,17 @@
import motor.motor_asyncio
from beanie import PydanticObjectId
from fastapi_users.db import BeanieBaseUser, BeanieUserDatabase
DATABASE_URL = "mongodb://localhost:27017"
client = motor.motor_asyncio.AsyncIOMotorClient(
DATABASE_URL, uuidRepresentation="standard"
)
db = client["database_name"]
class User(BeanieBaseUser[PydanticObjectId]):
pass
async def get_user_db():
yield BeanieUserDatabase(User)

View File

@@ -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

View File

@@ -1,5 +1,6 @@
from typing import Optional
from beanie import PydanticObjectId
from fastapi import Depends, Request
from fastapi_users import BaseUserManager, FastAPIUsers
from fastapi_users.authentication import (
@@ -7,34 +8,32 @@ from fastapi_users.authentication import (
BearerTransport,
JWTStrategy,
)
from fastapi_users.db import MongoDBUserDatabase
from fastapi_users.db import BeanieUserDatabase, ObjectIDIDMixin
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"
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)
@@ -50,13 +49,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)

4
examples/beanie/main.py Normal file
View File

@@ -0,0 +1,4 @@
import uvicorn
if __name__ == "__main__":
uvicorn.run("app.app:app", host="0.0.0.0", log_level="info")

View File

@@ -0,0 +1,3 @@
fastapi
fastapi-users[beanie]
uvicorn[standard]

View File

@@ -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}!"}

View File

@@ -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)

View File

@@ -1,17 +0,0 @@
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

View File

@@ -1,4 +0,0 @@
import uvicorn
if __name__ == "__main__":
uvicorn.run("app.app:app", host="0.0.0.0", port=5000, log_level="info")

View File

@@ -1,3 +0,0 @@
fastapi
fastapi-users[mongodb,oauth]
uvicorn[standard]

View File

@@ -1,27 +0,0 @@
from fastapi import Depends, FastAPI
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}!"}

View File

@@ -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)

View File

@@ -1,17 +0,0 @@
from fastapi_users import models
class User(models.BaseUser):
pass
class UserCreate(models.BaseUserCreate):
pass
class UserUpdate(models.BaseUserUpdate):
pass
class UserDB(User, models.BaseUserDB):
pass

View File

@@ -1,4 +0,0 @@
import uvicorn
if __name__ == "__main__":
uvicorn.run("app.app:app", host="0.0.0.0", port=5000, log_level="info")

View File

@@ -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}!"}

View File

@@ -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)

View File

@@ -1,17 +0,0 @@
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

View File

@@ -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

View File

@@ -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)

View File

@@ -1,4 +1,4 @@
fastapi
fastapi-users[sqlalchemy2,oauth]
fastapi-users[sqlalchemy]
uvicorn[standard]
aiosqlite

View File

@@ -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, fastapi_users
app = FastAPI()
@@ -9,22 +9,30 @@ 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.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}!"}

View File

@@ -1,18 +1,16 @@
from typing import AsyncGenerator
from fastapi import Depends
from fastapi_users.db import SQLAlchemyBaseUserTable, SQLAlchemyUserDatabase
from fastapi_users.db import SQLAlchemyBaseUserTableUUID, SQLAlchemyUserDatabase
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.ext.declarative import DeclarativeMeta, declarative_base
from sqlalchemy.orm import sessionmaker
from app.models import UserDB
DATABASE_URL = "sqlite+aiosqlite:///./test.db"
Base: DeclarativeMeta = declarative_base()
class UserTable(Base, SQLAlchemyBaseUserTable):
class User(SQLAlchemyBaseUserTableUUID, Base):
pass
@@ -31,4 +29,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)
yield SQLAlchemyUserDatabase(session, User)

View File

@@ -1,17 +0,0 @@
from fastapi_users import models
class User(models.BaseUser):
pass
class UserCreate(models.BaseUserCreate):
pass
class UserUpdate(models.BaseUserUpdate):
pass
class UserDB(User, models.BaseUserDB):
pass

View File

@@ -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

View File

@@ -1,7 +1,8 @@
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,
@@ -9,27 +10,25 @@ from fastapi_users.authentication import (
)
from fastapi_users.db import SQLAlchemyUserDatabase
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"
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}")
@@ -50,13 +49,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)

View File

@@ -1,4 +1,4 @@
fastapi
fastapi-users[sqlalchemy2]
fastapi-users[sqlalchemy]
uvicorn[standard]
aiosqlite

View File

View File

@@ -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)

View File

@@ -1,30 +0,0 @@
from fastapi_users import models
from fastapi_users.db import TortoiseBaseOAuthAccountModel, TortoiseBaseUserModel
from tortoise import fields
from tortoise.contrib.pydantic import PydanticModel
class User(models.BaseUser, models.BaseOAuthAccountMixin):
pass
class UserCreate(models.BaseUserCreate):
pass
class UserUpdate(models.BaseUserUpdate):
pass
class UserModel(TortoiseBaseUserModel):
pass
class UserDB(User, models.BaseUserDB, PydanticModel):
class Config:
orm_mode = True
orig_model = UserModel
class OAuthAccount(TortoiseBaseOAuthAccountModel):
user = fields.ForeignKeyField("models.UserModel", related_name="oauth_accounts")

View File

@@ -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)

View File

@@ -1,4 +0,0 @@
import uvicorn
if __name__ == "__main__":
uvicorn.run("app.app:app", host="0.0.0.0", port=5000, log_level="info")

View File

@@ -1,3 +0,0 @@
fastapi
fastapi-users[tortoise-orm,oauth]
uvicorn[standard]

View File

View File

@@ -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,
)

View File

@@ -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)

View File

@@ -1,25 +0,0 @@
from fastapi_users import models
from fastapi_users.db import TortoiseBaseUserModel
from tortoise.contrib.pydantic import PydanticModel
class User(models.BaseUser):
pass
class UserCreate(models.BaseUserCreate):
pass
class UserUpdate(models.BaseUserUpdate):
pass
class UserModel(TortoiseBaseUserModel):
pass
class UserDB(User, models.BaseUserDB, PydanticModel):
class Config:
orm_mode = True
orig_model = UserModel

View File

@@ -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)

View File

@@ -1,4 +0,0 @@
import uvicorn
if __name__ == "__main__":
uvicorn.run("app.app:app", host="0.0.0.0", port=5000, log_level="info")

View File

@@ -1,3 +0,0 @@
fastapi
fastapi-users[tortoise-orm]
uvicorn[standard]

View File

@@ -2,16 +2,22 @@
__version__ = "9.3.2"
from fastapi_users import models # noqa: F401
from fastapi_users import models, schemas # noqa: F401
from fastapi_users.fastapi_users import FastAPIUsers # noqa: F401
from fastapi_users.manager import ( # noqa: F401
BaseUserManager,
IntegerIDMixin,
InvalidID,
InvalidPasswordException,
UUIDIDMixin,
)
__all__ = [
"models",
"schemas",
"FastAPIUsers",
"BaseUserManager",
"InvalidPasswordException",
"InvalidID",
"UUIDIDMixin",
"IntegerIDMixin",
]

View File

@@ -51,7 +51,7 @@ class Authenticator:
def __init__(
self,
backends: Sequence[AuthenticationBackend],
get_user_manager: UserManagerDependency[models.UC, models.UD],
get_user_manager: UserManagerDependency[models.UP, models.ID],
):
self.backends = backends
self.get_user_manager = get_user_manager
@@ -148,14 +148,14 @@ class Authenticator:
async def _authenticate(
self,
*args,
user_manager: BaseUserManager[models.UC, models.UD],
user_manager: BaseUserManager[models.UP, models.ID],
optional: bool = False,
active: bool = False,
verified: bool = False,
superuser: bool = False,
**kwargs,
) -> Tuple[Optional[models.UD], Optional[str]]:
user: Optional[models.UD] = None
) -> Tuple[Optional[models.UP], Optional[str]]:
user: Optional[models.UP] = None
token: Optional[str] = None
enabled_backends: Sequence[AuthenticationBackend] = kwargs.get(
"enabled_backends", self.backends
@@ -163,7 +163,7 @@ class Authenticator:
for backend in self.backends:
if backend in enabled_backends:
token = kwargs[name_to_variable_name(backend.name)]
strategy: Strategy[models.UC, models.UD] = kwargs[
strategy: Strategy[models.UP, models.ID] = kwargs[
name_to_strategy_variable_name(backend.name)
]
if token is not None:

View File

@@ -14,7 +14,7 @@ from fastapi_users.authentication.transport import (
from fastapi_users.types import DependencyCallable
class AuthenticationBackend(Generic[models.UC, models.UD]):
class AuthenticationBackend(Generic[models.UP]):
"""
Combination of an authentication transport and strategy.
@@ -33,7 +33,7 @@ class AuthenticationBackend(Generic[models.UC, models.UD]):
self,
name: str,
transport: Transport,
get_strategy: DependencyCallable[Strategy[models.UC, models.UD]],
get_strategy: DependencyCallable[Strategy[models.UP, models.ID]],
):
self.name = name
self.transport = transport
@@ -41,8 +41,8 @@ class AuthenticationBackend(Generic[models.UC, models.UD]):
async def login(
self,
strategy: Strategy[models.UC, models.UD],
user: models.UD,
strategy: Strategy[models.UP, models.ID],
user: models.UP,
response: Response,
) -> Any:
token = await strategy.write_token(user)
@@ -50,8 +50,8 @@ class AuthenticationBackend(Generic[models.UC, models.UD]):
async def logout(
self,
strategy: Strategy[models.UC, models.UD],
user: models.UD,
strategy: Strategy[models.UP, models.ID],
user: models.UP,
token: str,
response: Response,
) -> Any:

View File

@@ -3,9 +3,9 @@ from fastapi_users.authentication.strategy.base import (
StrategyDestroyNotSupportedError,
)
from fastapi_users.authentication.strategy.db import (
A,
AP,
AccessTokenDatabase,
BaseAccessToken,
AccessTokenProtocol,
DatabaseStrategy,
)
from fastapi_users.authentication.strategy.jwt import JWTStrategy
@@ -16,9 +16,9 @@ except ImportError: # pragma: no cover
pass
__all__ = [
"A",
"AP",
"AccessTokenDatabase",
"BaseAccessToken",
"AccessTokenProtocol",
"DatabaseStrategy",
"JWTStrategy",
"Strategy",

View File

@@ -14,14 +14,14 @@ class StrategyDestroyNotSupportedError(Exception):
pass
class Strategy(Protocol, Generic[models.UC, models.UD]):
class Strategy(Protocol, Generic[models.UP, models.ID]):
async def read_token(
self, token: Optional[str], user_manager: BaseUserManager[models.UC, models.UD]
) -> Optional[models.UD]:
self, token: Optional[str], user_manager: BaseUserManager[models.UP, models.ID]
) -> Optional[models.UP]:
... # pragma: no cover
async def write_token(self, user: models.UD) -> str:
async def write_token(self, user: models.UP) -> str:
... # pragma: no cover
async def destroy_token(self, token: str, user: models.UD) -> None:
async def destroy_token(self, token: str, user: models.UP) -> None:
... # pragma: no cover

View File

@@ -1,5 +1,5 @@
from fastapi_users.authentication.strategy.db.adapter import AccessTokenDatabase
from fastapi_users.authentication.strategy.db.models import A, BaseAccessToken
from fastapi_users.authentication.strategy.db.models import AP, AccessTokenProtocol
from fastapi_users.authentication.strategy.db.strategy import DatabaseStrategy
__all__ = ["A", "AccessTokenDatabase", "BaseAccessToken", "DatabaseStrategy"]
__all__ = ["AP", "AccessTokenDatabase", "AccessTokenProtocol", "DatabaseStrategy"]

View File

@@ -1,38 +1,32 @@
import sys
from datetime import datetime
from typing import Generic, Optional, Type
from typing import Any, Dict, Generic, Optional
if sys.version_info < (3, 8):
from typing_extensions import Protocol # pragma: no cover
else:
from typing import Protocol # pragma: no cover
from fastapi_users.authentication.strategy.db.models import A
from fastapi_users.authentication.strategy.db.models import AP
class AccessTokenDatabase(Protocol, Generic[A]):
"""
Protocol for retrieving, creating and updating access tokens from a database.
:param access_token_model: Pydantic model of an access token.
"""
access_token_model: Type[A]
class AccessTokenDatabase(Protocol, Generic[AP]):
"""Protocol for retrieving, creating and updating access tokens from a database."""
async def get_by_token(
self, token: str, max_age: Optional[datetime] = None
) -> Optional[A]:
) -> Optional[AP]:
"""Get a single access token by token."""
... # pragma: no cover
async def create(self, access_token: A) -> A:
async def create(self, create_dict: Dict[str, Any]) -> AP:
"""Create an access token."""
... # pragma: no cover
async def update(self, access_token: A) -> A:
async def update(self, access_token: AP, update_dict: Dict[str, Any]) -> AP:
"""Update an access token."""
... # pragma: no cover
async def delete(self, access_token: A) -> None:
async def delete(self, access_token: AP) -> None:
"""Delete an access token."""
... # pragma: no cover

View File

@@ -1,22 +1,24 @@
from datetime import datetime, timezone
import sys
from datetime import datetime
from typing import TypeVar
from pydantic import UUID4, BaseModel, Field
if sys.version_info < (3, 8):
from typing_extensions import Protocol # pragma: no cover
else:
from typing import Protocol # pragma: no cover
from fastapi_users import models
def now_utc():
return datetime.now(timezone.utc)
class BaseAccessToken(BaseModel):
"""Base access token model."""
class AccessTokenProtocol(Protocol[models.ID]):
"""Access token protocol that ORM model should follow."""
token: str
user_id: UUID4
created_at: datetime = Field(default_factory=now_utc)
user_id: models.ID
created_at: datetime
class Config:
orm_mode = True
def __init__(self, *args, **kwargs) -> None:
... # pragma: no cover
A = TypeVar("A", bound=BaseAccessToken)
AP = TypeVar("AP", bound=AccessTokenProtocol)

View File

@@ -1,24 +1,26 @@
import secrets
from datetime import datetime, timedelta, timezone
from typing import Generic, Optional
from typing import Any, Dict, Generic, Optional
from fastapi_users import models
from fastapi_users.authentication.strategy.base import Strategy
from fastapi_users.authentication.strategy.db.adapter import AccessTokenDatabase
from fastapi_users.authentication.strategy.db.models import A
from fastapi_users.manager import BaseUserManager, UserNotExists
from fastapi_users.authentication.strategy.db.models import AP
from fastapi_users.manager import BaseUserManager, InvalidID, UserNotExists
class DatabaseStrategy(Strategy, Generic[models.UC, models.UD, A]):
class DatabaseStrategy(
Strategy[models.UP, models.ID], Generic[models.UP, models.ID, AP]
):
def __init__(
self, database: AccessTokenDatabase[A], lifetime_seconds: Optional[int] = None
self, database: AccessTokenDatabase[AP], lifetime_seconds: Optional[int] = None
):
self.database = database
self.lifetime_seconds = lifetime_seconds
async def read_token(
self, token: Optional[str], user_manager: BaseUserManager[models.UC, models.UD]
) -> Optional[models.UD]:
self, token: Optional[str], user_manager: BaseUserManager[models.UP, models.ID]
) -> Optional[models.UP]:
if token is None:
return None
@@ -33,21 +35,21 @@ class DatabaseStrategy(Strategy, Generic[models.UC, models.UD, A]):
return None
try:
user_id = access_token.user_id
return await user_manager.get(user_id)
except UserNotExists:
parsed_id = user_manager.parse_id(access_token.user_id)
return await user_manager.get(parsed_id)
except (UserNotExists, InvalidID):
return None
async def write_token(self, user: models.UD) -> str:
access_token = self._create_access_token(user)
await self.database.create(access_token)
async def write_token(self, user: models.UP) -> str:
access_token_dict = self._create_access_token_dict(user)
access_token = await self.database.create(access_token_dict)
return access_token.token
async def destroy_token(self, token: str, user: models.UD) -> None:
async def destroy_token(self, token: str, user: models.UP) -> None:
access_token = await self.database.get_by_token(token)
if access_token is not None:
await self.database.delete(access_token)
def _create_access_token(self, user: models.UD) -> A:
def _create_access_token_dict(self, user: models.UP) -> Dict[str, Any]:
token = secrets.token_urlsafe()
return self.database.access_token_model(token=token, user_id=user.id)
return {"token": token, "user_id": user.id}

View File

@@ -1,7 +1,6 @@
from typing import Generic, List, Optional
import jwt
from pydantic import UUID4
from fastapi_users import models
from fastapi_users.authentication.strategy.base import (
@@ -9,10 +8,10 @@ from fastapi_users.authentication.strategy.base import (
StrategyDestroyNotSupportedError,
)
from fastapi_users.jwt import SecretType, decode_jwt, generate_jwt
from fastapi_users.manager import BaseUserManager, UserNotExists
from fastapi_users.manager import BaseUserManager, InvalidID, UserNotExists
class JWTStrategy(Strategy, Generic[models.UC, models.UD]):
class JWTStrategy(Strategy[models.UP, models.ID], Generic[models.UP, models.ID]):
def __init__(
self,
secret: SecretType,
@@ -36,8 +35,8 @@ class JWTStrategy(Strategy, Generic[models.UC, models.UD]):
return self.public_key or self.secret
async def read_token(
self, token: Optional[str], user_manager: BaseUserManager[models.UC, models.UD]
) -> Optional[models.UD]:
self, token: Optional[str], user_manager: BaseUserManager[models.UP, models.ID]
) -> Optional[models.UP]:
if token is None:
return None
@@ -52,20 +51,18 @@ class JWTStrategy(Strategy, Generic[models.UC, models.UD]):
return None
try:
user_uiid = UUID4(user_id)
return await user_manager.get(user_uiid)
except ValueError:
return None
except UserNotExists:
parsed_id = user_manager.parse_id(user_id)
return await user_manager.get(parsed_id)
except (UserNotExists, InvalidID):
return None
async def write_token(self, user: models.UD) -> str:
async def write_token(self, user: models.UP) -> str:
data = {"user_id": str(user.id), "aud": self.token_audience}
return generate_jwt(
data, self.encode_key, self.lifetime_seconds, algorithm=self.algorithm
)
async def destroy_token(self, token: str, user: models.UD) -> None:
async def destroy_token(self, token: str, user: models.UP) -> None:
raise StrategyDestroyNotSupportedError(
"A JWT can't be invalidated: it's valid until it expires."
)

View File

@@ -2,21 +2,20 @@ import secrets
from typing import Generic, Optional
import aioredis
from pydantic import UUID4
from fastapi_users import models
from fastapi_users.authentication.strategy.base import Strategy
from fastapi_users.manager import BaseUserManager, UserNotExists
from fastapi_users.manager import BaseUserManager, InvalidID, UserNotExists
class RedisStrategy(Strategy, Generic[models.UC, models.UD]):
class RedisStrategy(Strategy[models.UP, models.ID], Generic[models.UP, models.ID]):
def __init__(self, redis: aioredis.Redis, lifetime_seconds: Optional[int] = None):
self.redis = redis
self.lifetime_seconds = lifetime_seconds
async def read_token(
self, token: Optional[str], user_manager: BaseUserManager[models.UC, models.UD]
) -> Optional[models.UD]:
self, token: Optional[str], user_manager: BaseUserManager[models.UP, models.ID]
) -> Optional[models.UP]:
if token is None:
return None
@@ -25,17 +24,15 @@ class RedisStrategy(Strategy, Generic[models.UC, models.UD]):
return None
try:
user_uiid = UUID4(user_id)
return await user_manager.get(user_uiid)
except ValueError:
return None
except UserNotExists:
parsed_id = user_manager.parse_id(user_id)
return await user_manager.get(parsed_id)
except (UserNotExists, InvalidID):
return None
async def write_token(self, user: models.UD) -> str:
async def write_token(self, user: models.UP) -> str:
token = secrets.token_urlsafe()
await self.redis.set(token, str(user.id), ex=self.lifetime_seconds)
return token
async def destroy_token(self, token: str, user: models.UD) -> None:
async def destroy_token(self, token: str, user: models.UP) -> None:
await self.redis.delete(token)

View File

@@ -1,52 +1,36 @@
from fastapi_users.db.base import BaseUserDatabase, UserDatabaseDependency
__all__ = [
"BaseUserDatabase",
"UserDatabaseDependency",
]
__all__ = ["BaseUserDatabase", "UserDatabaseDependency"]
try: # pragma: no cover
from fastapi_users_db_mongodb import MongoDBUserDatabase # noqa: F401
__all__.append("MongoDBUserDatabase")
except ImportError: # pragma: no cover
pass
try: # pragma: no cover
from fastapi_users_db_sqlalchemy import ( # noqa: F401
SQLAlchemyBaseOAuthAccountTable,
SQLAlchemyBaseOAuthAccountTableUUID,
SQLAlchemyBaseUserTable,
SQLAlchemyBaseUserTableUUID,
SQLAlchemyUserDatabase,
)
__all__.append("SQLAlchemyBaseOAuthAccountTable")
__all__.append("SQLAlchemyBaseUserTable")
__all__.append("SQLAlchemyBaseUserTableUUID")
__all__.append("SQLAlchemyBaseOAuthAccountTable")
__all__.append("SQLAlchemyBaseOAuthAccountTableUUID")
__all__.append("SQLAlchemyUserDatabase")
except ImportError: # pragma: no cover
pass
try: # pragma: no cover
from fastapi_users_db_tortoise import ( # noqa: F401
TortoiseBaseOAuthAccountModel,
TortoiseBaseUserModel,
TortoiseUserDatabase,
from fastapi_users_db_beanie import ( # noqa: F401
BaseOAuthAccount,
BeanieBaseUser,
BeanieUserDatabase,
ObjectIDIDMixin,
)
__all__.append("TortoiseBaseOAuthAccountModel")
__all__.append("TortoiseBaseUserModel")
__all__.append("TortoiseUserDatabase")
except ImportError: # pragma: no cover
pass
try: # pragma: no cover
from fastapi_users_db_ormar import ( # noqa: F401
OrmarBaseOAuthAccountModel,
OrmarBaseUserModel,
OrmarUserDatabase,
)
__all__.append("OrmarBaseOAuthAccountModel")
__all__.append("OrmarBaseUserModel")
__all__.append("OrmarUserDatabase")
__all__.append("BeanieBaseUser")
__all__.append("BaseOAuthAccount")
__all__.append("BeanieUserDatabase")
__all__.append("ObjectIDIDMixin")
except ImportError: # pragma: no cover
pass

View File

@@ -1,46 +1,50 @@
from typing import Generic, Optional, Type
from typing import Any, Dict, Generic, Optional
from pydantic import UUID4
from fastapi_users.models import UD
from fastapi_users.models import ID, OAP, UOAP, UP
from fastapi_users.types import DependencyCallable
class BaseUserDatabase(Generic[UD]):
"""
Base adapter for retrieving, creating and updating users from a database.
class BaseUserDatabase(Generic[UP, ID]):
"""Base adapter for retrieving, creating and updating users from a database."""
:param user_db_model: Pydantic model of a DB representation of a user.
"""
user_db_model: Type[UD]
def __init__(self, user_db_model: Type[UD]):
self.user_db_model = user_db_model
async def get(self, id: UUID4) -> Optional[UD]:
async def get(self, id: ID) -> Optional[UP]:
"""Get a single user by id."""
raise NotImplementedError()
async def get_by_email(self, email: str) -> Optional[UD]:
async def get_by_email(self, email: str) -> Optional[UP]:
"""Get a single user by email."""
raise NotImplementedError()
async def get_by_oauth_account(self, oauth: str, account_id: str) -> Optional[UD]:
async def get_by_oauth_account(self, oauth: str, account_id: str) -> Optional[UP]:
"""Get a single user by OAuth account id."""
raise NotImplementedError()
async def create(self, user: UD) -> UD:
async def create(self, create_dict: Dict[str, Any]) -> UP:
"""Create a user."""
raise NotImplementedError()
async def update(self, user: UD) -> UD:
async def update(self, user: UP, update_dict: Dict[str, Any]) -> UP:
"""Update a user."""
raise NotImplementedError()
async def delete(self, user: UD) -> None:
async def delete(self, user: UP) -> None:
"""Delete a user."""
raise NotImplementedError()
async def add_oauth_account(
self: "BaseUserDatabase[UOAP, ID]", user: UOAP, create_dict: Dict[str, Any]
) -> UOAP:
"""Create an OAuth account and add it to the user."""
raise NotImplementedError()
UserDatabaseDependency = DependencyCallable[BaseUserDatabase[UD]]
async def update_oauth_account(
self: "BaseUserDatabase[UOAP, ID]",
user: UOAP,
oauth_account: OAP,
update_dict: Dict[str, Any],
) -> UOAP:
"""Update an OAuth account on a user."""
raise NotImplementedError()
UserDatabaseDependency = DependencyCallable[BaseUserDatabase[UP, ID]]

View File

@@ -2,7 +2,7 @@ from typing import Generic, Sequence, Type
from fastapi import APIRouter
from fastapi_users import models
from fastapi_users import models, schemas
from fastapi_users.authentication import AuthenticationBackend, Authenticator
from fastapi_users.jwt import SecretType
from fastapi_users.manager import UserManagerDependency
@@ -22,58 +22,49 @@ except ModuleNotFoundError: # pragma: no cover
BaseOAuth2 = Type # type: ignore
class FastAPIUsers(Generic[models.U, models.UC, models.UU, models.UD]):
class FastAPIUsers(Generic[models.UP, models.ID]):
"""
Main object that ties together the component for users authentication.
:param get_user_manager: Dependency callable getter to inject the
user manager class instance.
:param auth_backends: List of authentication backends.
:param user_model: Pydantic model of a user.
:param user_create_model: Pydantic model for creating a user.
:param user_update_model: Pydantic model for updating a user.
:param user_db_model: Pydantic model of a DB representation of a user.
:attribute current_user: Dependency callable getter to inject authenticated user
with a specific set of parameters.
"""
authenticator: Authenticator
_user_model: Type[models.U]
_user_create_model: Type[models.UC]
_user_update_model: Type[models.UU]
_user_db_model: Type[models.UD]
def __init__(
self,
get_user_manager: UserManagerDependency[models.UC, models.UD],
get_user_manager: UserManagerDependency[models.UP, models.ID],
auth_backends: Sequence[AuthenticationBackend],
user_model: Type[models.U],
user_create_model: Type[models.UC],
user_update_model: Type[models.UU],
user_db_model: Type[models.UD],
):
self.authenticator = Authenticator(auth_backends, get_user_manager)
self._user_model = user_model
self._user_db_model = user_db_model
self._user_create_model = user_create_model
self._user_update_model = user_update_model
self.get_user_manager = get_user_manager
self.current_user = self.authenticator.current_user
def get_register_router(self) -> APIRouter:
"""Return a router with a register route."""
def get_register_router(
self, user_schema: Type[schemas.U], user_create_schema: Type[schemas.UC]
) -> APIRouter:
"""
Return a router with a register route.
:param user_schema: Pydantic schema of a public user.
:param user_create_schema: Pydantic schema for creating a user.
"""
return get_register_router(
self.get_user_manager,
self._user_model,
self._user_create_model,
self.get_user_manager, user_schema, user_create_schema
)
def get_verify_router(self) -> APIRouter:
"""Return a router with e-mail verification routes."""
return get_verify_router(self.get_user_manager, self._user_model)
def get_verify_router(self, user_schema: Type[schemas.U]) -> APIRouter:
"""
Return a router with e-mail verification routes.
:param user_schema: Pydantic schema of a public user.
"""
return get_verify_router(self.get_user_manager, user_schema)
def get_reset_password_router(self) -> APIRouter:
"""Return a reset password process router."""
@@ -122,19 +113,22 @@ class FastAPIUsers(Generic[models.U, models.UC, models.UU, models.UD]):
def get_users_router(
self,
user_schema: Type[schemas.U],
user_update_schema: Type[schemas.UU],
requires_verification: bool = False,
) -> APIRouter:
"""
Return a router with routes to manage users.
:param user_schema: Pydantic schema of a public user.
:param user_update_schema: Pydantic schema for updating a user.
:param requires_verification: Whether the endpoints
require the users to be verified or not.
"""
return get_users_router(
self.get_user_manager,
self._user_model,
self._user_update_model,
self._user_db_model,
user_schema,
user_update_schema,
self.authenticator,
requires_verification,
)

View File

@@ -1,11 +1,11 @@
from typing import Any, Dict, Generic, Optional, Type, Union
import uuid
from typing import Any, Dict, Generic, Optional, Union
import jwt
from fastapi import Request
from fastapi.security import OAuth2PasswordRequestForm
from pydantic import UUID4
from fastapi_users import models
from fastapi_users import models, schemas
from fastapi_users.db import BaseUserDatabase
from fastapi_users.jwt import SecretType, decode_jwt, generate_jwt
from fastapi_users.password import PasswordHelper, PasswordHelperProtocol
@@ -19,6 +19,10 @@ class FastAPIUsersException(Exception):
pass
class InvalidID(FastAPIUsersException):
pass
class UserAlreadyExists(FastAPIUsersException):
pass
@@ -48,11 +52,10 @@ class InvalidPasswordException(FastAPIUsersException):
self.reason = reason
class BaseUserManager(Generic[models.UC, models.UD]):
class BaseUserManager(Generic[models.UP, models.ID]):
"""
User management logic.
:attribute user_db_model: Pydantic model of a DB representation of a user.
:attribute reset_password_token_secret: Secret to encode reset password token.
:attribute reset_password_token_lifetime_seconds: Lifetime of reset password token.
:attribute reset_password_token_audience: JWT audience of reset password token.
@@ -63,7 +66,6 @@ class BaseUserManager(Generic[models.UC, models.UD]):
:param user_db: Database adapter instance.
"""
user_db_model: Type[models.UD]
reset_password_token_secret: SecretType
reset_password_token_lifetime_seconds: int = 3600
reset_password_token_audience: str = RESET_PASSWORD_TOKEN_AUDIENCE
@@ -72,12 +74,12 @@ class BaseUserManager(Generic[models.UC, models.UD]):
verification_token_lifetime_seconds: int = 3600
verification_token_audience: str = VERIFY_USER_TOKEN_AUDIENCE
user_db: BaseUserDatabase[models.UD]
user_db: BaseUserDatabase[models.UP, models.ID]
password_helper: PasswordHelperProtocol
def __init__(
self,
user_db: BaseUserDatabase[models.UD],
user_db: BaseUserDatabase[models.UP, models.ID],
password_helper: Optional[PasswordHelperProtocol] = None,
):
self.user_db = user_db
@@ -86,7 +88,17 @@ class BaseUserManager(Generic[models.UC, models.UD]):
else:
self.password_helper = password_helper # pragma: no cover
async def get(self, id: UUID4) -> models.UD:
def parse_id(self, value: Any) -> models.ID:
"""
Parse a value into a correct models.ID instance.
:param value: The value to parse.
:raises InvalidID: The models.ID value is invalid.
:return: An models.ID object.
"""
raise NotImplementedError() # pragma: no cover
async def get(self, id: models.ID) -> models.UP:
"""
Get a user by id.
@@ -101,7 +113,7 @@ class BaseUserManager(Generic[models.UC, models.UD]):
return user
async def get_by_email(self, user_email: str) -> models.UD:
async def get_by_email(self, user_email: str) -> models.UP:
"""
Get a user by e-mail.
@@ -116,7 +128,7 @@ class BaseUserManager(Generic[models.UC, models.UD]):
return user
async def get_by_oauth_account(self, oauth: str, account_id: str) -> models.UD:
async def get_by_oauth_account(self, oauth: str, account_id: str) -> models.UP:
"""
Get a user by OAuth account.
@@ -133,14 +145,17 @@ class BaseUserManager(Generic[models.UC, models.UD]):
return user
async def create(
self, user: models.UC, safe: bool = False, request: Optional[Request] = None
) -> models.UD:
self,
user_create: schemas.UC,
safe: bool = False,
request: Optional[Request] = None,
) -> models.UP:
"""
Create a user in database.
Triggers the on_after_register handler on success.
:param user: The UserCreate model to create.
:param user_create: The UserCreate model to create.
:param safe: If True, sensitive values like is_superuser or is_verified
will be ignored during the creation, defaults to False.
:param request: Optional FastAPI request that
@@ -148,27 +163,36 @@ class BaseUserManager(Generic[models.UC, models.UD]):
:raises UserAlreadyExists: A user already exists with the same e-mail.
:return: A new user.
"""
await self.validate_password(user.password, user)
await self.validate_password(user_create.password, user_create)
existing_user = await self.user_db.get_by_email(user.email)
existing_user = await self.user_db.get_by_email(user_create.email)
if existing_user is not None:
raise UserAlreadyExists()
hashed_password = self.password_helper.hash(user.password)
user_dict = (
user.create_update_dict() if safe else user.create_update_dict_superuser()
user_create.create_update_dict()
if safe
else user_create.create_update_dict_superuser()
)
db_user = self.user_db_model(**user_dict, hashed_password=hashed_password)
password = user_dict.pop("password")
user_dict["hashed_password"] = self.password_helper.hash(password)
created_user = await self.user_db.create(db_user)
created_user = await self.user_db.create(user_dict)
await self.on_after_register(created_user, request)
return created_user
async def oauth_callback(
self, oauth_account: models.BaseOAuthAccount, request: Optional[Request] = None
) -> models.UD:
self: "BaseUserManager[models.UOAP, models.ID]",
oauth_name: str,
access_token: str,
account_id: str,
account_email: str,
expires_at: Optional[int] = None,
refresh_token: Optional[str] = None,
request: Optional[Request] = None,
) -> models.UOAP:
"""
Handle the callback after a successful OAuth authentication.
@@ -180,50 +204,58 @@ class BaseUserManager(Generic[models.UC, models.UD]):
If the user does not exist, it is created and the on_after_register handler
is triggered.
:param oauth_account: The new OAuth account to create.
:param oauth_name: Name of the OAuth client.
:param access_token: Valid access token for the service provider.
:param account_id: models.ID of the user on the service provider.
:param account_email: E-mail of the user on the service provider.
:param expires_at: Optional timestamp at which the access token expires.
:param refresh_token: Optional refresh token to get a
fresh access token from the service provider.
:param request: Optional FastAPI request that
triggered the operation, defaults to None
:return: A user.
"""
oauth_account_dict = {
"oauth_name": oauth_name,
"access_token": access_token,
"account_id": account_id,
"account_email": account_email,
"expires_at": expires_at,
"refresh_token": refresh_token,
}
try:
user = await self.get_by_oauth_account(
oauth_account.oauth_name, oauth_account.account_id
)
user = await self.get_by_oauth_account(oauth_name, account_id)
except UserNotExists:
try:
# Link account
user = await self.get_by_email(oauth_account.account_email)
user.oauth_accounts.append(oauth_account) # type: ignore
await self.user_db.update(user)
user = await self.get_by_email(account_email)
user = await self.user_db.add_oauth_account(user, oauth_account_dict)
except UserNotExists:
# Create account
password = self.password_helper.generate()
user = self.user_db_model(
email=oauth_account.account_email,
hashed_password=self.password_helper.hash(password),
oauth_accounts=[oauth_account],
)
await self.user_db.create(user)
user_dict = {
"email": account_email,
"hashed_password": self.password_helper.hash(password),
}
user = await self.user_db.create(user_dict)
user = await self.user_db.add_oauth_account(user, oauth_account_dict)
await self.on_after_register(user, request)
else:
# Update oauth
updated_oauth_accounts = []
for existing_oauth_account in user.oauth_accounts: # type: ignore
for existing_oauth_account in user.oauth_accounts:
if (
existing_oauth_account.account_id == oauth_account.account_id
and existing_oauth_account.oauth_name == oauth_account.oauth_name
existing_oauth_account.account_id == account_id
and existing_oauth_account.oauth_name == oauth_name
):
oauth_account.id = existing_oauth_account.id
updated_oauth_accounts.append(oauth_account)
else:
updated_oauth_accounts.append(existing_oauth_account)
user.oauth_accounts = updated_oauth_accounts # type: ignore
await self.user_db.update(user)
user = await self.user_db.update_oauth_account(
user, existing_oauth_account, oauth_account_dict
)
return user
async def request_verify(
self, user: models.UD, request: Optional[Request] = None
self, user: models.UP, request: Optional[Request] = None
) -> None:
"""
Start a verification request.
@@ -253,7 +285,7 @@ class BaseUserManager(Generic[models.UC, models.UD]):
)
await self.on_after_request_verify(user, token, request)
async def verify(self, token: str, request: Optional[Request] = None) -> models.UD:
async def verify(self, token: str, request: Optional[Request] = None) -> models.UP:
"""
Validate a verification request.
@@ -289,11 +321,11 @@ class BaseUserManager(Generic[models.UC, models.UD]):
raise InvalidVerifyToken()
try:
user_uuid = UUID4(user_id)
except ValueError:
parsed_id = self.parse_id(user_id)
except InvalidID:
raise InvalidVerifyToken()
if user_uuid != user.id:
if parsed_id != user.id:
raise InvalidVerifyToken()
if user.is_verified:
@@ -306,7 +338,7 @@ class BaseUserManager(Generic[models.UC, models.UD]):
return verified_user
async def forgot_password(
self, user: models.UD, request: Optional[Request] = None
self, user: models.UP, request: Optional[Request] = None
) -> None:
"""
Start a forgot password request.
@@ -334,7 +366,7 @@ class BaseUserManager(Generic[models.UC, models.UD]):
async def reset_password(
self, token: str, password: str, request: Optional[Request] = None
) -> models.UD:
) -> models.UP:
"""
Reset the password of a user.
@@ -364,11 +396,11 @@ class BaseUserManager(Generic[models.UC, models.UD]):
raise InvalidResetPasswordToken()
try:
user_uuid = UUID4(user_id)
except ValueError:
parsed_id = self.parse_id(user_id)
except InvalidID:
raise InvalidResetPasswordToken()
user = await self.get(user_uuid)
user = await self.get(parsed_id)
if not user.is_active:
raise UserInactive()
@@ -381,11 +413,11 @@ class BaseUserManager(Generic[models.UC, models.UD]):
async def update(
self,
user_update: models.UU,
user: models.UD,
user_update: schemas.UU,
user: models.UP,
safe: bool = False,
request: Optional[Request] = None,
) -> models.UD:
) -> models.UP:
"""
Update a user.
@@ -408,7 +440,7 @@ class BaseUserManager(Generic[models.UC, models.UD]):
await self.on_after_update(updated_user, updated_user_data, request)
return updated_user
async def delete(self, user: models.UD) -> None:
async def delete(self, user: models.UP) -> None:
"""
Delete a user.
@@ -417,7 +449,7 @@ class BaseUserManager(Generic[models.UC, models.UD]):
await self.user_db.delete(user)
async def validate_password(
self, password: str, user: Union[models.UC, models.UD]
self, password: str, user: Union[schemas.UC, models.UP]
) -> None:
"""
Validate a password.
@@ -432,7 +464,7 @@ class BaseUserManager(Generic[models.UC, models.UD]):
return # pragma: no cover
async def on_after_register(
self, user: models.UD, request: Optional[Request] = None
self, user: models.UP, request: Optional[Request] = None
) -> None:
"""
Perform logic after successful user registration.
@@ -447,7 +479,7 @@ class BaseUserManager(Generic[models.UC, models.UD]):
async def on_after_update(
self,
user: models.UD,
user: models.UP,
update_dict: Dict[str, Any],
request: Optional[Request] = None,
) -> None:
@@ -464,7 +496,7 @@ class BaseUserManager(Generic[models.UC, models.UD]):
return # pragma: no cover
async def on_after_request_verify(
self, user: models.UD, token: str, request: Optional[Request] = None
self, user: models.UP, token: str, request: Optional[Request] = None
) -> None:
"""
Perform logic after successful verification request.
@@ -479,7 +511,7 @@ class BaseUserManager(Generic[models.UC, models.UD]):
return # pragma: no cover
async def on_after_verify(
self, user: models.UD, request: Optional[Request] = None
self, user: models.UP, request: Optional[Request] = None
) -> None:
"""
Perform logic after successful user verification.
@@ -493,7 +525,7 @@ class BaseUserManager(Generic[models.UC, models.UD]):
return # pragma: no cover
async def on_after_forgot_password(
self, user: models.UD, token: str, request: Optional[Request] = None
self, user: models.UP, token: str, request: Optional[Request] = None
) -> None:
"""
Perform logic after successful forgot password request.
@@ -508,7 +540,7 @@ class BaseUserManager(Generic[models.UC, models.UD]):
return # pragma: no cover
async def on_after_reset_password(
self, user: models.UD, request: Optional[Request] = None
self, user: models.UP, request: Optional[Request] = None
) -> None:
"""
Perform logic after successful password reset.
@@ -523,7 +555,7 @@ class BaseUserManager(Generic[models.UC, models.UD]):
async def authenticate(
self, credentials: OAuth2PasswordRequestForm
) -> Optional[models.UD]:
) -> Optional[models.UP]:
"""
Authenticate and return a user following an email and a password.
@@ -546,27 +578,48 @@ class BaseUserManager(Generic[models.UC, models.UD]):
return None
# Update password hash to a more robust one if needed
if updated_password_hash is not None:
user.hashed_password = updated_password_hash
await self.user_db.update(user)
await self.user_db.update(user, {"hashed_password": updated_password_hash})
return user
async def _update(self, user: models.UD, update_dict: Dict[str, Any]) -> models.UD:
async def _update(self, user: models.UP, update_dict: Dict[str, Any]) -> models.UP:
validated_update_dict = {}
for field, value in update_dict.items():
if field == "email" and value != user.email:
try:
await self.get_by_email(value)
raise UserAlreadyExists()
except UserNotExists:
user.email = value
user.is_verified = False
validated_update_dict["email"] = value
validated_update_dict["is_verified"] = False
elif field == "password":
await self.validate_password(value, user)
hashed_password = self.password_helper.hash(value)
user.hashed_password = hashed_password
validated_update_dict["hashed_password"] = self.password_helper.hash(
value
)
else:
setattr(user, field, value)
return await self.user_db.update(user)
validated_update_dict[field] = value
return await self.user_db.update(user, validated_update_dict)
UserManagerDependency = DependencyCallable[BaseUserManager[models.UC, models.UD]]
class UUIDIDMixin:
def parse_id(self, value: Any) -> uuid.UUID:
if isinstance(value, uuid.UUID):
return value
try:
return uuid.UUID(value)
except ValueError as e:
raise InvalidID() from e
class IntegerIDMixin:
def parse_id(self, value: Any) -> int:
if isinstance(value, float):
raise InvalidID()
try:
return int(value)
except ValueError as e:
raise InvalidID() from e
UserManagerDependency = DependencyCallable[BaseUserManager[models.UP, models.ID]]

Some files were not shown because too many files have changed in this diff Show More