mirror of
https://github.com/fastapi-users/fastapi-users.git
synced 2026-03-13 07:49:55 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
```
|
||||
|
||||
|
||||
75
docs/configuration/databases/beanie.md
Normal file
75
docs/configuration/databases/beanie.md
Normal 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!
|
||||
@@ -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`.
|
||||
@@ -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/).
|
||||
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -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"
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"],
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"],
|
||||
)
|
||||
|
||||
@@ -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"],
|
||||
)
|
||||
|
||||
73
docs/configuration/schemas.md
Normal file
73
docs/configuration/schemas.md
Normal 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.
|
||||
@@ -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.")
|
||||
```
|
||||
|
||||
@@ -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
361
docs/migration/9x_to_10x.md
Normal 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).
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
29
docs/src/db_beanie_access_tokens.py
Normal file
29
docs/src/db_beanie_access_tokens.py
Normal 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)
|
||||
24
docs/src/db_beanie_oauth.py
Normal file
24
docs/src/db_beanie_oauth.py
Normal 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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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}")
|
||||
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
)
|
||||
24
examples/beanie-oauth/app/db.py
Normal file
24
examples/beanie-oauth/app/db.py
Normal 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)
|
||||
14
examples/beanie-oauth/app/schemas.py
Normal file
14
examples/beanie-oauth/app/schemas.py
Normal 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
|
||||
@@ -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)
|
||||
4
examples/beanie-oauth/main.py
Normal file
4
examples/beanie-oauth/main.py
Normal file
@@ -0,0 +1,4 @@
|
||||
import uvicorn
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run("app.app:app", host="0.0.0.0", log_level="info")
|
||||
@@ -1,3 +1,3 @@
|
||||
fastapi
|
||||
fastapi-users[mongodb]
|
||||
fastapi-users[beanie]
|
||||
uvicorn[standard]
|
||||
47
examples/beanie/app/app.py
Normal file
47
examples/beanie/app/app.py
Normal 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
17
examples/beanie/app/db.py
Normal 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)
|
||||
14
examples/beanie/app/schemas.py
Normal file
14
examples/beanie/app/schemas.py
Normal 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
|
||||
@@ -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
4
examples/beanie/main.py
Normal file
@@ -0,0 +1,4 @@
|
||||
import uvicorn
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run("app.app:app", host="0.0.0.0", log_level="info")
|
||||
3
examples/beanie/requirements.txt
Normal file
3
examples/beanie/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
fastapi
|
||||
fastapi-users[beanie]
|
||||
uvicorn[standard]
|
||||
@@ -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}!"}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -1,4 +0,0 @@
|
||||
import uvicorn
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run("app.app:app", host="0.0.0.0", port=5000, log_level="info")
|
||||
@@ -1,3 +0,0 @@
|
||||
fastapi
|
||||
fastapi-users[mongodb,oauth]
|
||||
uvicorn[standard]
|
||||
@@ -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}!"}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -1,4 +0,0 @@
|
||||
import uvicorn
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run("app.app:app", host="0.0.0.0", port=5000, log_level="info")
|
||||
@@ -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}!"}
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
15
examples/sqlalchemy-oauth/app/schemas.py
Normal file
15
examples/sqlalchemy-oauth/app/schemas.py
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
fastapi
|
||||
fastapi-users[sqlalchemy2,oauth]
|
||||
fastapi-users[sqlalchemy]
|
||||
uvicorn[standard]
|
||||
aiosqlite
|
||||
|
||||
@@ -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}!"}
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
15
examples/sqlalchemy/app/schemas.py
Normal file
15
examples/sqlalchemy/app/schemas.py
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
fastapi
|
||||
fastapi-users[sqlalchemy2]
|
||||
fastapi-users[sqlalchemy]
|
||||
uvicorn[standard]
|
||||
aiosqlite
|
||||
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -1,4 +0,0 @@
|
||||
import uvicorn
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run("app.app:app", host="0.0.0.0", port=5000, log_level="info")
|
||||
@@ -1,3 +0,0 @@
|
||||
fastapi
|
||||
fastapi-users[tortoise-orm,oauth]
|
||||
uvicorn[standard]
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -1,4 +0,0 @@
|
||||
import uvicorn
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run("app.app:app", host="0.0.0.0", port=5000, log_level="info")
|
||||
@@ -1,3 +0,0 @@
|
||||
fastapi
|
||||
fastapi-users[tortoise-orm]
|
||||
uvicorn[standard]
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]]
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user