mirror of
https://github.com/fastapi-users/fastapi-users.git
synced 2026-03-13 07:49:55 +08:00
Revamp authentication routes structure (#201)
* Fix #68: use makefun to generate dynamic dependencies * Remove every Starlette imports * Split every routers and remove event handlers * Make users router optional * Pass after_update handler to get_users_router * Update documentation * Remove test file * Write migration doc for splitted routers
This commit is contained in:
1
Pipfile
1
Pipfile
@@ -39,6 +39,7 @@ pyjwt = "==1.7.1"
|
||||
python-multipart = "==0.0.5"
|
||||
motor = "==2.1.0"
|
||||
tortoise-orm = ">=0.15.18,<0.17.0"
|
||||
makefun = ">=1.9.2,<1.10"
|
||||
|
||||
[requires]
|
||||
python_version = "3.7"
|
||||
|
||||
32
Pipfile.lock
generated
32
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "cd178656ef0c106c2a71e1c48b5da4ef08a8dcc650838b9f21ba8c30c2cc417c"
|
||||
"sha256": "adf89bd2e09507bfc26904d8fa1a98ceb16f1f9c1486bbca53834d68f85c40f2"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@@ -123,6 +123,14 @@
|
||||
],
|
||||
"version": "==2.9"
|
||||
},
|
||||
"makefun": {
|
||||
"hashes": [
|
||||
"sha256:3fb5993cfe3b318ea8bb820a707898c8dfd15e3864a1c923430c35528094d146",
|
||||
"sha256:8ebc8f5bbd84a010ad67f0ee6bf6ca896481869aba2a740809590f8bff7e7b94"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.9.2"
|
||||
},
|
||||
"motor": {
|
||||
"hashes": [
|
||||
"sha256:599719bc6dcddc3b9ea4e09659fb0073d5fadcc24735999b2902f48cef33f909",
|
||||
@@ -253,10 +261,10 @@
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
|
||||
"sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
|
||||
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
||||
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
|
||||
],
|
||||
"version": "==1.14.0"
|
||||
"version": "==1.15.0"
|
||||
},
|
||||
"sqlalchemy": {
|
||||
"hashes": [
|
||||
@@ -399,10 +407,10 @@
|
||||
},
|
||||
"codecov": {
|
||||
"hashes": [
|
||||
"sha256:43ad6cb3e7de073d911aa3ab6a754db88d270fcb0e0d8e2062b964098a51d69b"
|
||||
"sha256:2ebd639d8f621aabcce399e475b0302e436cb7e00e7724d1b2224bbf3f215a0c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.1.0"
|
||||
"version": "==2.1.3"
|
||||
},
|
||||
"coverage": {
|
||||
"hashes": [
|
||||
@@ -728,10 +736,10 @@
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3",
|
||||
"sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"
|
||||
"sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8",
|
||||
"sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"
|
||||
],
|
||||
"version": "==20.3"
|
||||
"version": "==20.4"
|
||||
},
|
||||
"pathspec": {
|
||||
"hashes": [
|
||||
@@ -895,10 +903,10 @@
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
|
||||
"sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
|
||||
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
||||
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
|
||||
],
|
||||
"version": "==1.14.0"
|
||||
"version": "==1.15.0"
|
||||
},
|
||||
"sniffio": {
|
||||
"hashes": [
|
||||
|
||||
@@ -37,6 +37,7 @@ Add quickly a registration and authentication system to your [FastAPI](https://f
|
||||
* [X] Multiple customizable authentication backends
|
||||
* [X] JWT authentication backend included
|
||||
* [X] Cookie authentication backend included
|
||||
* [X] Full OpenAPI schema support, even with several authentication backends
|
||||
|
||||
## Development
|
||||
|
||||
|
||||
@@ -18,16 +18,6 @@ auth_backends.append(cookie_authentication)
|
||||
|
||||
As you can see, instantiation is quite simple. You just have to define a constant `SECRET` which is used to encode the token and the lifetime of the cookie (in seconds).
|
||||
|
||||
You can optionally define the `name` which will be used to generate its [`/login` route](../../usage/routes.md#post-loginname). **Defaults to `cookie`**.
|
||||
|
||||
```py
|
||||
cookie_authentication = CookieAuthentication(
|
||||
secret=SECRET,
|
||||
lifetime_seconds=3600,
|
||||
name="my-cookie",
|
||||
)
|
||||
```
|
||||
|
||||
You can also define the parameters for the generated cookie:
|
||||
|
||||
* `cookie_name` (`fastapiusersauth`): Name of the cookie.
|
||||
@@ -36,6 +26,17 @@ You can also define the parameters for the generated cookie:
|
||||
* `cookie_secure` (`True`): Whether to only send the cookie to the server via SSL request.
|
||||
* `cookie_httponly` (`True`): Whether to prevent access to the cookie via JavaScript.
|
||||
|
||||
!!! tip
|
||||
You can also optionally define the `name`. It's useful in the case you wish to have several backends of the same class. Each backend should have a unique name. **Defaults to `cookie`**.
|
||||
|
||||
```py
|
||||
cookie_authentication = CookieAuthentication(
|
||||
secret=SECRET,
|
||||
lifetime_seconds=3600,
|
||||
name="my-cookie",
|
||||
)
|
||||
```
|
||||
|
||||
!!! tip
|
||||
The value of the cookie is actually a JWT. This authentication backend shares most of its logic with the [JWT](./jwt.md) one.
|
||||
|
||||
@@ -61,4 +62,4 @@ This method expects that you provide a valid cookie in the headers.
|
||||
|
||||
## Next steps
|
||||
|
||||
We will now configure the main **FastAPI Users** object that will expose the [API router](../router.md).
|
||||
We will now configure the main **FastAPI Users** object that will expose the [routers](../routers/index.md).
|
||||
|
||||
@@ -8,9 +8,7 @@ You can have **several** authentication methods, e.g. a cookie authentication fo
|
||||
|
||||
When checking authentication, each method is run one after the other. The first method yielding a user wins. If no method yields a user, an `HTTPException` is raised.
|
||||
|
||||
Each defined method will generate a [`/login/{name}`](../../usage/routes.md#post-loginname) route where `name` is defined on the authentication method object.
|
||||
|
||||
Each defined method will generate a [`/logout/{name}`](../../usage/routes.md#post-logoutname) route where `name` is defined on the authentication method object.
|
||||
For each backend, you'll be able to add a router with the corresponding `/login` and `/logout` (if applicable routes). More on this in the [routers documentation](routers/index.md).
|
||||
|
||||
## Provided methods
|
||||
|
||||
|
||||
@@ -18,15 +18,16 @@ auth_backends.append(jwt_authentication)
|
||||
|
||||
As you can see, instantiation is quite simple. You just have to define a constant `SECRET` which is used to encode the token and the lifetime of token (in seconds).
|
||||
|
||||
You can also optionally define the `name` which will be used to generate its [`/login` route](../../usage/routes.md#post-loginname). **Defaults to `jwt`**.
|
||||
!!! tip
|
||||
You can also optionally define the `name`. It's useful in the case you wish to have several backends of the same class. Each backend should have a unique name. **Defaults to `jwt`**.
|
||||
|
||||
```py
|
||||
jwt_authentication = JWTAuthentication(
|
||||
secret=SECRET,
|
||||
lifetime_seconds=3600,
|
||||
name="my-jwt",
|
||||
)
|
||||
```
|
||||
```py
|
||||
jwt_authentication = JWTAuthentication(
|
||||
secret=SECRET,
|
||||
lifetime_seconds=3600,
|
||||
name="my-jwt",
|
||||
)
|
||||
```
|
||||
|
||||
## Login
|
||||
|
||||
@@ -43,11 +44,7 @@ This method will return a JWT token upon successful login:
|
||||
|
||||
## Logout
|
||||
|
||||
This method is not applicable to this backend and won't do anything.
|
||||
|
||||
!!! success "`202 Accepted`"
|
||||
|
||||
> Check documentation about [logout route](../../usage/routes.md#post-logoutname).
|
||||
This backend does not provide a logout method (a JWT is valid until it expires).
|
||||
|
||||
## Authentication
|
||||
|
||||
@@ -59,4 +56,4 @@ curl http://localhost:9000/protected-route -H'Authorization: Bearer eyJ0eXAiOiJK
|
||||
|
||||
## Next steps
|
||||
|
||||
We will now configure the main **FastAPI Users** object that will expose the [API router](../router.md).
|
||||
We will now configure the main **FastAPI Users** object that will expose the [routers](../routers/index.md).
|
||||
|
||||
@@ -121,12 +121,43 @@ google_oauth_client = GoogleOAuth2("CLIENT_ID", "CLIENT_SECRET")
|
||||
|
||||
app = FastAPI()
|
||||
fastapi_users = FastAPIUsers(
|
||||
user_db, auth_backends, User, UserCreate, UserUpdate, UserDB, SECRET,
|
||||
user_db, auth_backends, User, UserCreate, UserUpdate, UserDB
|
||||
)
|
||||
|
||||
google_oauth_router = fastapi_users.get_oauth_router(google_oauth_client, SECRET)
|
||||
|
||||
app.include_router(google_oauth_router, prefix="/google-oauth", tags=["users"])
|
||||
app.include_router(google_oauth_router, prefix="/auth/google", tags=["auth"])
|
||||
```
|
||||
|
||||
### After register
|
||||
|
||||
You can provide a custom function to be called after a successful registration. It is called with **two argument**: the **user** that has just registered, and the original **`Request` object**.
|
||||
|
||||
Typically, you'll want to **send a welcome e-mail** or add it to your marketing analytics pipeline.
|
||||
|
||||
You can define it as an `async` or standard method.
|
||||
|
||||
Example:
|
||||
|
||||
```py
|
||||
from fastapi import FastAPI
|
||||
from fastapi_users import FastAPIUsers
|
||||
from httpx_oauth.clients.google import GoogleOAuth2
|
||||
|
||||
|
||||
def on_after_register(user: UserDB, request: Request):
|
||||
print(f"User {user.id} has registered.")
|
||||
|
||||
google_oauth_client = GoogleOAuth2("CLIENT_ID", "CLIENT_SECRET")
|
||||
|
||||
app = FastAPI()
|
||||
fastapi_users = FastAPIUsers(
|
||||
user_db, auth_backends, User, UserCreate, UserUpdate, UserDB
|
||||
)
|
||||
|
||||
google_oauth_router = fastapi_users.get_oauth_router(google_oauth_client, SECRET, after_register=on_after_register)
|
||||
|
||||
app.include_router(google_oauth_router, prefix="/auth/google", tags=["auth"])
|
||||
```
|
||||
|
||||
### Full example
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
# Router
|
||||
|
||||
We're almost there! The last step is to configure the `FastAPIUsers` object that will wire the database adapter, the authentication class and the user models to expose the FastAPI router.
|
||||
|
||||
## Configure `FastAPIUsers`
|
||||
|
||||
Configure `FastAPIUsers` object with all the elements we defined before. More precisely:
|
||||
|
||||
* `db`: Database adapter instance.
|
||||
* `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.
|
||||
* `reset_password_token_secret`: Secret to encode reset password token.
|
||||
* `reset_password_token_lifetime_seconds`: Lifetime of reset password token in seconds. Default to one hour.
|
||||
|
||||
```py
|
||||
from fastapi_users import FastAPIUsers
|
||||
|
||||
fastapi_users = FastAPIUsers(
|
||||
user_db,
|
||||
auth_backends,
|
||||
User,
|
||||
UserCreate,
|
||||
UserUpdate,
|
||||
UserDB,
|
||||
SECRET,
|
||||
)
|
||||
```
|
||||
|
||||
And then, include the router in the FastAPI app:
|
||||
|
||||
```py
|
||||
app = FastAPI()
|
||||
app.include_router(fastapi_users.router, prefix="/users", tags=["users"])
|
||||
```
|
||||
|
||||
## Event handlers
|
||||
|
||||
In order to be as unopinionated as possible, we expose decorators that allow you to plug your own logic after some actions. You can have several handlers per event.
|
||||
|
||||
### After register
|
||||
|
||||
This event handler is called after a successful registration. It is called with **two argument**: the **user** that has just registered, and the original **`Request` object**.
|
||||
|
||||
Typically, you'll want to **send a welcome e-mail** or add it to your marketing analytics pipeline.
|
||||
|
||||
You can define it as an `async` or standard method.
|
||||
|
||||
Example:
|
||||
|
||||
```py
|
||||
@fastapi_users.on_after_register()
|
||||
def on_after_register(user: User, request: Request):
|
||||
print(f"User {user.id} has registered.")
|
||||
```
|
||||
|
||||
### After forgot password
|
||||
|
||||
This event handler is called after a successful forgot password request. It is called with **three arguments**:
|
||||
|
||||
* The **user** which has requested to reset their password.
|
||||
* A ready-to-use **JWT token** that will be accepted by the reset password route.
|
||||
* The original **`Request` object**.
|
||||
|
||||
Typically, you'll want to **send an e-mail** with the link (and the token) that allows the user to reset their password.
|
||||
|
||||
You can define it as an `async` or standard method.
|
||||
|
||||
Example:
|
||||
|
||||
```py
|
||||
@fastapi_users.on_after_forgot_password()
|
||||
def on_after_forgot_password(user: User, token: str, request: Request):
|
||||
print(f"User {user.id} has forgot their password. Reset token: {token}")
|
||||
```
|
||||
|
||||
### After update
|
||||
|
||||
This event handler is called after a successful update user request. It is called with **three arguments**:
|
||||
|
||||
* The **user** which was updated.
|
||||
* The dictionary containing the updated fields.
|
||||
* The original **`Request` object**.
|
||||
|
||||
It may be useful if you wish for example update your user in a data analytics or customer success platform.
|
||||
|
||||
You can define it as an `async` or standard method.
|
||||
|
||||
Example:
|
||||
|
||||
```py
|
||||
@fastapi_users.on_after_update()
|
||||
def on_after_update(user: User, updated_user_data: Dict[str, Any], request: Request):
|
||||
print(f"User {user.id} has been updated with the following data: {updated_user_data}")
|
||||
```
|
||||
|
||||
## Next steps
|
||||
|
||||
Check out a [full example](full_example.md) that will show you the big picture.
|
||||
33
docs/configuration/routers/auth.md
Normal file
33
docs/configuration/routers/auth.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Auth router
|
||||
|
||||
The auth router will generate `/login` and `/logout` (if applicable) routes for a given [authentication backend](../authentication/index.md).
|
||||
|
||||
Check the [routes usage](../../usage/routes.md) to learn how to use them.
|
||||
|
||||
## Setup
|
||||
|
||||
```py
|
||||
from fastapi import FastAPI
|
||||
from fastapi_users import FastAPIUsers
|
||||
from fastapi_users.authentication import JWTAuthentication
|
||||
|
||||
SECRET = "SECRET"
|
||||
|
||||
jwt_authentication = JWTAuthentication(secret=SECRET, lifetime_seconds=3600))
|
||||
|
||||
fastapi_users = FastAPIUsers(
|
||||
user_db,
|
||||
[jwt_authentication],
|
||||
User,
|
||||
UserCreate,
|
||||
UserUpdate,
|
||||
UserDB,
|
||||
)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(
|
||||
fastapi_users.get_auth_router(jwt_authentication),
|
||||
prefix="/auth/jwt",
|
||||
tags=["auth"],
|
||||
)
|
||||
```
|
||||
39
docs/configuration/routers/index.md
Normal file
39
docs/configuration/routers/index.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Routers
|
||||
|
||||
We're almost there! The last step is to configure the `FastAPIUsers` object that will wire the database adapter, the authentication classes and let us generate the actual **API routes**.
|
||||
|
||||
## Configure `FastAPIUsers`
|
||||
|
||||
Configure `FastAPIUsers` object with all the elements we defined before. More precisely:
|
||||
|
||||
* `db`: Database adapter instance.
|
||||
* `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
|
||||
from fastapi_users import FastAPIUsers
|
||||
|
||||
fastapi_users = FastAPIUsers(
|
||||
user_db,
|
||||
auth_backends,
|
||||
User,
|
||||
UserCreate,
|
||||
UserUpdate,
|
||||
UserDB,
|
||||
)
|
||||
```
|
||||
|
||||
## 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:
|
||||
|
||||
* [Auth router](./auth.md): Provides `/login` and `/logout` routes for a given [authentication backend](../authentication/index.md).
|
||||
* [Register router](./register.md): Provides `/register` routes to allow a user to create a new account.
|
||||
* [Reset password router](./reset.md): Provides `/forgot-password` and `/reset-password` routes to allow a user to reset its password.
|
||||
* [Users router](./users.md): Provides routes to manage users.
|
||||
* [OAuth router](../oauth.md): Provides routes to perform an OAuth authentication against a service provider (like Google or Facebook).
|
||||
|
||||
You should check out each of them to understand how to use them.
|
||||
54
docs/configuration/routers/register.md
Normal file
54
docs/configuration/routers/register.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Register routes
|
||||
|
||||
The register router will generate a `/register` route to allow a user to create a new account.
|
||||
|
||||
Check the [routes usage](../../usage/routes.md) to learn how to use them.
|
||||
|
||||
## Setup
|
||||
|
||||
```py
|
||||
from fastapi import FastAPI
|
||||
from fastapi_users import FastAPIUsers
|
||||
from fastapi_users.authentication import JWTAuthentication
|
||||
|
||||
SECRET = "SECRET"
|
||||
|
||||
jwt_authentication = JWTAuthentication(secret=SECRET, lifetime_seconds=3600))
|
||||
|
||||
fastapi_users = FastAPIUsers(
|
||||
user_db,
|
||||
[jwt_authentication],
|
||||
User,
|
||||
UserCreate,
|
||||
UserUpdate,
|
||||
UserDB,
|
||||
)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(
|
||||
fastapi_users.get_register_router(),
|
||||
prefix="/auth",
|
||||
tags=["auth"],
|
||||
)
|
||||
```
|
||||
|
||||
## After register
|
||||
|
||||
You can provide a custom function to be called after a successful registration. It is called with **two argument**: the **user** that has just registered, and the original **`Request` object**.
|
||||
|
||||
Typically, you'll want to **send a welcome e-mail** or add it to your marketing analytics pipeline.
|
||||
|
||||
You can define it as an `async` or standard method.
|
||||
|
||||
Example:
|
||||
|
||||
```py
|
||||
def on_after_register(user: UserDB, request: Request):
|
||||
print(f"User {user.id} has registered.")
|
||||
|
||||
app.include_router(
|
||||
fastapi_users.get_register_router(on_after_register),
|
||||
prefix="/auth",
|
||||
tags=["auth"],
|
||||
)
|
||||
```
|
||||
59
docs/configuration/routers/reset.md
Normal file
59
docs/configuration/routers/reset.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Reset password router
|
||||
|
||||
The reset password router will generate `/forgot-password` (the user asks for a token to reset its password) and `/reset-password` (the user changes its password given the token) routes.
|
||||
|
||||
Check the [routes usage](../../usage/routes.md) to learn how to use them.
|
||||
|
||||
## Setup
|
||||
|
||||
```py
|
||||
from fastapi import FastAPI
|
||||
from fastapi_users import FastAPIUsers
|
||||
|
||||
fastapi_users = FastAPIUsers(
|
||||
user_db,
|
||||
auth_backends,
|
||||
User,
|
||||
UserCreate,
|
||||
UserUpdate,
|
||||
UserDB,
|
||||
)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(
|
||||
fastapi_users.get_reset_password_router("SECRET"),
|
||||
prefix="/auth",
|
||||
tags=["auth"],
|
||||
)
|
||||
```
|
||||
|
||||
Parameters:
|
||||
|
||||
* `reset_password_token_secret`: Secret to encode reset password token.
|
||||
* `reset_password_token_lifetime_seconds`: Lifetime of reset password token. **Defaults to 3600**.
|
||||
* `after_forgot_password`: Optional function called after a successful forgot password request. See below.
|
||||
|
||||
## After forgot password
|
||||
|
||||
You can provide a custom function to be called after a successful forgot password request. It is called with **three arguments**:
|
||||
|
||||
* The **user** which has requested to reset their password.
|
||||
* A ready-to-use **JWT token** that will be accepted by the reset password route.
|
||||
* The original **`Request` object**.
|
||||
|
||||
Typically, you'll want to **send an e-mail** with the link (and the token) that allows the user to reset their password.
|
||||
|
||||
You can define it as an `async` or standard method.
|
||||
|
||||
Example:
|
||||
|
||||
```py
|
||||
def on_after_forgot_password(user: UserDB, token: str, request: Request):
|
||||
print(f"User {user.id} has forgot their password. Reset token: {token}")
|
||||
|
||||
app.include_router(
|
||||
fastapi_users.get_reset_password_router("SECRET", after_forgot_password=on_after_forgot_password),
|
||||
prefix="/auth",
|
||||
tags=["auth"],
|
||||
)
|
||||
```
|
||||
51
docs/configuration/routers/users.md
Normal file
51
docs/configuration/routers/users.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Users router
|
||||
|
||||
This router provides routes to manage users. Check the [routes usage](../../usage/routes.md) to learn how to use them.
|
||||
|
||||
## Setup
|
||||
|
||||
```py
|
||||
from fastapi import FastAPI
|
||||
from fastapi_users import FastAPIUsers
|
||||
|
||||
fastapi_users = FastAPIUsers(
|
||||
user_db,
|
||||
auth_backends,
|
||||
User,
|
||||
UserCreate,
|
||||
UserUpdate,
|
||||
UserDB,
|
||||
)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(
|
||||
fastapi_users.get_users_router(),
|
||||
prefix="/users",
|
||||
tags=["users"],
|
||||
)
|
||||
```
|
||||
|
||||
## After update
|
||||
|
||||
You can provide a custom function to be called after a successful update user request. It is called with **three arguments**:
|
||||
|
||||
* The **user** which was updated.
|
||||
* The dictionary containing the updated fields.
|
||||
* The original **`Request` object**.
|
||||
|
||||
It may be useful if you wish for example update your user in a data analytics or customer success platform.
|
||||
|
||||
You can define it as an `async` or standard method.
|
||||
|
||||
Example:
|
||||
|
||||
```py
|
||||
def on_after_update(user: UserDB, updated_user_data: Dict[str, Any], request: Request):
|
||||
print(f"User {user.id} has been updated with the following data: {updated_user_data}")
|
||||
|
||||
app.include_router(
|
||||
fastapi_users.get_users_router(on_after_update),
|
||||
prefix="/users",
|
||||
tags=["users"],
|
||||
)
|
||||
```
|
||||
@@ -80,3 +80,71 @@ db.getCollection('users').find().forEach(function(user) {
|
||||
db.getCollection('users').update({_id: user._id}, [{$set: {id: uuid}}]);
|
||||
});
|
||||
```
|
||||
|
||||
## Splitted routers
|
||||
|
||||
You now have the responsibility to **wire the routers**. FastAPI Users doesn't give a bloated users router anymore.
|
||||
|
||||
**Event handlers** are also removed. You have to provide your "after-" logic as a parameter of the router generator.
|
||||
|
||||
### Before
|
||||
|
||||
```py
|
||||
jwt_authentication = JWTAuthentication(secret=SECRET, lifetime_seconds=3600)
|
||||
|
||||
app = FastAPI()
|
||||
fastapi_users = FastAPIUsers(
|
||||
user_db, [jwt_authentication], User, UserCreate, UserUpdate, UserDB,
|
||||
)
|
||||
app.include_router(fastapi_users.router, prefix="/users", tags=["users"])
|
||||
|
||||
|
||||
@fastapi_users.on_after_register()
|
||||
def on_after_register(user: User, request: Request):
|
||||
print(f"User {user.id} has registered.")
|
||||
|
||||
|
||||
@fastapi_users.on_after_forgot_password()
|
||||
def on_after_forgot_password(user: User, token: str, request: Request):
|
||||
print(f"User {user.id} has forgot their password. Reset token: {token}")
|
||||
```
|
||||
|
||||
### After
|
||||
|
||||
```py
|
||||
def on_after_register(user: UserDB, request: Request):
|
||||
print(f"User {user.id} has registered.")
|
||||
|
||||
|
||||
def on_after_forgot_password(user: UserDB, token: str, request: Request):
|
||||
print(f"User {user.id} has forgot their password. Reset token: {token}")
|
||||
|
||||
|
||||
jwt_authentication = JWTAuthentication(secret=SECRET, lifetime_seconds=3600)
|
||||
|
||||
app = FastAPI()
|
||||
fastapi_users = FastAPIUsers(
|
||||
user_db, [jwt_authentication], User, UserCreate, UserUpdate, UserDB,
|
||||
)
|
||||
app.include_router(
|
||||
fastapi_users.get_auth_router(jwt_authentication), prefix="/auth/jwt", tags=["auth"]
|
||||
)
|
||||
app.include_router(
|
||||
fastapi_users.get_register_router(on_after_register), prefix="/auth", tags=["auth"]
|
||||
)
|
||||
app.include_router(
|
||||
fastapi_users.get_reset_password_router(
|
||||
SECRET, after_forgot_password=on_after_forgot_password
|
||||
),
|
||||
prefix="/auth",
|
||||
tags=["auth"],
|
||||
)
|
||||
app.include_router(fastapi_users.get_users_router(), prefix="/users", tags=["users"])
|
||||
```
|
||||
|
||||
Important things to notice:
|
||||
|
||||
* `FastAPIUsers` takes two arguments less (`reset_password_token_secret` and `reset_password_token_lifetime_seconds`).
|
||||
* You have more flexibility to choose the **prefix** and **tags** of the routers.
|
||||
* The `/login`/`/logout` are now your responsibility to include for each backend. The path will change (before `/login/jwt`, after `/jwt/login`).
|
||||
* If you don't care about some of those routers, you can discard them.
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import motor.motor_asyncio
|
||||
from fastapi import FastAPI
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi_users import FastAPIUsers, models
|
||||
from fastapi_users.authentication import JWTAuthentication
|
||||
from fastapi_users.db import MongoDBUserDatabase
|
||||
from starlette.requests import Request
|
||||
|
||||
DATABASE_URL = "mongodb://localhost:27017"
|
||||
SECRET = "SECRET"
|
||||
@@ -32,22 +31,32 @@ db = client["database_name"]
|
||||
collection = db["users"]
|
||||
user_db = MongoDBUserDatabase(UserDB, collection)
|
||||
|
||||
auth_backends = [
|
||||
JWTAuthentication(secret=SECRET, lifetime_seconds=3600),
|
||||
]
|
||||
|
||||
app = FastAPI()
|
||||
fastapi_users = FastAPIUsers(
|
||||
user_db, auth_backends, User, UserCreate, UserUpdate, UserDB, SECRET,
|
||||
)
|
||||
app.include_router(fastapi_users.router, prefix="/users", tags=["users"])
|
||||
|
||||
|
||||
@fastapi_users.on_after_register()
|
||||
def on_after_register(user: User, request: Request):
|
||||
def on_after_register(user: UserDB, request: Request):
|
||||
print(f"User {user.id} has registered.")
|
||||
|
||||
|
||||
@fastapi_users.on_after_forgot_password()
|
||||
def on_after_forgot_password(user: User, token: str, request: Request):
|
||||
def on_after_forgot_password(user: UserDB, token: str, request: Request):
|
||||
print(f"User {user.id} has forgot their password. Reset token: {token}")
|
||||
|
||||
|
||||
jwt_authentication = JWTAuthentication(secret=SECRET, lifetime_seconds=3600)
|
||||
|
||||
app = FastAPI()
|
||||
fastapi_users = FastAPIUsers(
|
||||
user_db, [jwt_authentication], User, UserCreate, UserUpdate, UserDB,
|
||||
)
|
||||
app.include_router(
|
||||
fastapi_users.get_auth_router(jwt_authentication), prefix="/auth/jwt", tags=["auth"]
|
||||
)
|
||||
app.include_router(
|
||||
fastapi_users.get_register_router(on_after_register), prefix="/auth", tags=["auth"]
|
||||
)
|
||||
app.include_router(
|
||||
fastapi_users.get_reset_password_router(
|
||||
SECRET, after_forgot_password=on_after_forgot_password
|
||||
),
|
||||
prefix="/auth",
|
||||
tags=["auth"],
|
||||
)
|
||||
app.include_router(fastapi_users.get_users_router(), prefix="/users", tags=["users"])
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import databases
|
||||
import sqlalchemy
|
||||
from fastapi import FastAPI
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi_users import FastAPIUsers, models
|
||||
from fastapi_users.authentication import JWTAuthentication
|
||||
from fastapi_users.db import SQLAlchemyBaseUserTable, SQLAlchemyUserDatabase
|
||||
from sqlalchemy.ext.declarative import DeclarativeMeta, declarative_base
|
||||
from starlette.requests import Request
|
||||
|
||||
DATABASE_URL = "sqlite:///./test.db"
|
||||
SECRET = "SECRET"
|
||||
@@ -44,27 +43,36 @@ users = UserTable.__table__
|
||||
user_db = SQLAlchemyUserDatabase(UserDB, database, users)
|
||||
|
||||
|
||||
auth_backends = [
|
||||
JWTAuthentication(secret=SECRET, lifetime_seconds=3600),
|
||||
]
|
||||
|
||||
app = FastAPI()
|
||||
fastapi_users = FastAPIUsers(
|
||||
user_db, auth_backends, User, UserCreate, UserUpdate, UserDB, SECRET,
|
||||
)
|
||||
app.include_router(fastapi_users.router, prefix="/users", tags=["users"])
|
||||
|
||||
|
||||
@fastapi_users.on_after_register()
|
||||
def on_after_register(user: User, request: Request):
|
||||
def on_after_register(user: UserDB, request: Request):
|
||||
print(f"User {user.id} has registered.")
|
||||
|
||||
|
||||
@fastapi_users.on_after_forgot_password()
|
||||
def on_after_forgot_password(user: User, token: str, request: Request):
|
||||
def on_after_forgot_password(user: UserDB, token: str, request: Request):
|
||||
print(f"User {user.id} has forgot their password. Reset token: {token}")
|
||||
|
||||
|
||||
jwt_authentication = JWTAuthentication(secret=SECRET, lifetime_seconds=3600)
|
||||
|
||||
app = FastAPI()
|
||||
fastapi_users = FastAPIUsers(
|
||||
user_db, [jwt_authentication], User, UserCreate, UserUpdate, UserDB,
|
||||
)
|
||||
app.include_router(
|
||||
fastapi_users.get_auth_router(jwt_authentication), prefix="/auth/jwt", tags=["auth"]
|
||||
)
|
||||
app.include_router(
|
||||
fastapi_users.get_register_router(on_after_register), prefix="/auth", tags=["auth"]
|
||||
)
|
||||
app.include_router(
|
||||
fastapi_users.get_reset_password_router(
|
||||
SECRET, after_forgot_password=on_after_forgot_password
|
||||
),
|
||||
prefix="/auth",
|
||||
tags=["auth"],
|
||||
)
|
||||
app.include_router(fastapi_users.get_users_router(), prefix="/users", tags=["users"])
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup():
|
||||
await database.connect()
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi_users import FastAPIUsers, models
|
||||
from fastapi_users.authentication import JWTAuthentication
|
||||
from fastapi_users.db import TortoiseBaseUserModel, TortoiseUserDatabase
|
||||
from starlette.requests import Request
|
||||
from tortoise.contrib.starlette import register_tortoise
|
||||
|
||||
DATABASE_URL = "sqlite://./test.db"
|
||||
@@ -33,21 +32,31 @@ user_db = TortoiseUserDatabase(UserDB, UserModel)
|
||||
app = FastAPI()
|
||||
register_tortoise(app, db_url=DATABASE_URL, modules={"models": ["test"]})
|
||||
|
||||
auth_backends = [
|
||||
JWTAuthentication(secret=SECRET, lifetime_seconds=3600),
|
||||
]
|
||||
|
||||
fastapi_users = FastAPIUsers(
|
||||
user_db, auth_backends, User, UserCreate, UserUpdate, UserDB, SECRET,
|
||||
)
|
||||
app.include_router(fastapi_users.router, prefix="/users", tags=["users"])
|
||||
|
||||
|
||||
@fastapi_users.on_after_register()
|
||||
def on_after_register(user: User, request: Request):
|
||||
def on_after_register(user: UserDB, request: Request):
|
||||
print(f"User {user.id} has registered.")
|
||||
|
||||
|
||||
@fastapi_users.on_after_forgot_password()
|
||||
def on_after_forgot_password(user: User, token: str, request: Request):
|
||||
def on_after_forgot_password(user: UserDB, token: str, request: Request):
|
||||
print(f"User {user.id} has forgot their password. Reset token: {token}")
|
||||
|
||||
|
||||
jwt_authentication = JWTAuthentication(secret=SECRET, lifetime_seconds=3600)
|
||||
|
||||
fastapi_users = FastAPIUsers(
|
||||
user_db, [jwt_authentication], User, UserCreate, UserUpdate, UserDB,
|
||||
)
|
||||
app.include_router(
|
||||
fastapi_users.get_auth_router(jwt_authentication), prefix="/auth/jwt", tags=["auth"]
|
||||
)
|
||||
app.include_router(
|
||||
fastapi_users.get_register_router(on_after_register), prefix="/auth", tags=["auth"]
|
||||
)
|
||||
app.include_router(
|
||||
fastapi_users.get_reset_password_router(
|
||||
SECRET, after_forgot_password=on_after_forgot_password
|
||||
),
|
||||
prefix="/auth",
|
||||
tags=["auth"],
|
||||
)
|
||||
app.include_router(fastapi_users.get_users_router(), prefix="/users", tags=["users"])
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import motor.motor_asyncio
|
||||
from fastapi import FastAPI
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi_users import FastAPIUsers, models
|
||||
from fastapi_users.authentication import JWTAuthentication
|
||||
from fastapi_users.db import MongoDBUserDatabase
|
||||
from httpx_oauth.clients.google import GoogleOAuth2
|
||||
from starlette.requests import Request
|
||||
|
||||
DATABASE_URL = "mongodb://localhost:27017"
|
||||
SECRET = "SECRET"
|
||||
@@ -36,25 +35,37 @@ db = client["database_name"]
|
||||
collection = db["users"]
|
||||
user_db = MongoDBUserDatabase(UserDB, collection)
|
||||
|
||||
auth_backends = [
|
||||
JWTAuthentication(secret=SECRET, lifetime_seconds=3600),
|
||||
]
|
||||
|
||||
app = FastAPI()
|
||||
fastapi_users = FastAPIUsers(
|
||||
user_db, auth_backends, User, UserCreate, UserUpdate, UserDB, SECRET,
|
||||
)
|
||||
app.include_router(fastapi_users.router, prefix="/users", tags=["users"])
|
||||
|
||||
google_oauth_router = fastapi_users.get_oauth_router(google_oauth_client, SECRET)
|
||||
app.include_router(google_oauth_router, prefix="/google-oauth", tags=["users"])
|
||||
|
||||
|
||||
@fastapi_users.on_after_register()
|
||||
def on_after_register(user: User, request: Request):
|
||||
def on_after_register(user: UserDB, request: Request):
|
||||
print(f"User {user.id} has registered.")
|
||||
|
||||
|
||||
@fastapi_users.on_after_forgot_password()
|
||||
def on_after_forgot_password(user: User, token: str, request: Request):
|
||||
def on_after_forgot_password(user: UserDB, token: str, request: Request):
|
||||
print(f"User {user.id} has forgot their password. Reset token: {token}")
|
||||
|
||||
|
||||
jwt_authentication = JWTAuthentication(secret=SECRET, lifetime_seconds=3600)
|
||||
|
||||
app = FastAPI()
|
||||
fastapi_users = FastAPIUsers(
|
||||
user_db, [jwt_authentication], User, UserCreate, UserUpdate, UserDB,
|
||||
)
|
||||
app.include_router(
|
||||
fastapi_users.get_auth_router(jwt_authentication), prefix="/auth/jwt", tags=["auth"]
|
||||
)
|
||||
app.include_router(
|
||||
fastapi_users.get_register_router(on_after_register), prefix="/auth", tags=["auth"]
|
||||
)
|
||||
app.include_router(
|
||||
fastapi_users.get_reset_password_router(
|
||||
SECRET, after_forgot_password=on_after_forgot_password
|
||||
),
|
||||
prefix="/auth",
|
||||
tags=["auth"],
|
||||
)
|
||||
app.include_router(fastapi_users.get_users_router(), prefix="/users", tags=["users"])
|
||||
|
||||
google_oauth_router = fastapi_users.get_oauth_router(
|
||||
google_oauth_client, SECRET, after_register=on_after_register
|
||||
)
|
||||
app.include_router(google_oauth_router, prefix="/auth/google", tags=["auth"])
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import databases
|
||||
import sqlalchemy
|
||||
from fastapi import FastAPI
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi_users import FastAPIUsers, models
|
||||
from fastapi_users.authentication import JWTAuthentication
|
||||
from fastapi_users.db import (
|
||||
@@ -10,7 +10,6 @@ from fastapi_users.db import (
|
||||
)
|
||||
from httpx_oauth.clients.google import GoogleOAuth2
|
||||
from sqlalchemy.ext.declarative import DeclarativeMeta, declarative_base
|
||||
from starlette.requests import Request
|
||||
|
||||
DATABASE_URL = "sqlite:///./test.db"
|
||||
SECRET = "SECRET"
|
||||
@@ -57,30 +56,41 @@ oauth_accounts = OAuthAccount.__table__
|
||||
user_db = SQLAlchemyUserDatabase(UserDB, database, users, oauth_accounts)
|
||||
|
||||
|
||||
auth_backends = [
|
||||
JWTAuthentication(secret=SECRET, lifetime_seconds=3600),
|
||||
]
|
||||
|
||||
app = FastAPI()
|
||||
fastapi_users = FastAPIUsers(
|
||||
user_db, auth_backends, User, UserCreate, UserUpdate, UserDB, SECRET,
|
||||
)
|
||||
app.include_router(fastapi_users.router, prefix="/users", tags=["users"])
|
||||
|
||||
google_oauth_router = fastapi_users.get_oauth_router(google_oauth_client, SECRET)
|
||||
app.include_router(google_oauth_router, prefix="/google-oauth", tags=["users"])
|
||||
|
||||
|
||||
@fastapi_users.on_after_register()
|
||||
def on_after_register(user: User, request: Request):
|
||||
def on_after_register(user: UserDB, request: Request):
|
||||
print(f"User {user.id} has registered.")
|
||||
|
||||
|
||||
@fastapi_users.on_after_forgot_password()
|
||||
def on_after_forgot_password(user: User, token: str, request: Request):
|
||||
def on_after_forgot_password(user: UserDB, token: str, request: Request):
|
||||
print(f"User {user.id} has forgot their password. Reset token: {token}")
|
||||
|
||||
|
||||
jwt_authentication = JWTAuthentication(secret=SECRET, lifetime_seconds=3600)
|
||||
|
||||
app = FastAPI()
|
||||
fastapi_users = FastAPIUsers(
|
||||
user_db, [jwt_authentication], User, UserCreate, UserUpdate, UserDB,
|
||||
)
|
||||
app.include_router(
|
||||
fastapi_users.get_auth_router(jwt_authentication), prefix="/auth/jwt", tags=["auth"]
|
||||
)
|
||||
app.include_router(
|
||||
fastapi_users.get_register_router(on_after_register), prefix="/auth", tags=["auth"]
|
||||
)
|
||||
app.include_router(
|
||||
fastapi_users.get_reset_password_router(
|
||||
SECRET, after_forgot_password=on_after_forgot_password
|
||||
),
|
||||
prefix="/auth",
|
||||
tags=["auth"],
|
||||
)
|
||||
app.include_router(fastapi_users.get_users_router(), prefix="/users", tags=["users"])
|
||||
|
||||
google_oauth_router = fastapi_users.get_oauth_router(
|
||||
google_oauth_client, SECRET, after_register=on_after_register
|
||||
)
|
||||
app.include_router(google_oauth_router, prefix="/auth/google", tags=["auth"])
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup():
|
||||
await database.connect()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi_users import FastAPIUsers, models
|
||||
from fastapi_users.authentication import JWTAuthentication
|
||||
from fastapi_users.db import (
|
||||
@@ -7,7 +7,6 @@ from fastapi_users.db import (
|
||||
TortoiseUserDatabase,
|
||||
)
|
||||
from httpx_oauth.clients.google import GoogleOAuth2
|
||||
from starlette.requests import Request
|
||||
from tortoise import fields
|
||||
from tortoise.contrib.starlette import register_tortoise
|
||||
|
||||
@@ -46,24 +45,36 @@ user_db = TortoiseUserDatabase(UserDB, UserModel, OAuthAccountModel)
|
||||
app = FastAPI()
|
||||
register_tortoise(app, db_url=DATABASE_URL, modules={"models": ["test"]})
|
||||
|
||||
auth_backends = [
|
||||
JWTAuthentication(secret=SECRET, lifetime_seconds=3600),
|
||||
]
|
||||
|
||||
fastapi_users = FastAPIUsers(
|
||||
user_db, auth_backends, User, UserCreate, UserUpdate, UserDB, SECRET,
|
||||
)
|
||||
app.include_router(fastapi_users.router, prefix="/users", tags=["users"])
|
||||
|
||||
google_oauth_router = fastapi_users.get_oauth_router(google_oauth_client, SECRET)
|
||||
app.include_router(google_oauth_router, prefix="/google-oauth", tags=["users"])
|
||||
|
||||
|
||||
@fastapi_users.on_after_register()
|
||||
def on_after_register(user: User, request: Request):
|
||||
def on_after_register(user: UserDB, request: Request):
|
||||
print(f"User {user.id} has registered.")
|
||||
|
||||
|
||||
@fastapi_users.on_after_forgot_password()
|
||||
def on_after_forgot_password(user: User, token: str, request: Request):
|
||||
def on_after_forgot_password(user: UserDB, token: str, request: Request):
|
||||
print(f"User {user.id} has forgot their password. Reset token: {token}")
|
||||
|
||||
|
||||
jwt_authentication = JWTAuthentication(secret=SECRET, lifetime_seconds=3600)
|
||||
|
||||
fastapi_users = FastAPIUsers(
|
||||
user_db, [jwt_authentication], User, UserCreate, UserUpdate, UserDB,
|
||||
)
|
||||
app.include_router(
|
||||
fastapi_users.get_auth_router(jwt_authentication), prefix="/auth/jwt", tags=["auth"]
|
||||
)
|
||||
app.include_router(
|
||||
fastapi_users.get_register_router(on_after_register), prefix="/auth", tags=["auth"]
|
||||
)
|
||||
app.include_router(
|
||||
fastapi_users.get_reset_password_router(
|
||||
SECRET, after_forgot_password=on_after_forgot_password
|
||||
),
|
||||
prefix="/auth",
|
||||
tags=["auth"],
|
||||
)
|
||||
app.include_router(fastapi_users.get_users_router(), prefix="/users", tags=["users"])
|
||||
|
||||
google_oauth_router = fastapi_users.get_oauth_router(
|
||||
google_oauth_client, SECRET, after_register=on_after_register
|
||||
)
|
||||
app.include_router(google_oauth_router, prefix="/auth/google", tags=["auth"])
|
||||
|
||||
@@ -45,4 +45,4 @@ def protected_route():
|
||||
return 'Hello, some user.'
|
||||
```
|
||||
|
||||
You can see more about it [in FastAPI docs](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/).
|
||||
You can read more about this [in FastAPI docs](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-in-path-operation-decorators/).
|
||||
|
||||
@@ -14,12 +14,12 @@ First step, of course, is to register as a user.
|
||||
-H "Content-Type: application/json" \
|
||||
-X POST \
|
||||
-d "{\"email\": \"king.arthur@camelot.bt\",\"password\": \"guinevere\"}" \
|
||||
http://localhost:8000/users/register
|
||||
http://localhost:8000/auth/register
|
||||
```
|
||||
|
||||
=== "axios"
|
||||
```ts
|
||||
axios.post('http://localhost:8000/users/register', {
|
||||
axios.post('http://localhost:8000/auth/register', {
|
||||
email: 'king.arthur@camelot.bt',
|
||||
password: 'guinevere',
|
||||
})
|
||||
@@ -51,7 +51,7 @@ You'll get a JSON response looking like this:
|
||||
|
||||
Now, you can login as this new user.
|
||||
|
||||
Each [authentication backend](../configuration/authentication/index.md) will produce a single route. For example, the [JWT backend](../configuration/authentication/jwt.md) will produce the `/users/login/jwt` route. Each backend will have a different response.
|
||||
You can generate a [login route](../configuration/routers/auth.md) for each [authentication backend](../configuration/authentication/index.md). Each backend will have a different response.
|
||||
|
||||
### JWT backend
|
||||
|
||||
@@ -64,7 +64,7 @@ Each [authentication backend](../configuration/authentication/index.md) will pro
|
||||
-X POST \
|
||||
-F "username=king.arthur@camelot.bt" \
|
||||
-F "password=guinevere" \
|
||||
http://localhost:8000/users/login/jwt
|
||||
http://localhost:8000/auth/jwt/login
|
||||
```
|
||||
|
||||
=== "axios"
|
||||
@@ -73,7 +73,7 @@ Each [authentication backend](../configuration/authentication/index.md) will pro
|
||||
formData.set('username', 'king.arthur@camelot.bt');
|
||||
formData.set('password', 'guinevere');
|
||||
axios.post(
|
||||
'http://localhost:8000/users/login/jwt',
|
||||
'http://localhost:8000/auth/jwt/login',
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
@@ -112,7 +112,7 @@ You can use this token to make authenticated requests as the user `king.arthur@c
|
||||
-X POST \
|
||||
-F "username=king.arthur@camelot.bt" \
|
||||
-F "password=guinevere" \
|
||||
http://localhost:8000/users/login/cookie
|
||||
http://localhost:8000/auth/cookie/login
|
||||
```
|
||||
|
||||
=== "axios"
|
||||
@@ -121,7 +121,7 @@ You can use this token to make authenticated requests as the user `king.arthur@c
|
||||
formData.set('username', 'king.arthur@camelot.bt');
|
||||
formData.set('password', 'guinevere');
|
||||
axios.post(
|
||||
'http://localhost:8000/users/login/cookie',
|
||||
'http://localhost:8000/auth/cookie/login',
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
@@ -380,12 +380,12 @@ We can also end the session. Note that it doesn't apply to every [authentication
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-X POST \
|
||||
http://localhost:8000/users/logout/cookie
|
||||
http://localhost:8000/auth/cookie/logout
|
||||
```
|
||||
|
||||
=== "axios"
|
||||
```ts
|
||||
axios.post('http://localhost:8000/users/logout/cookie',
|
||||
axios.post('http://localhost:8000/auth/cookie/logout',
|
||||
null,
|
||||
{
|
||||
headers: {
|
||||
|
||||
@@ -2,11 +2,49 @@
|
||||
|
||||
You'll find here the routes exposed by **FastAPI Users**. Note that you can also review them through the [interactive API docs](https://fastapi.tiangolo.com/tutorial/first-steps/#interactive-api-docs).
|
||||
|
||||
## Unauthenticated
|
||||
## Auth router
|
||||
|
||||
Each [authentication backend](../configuration/authentication/index.md) you [generate a router for](../configuration/routers/auth.md) will produce the following routes. Take care about the prefix you gave it, especially if you have several backends.
|
||||
|
||||
### `POST /login`
|
||||
|
||||
Login a user against the method named `name`. Check the corresponding [authentication method](../configuration/authentication/index.md) to view the success response.
|
||||
|
||||
!!! abstract "Payload (`application/x-www-form-urlencoded`)"
|
||||
```
|
||||
username=king.arthur@camelot.bt&password=guinevere
|
||||
```
|
||||
|
||||
!!! fail "`422 Validation Error`"
|
||||
|
||||
!!! fail "`400 Bad Request`"
|
||||
Bad credentials or the user is inactive.
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "LOGIN_BAD_CREDENTIALS"
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /logout`
|
||||
|
||||
Logout the authenticated user against the method named `name`. Check the corresponding [authentication method](../configuration/authentication/index.md) to view the success response.
|
||||
|
||||
!!! fail "`401 Unauthorized`"
|
||||
Missing token or inactive user.
|
||||
|
||||
!!! success "`200 OK`"
|
||||
The logout process was successful.
|
||||
|
||||
|
||||
!!! tip
|
||||
Some backend (like JWT) won't produce this route.
|
||||
|
||||
## Register router
|
||||
|
||||
### `POST /register`
|
||||
|
||||
Register a new user. Will call the `on_after_register` [event handlers](../configuration/router.md#event-handlers) on successful registration.
|
||||
Register a new user. Will call the `after_register` [handler](../configuration/routers/register.md#after-register) on successful registration.
|
||||
|
||||
!!! abstract "Payload"
|
||||
```json
|
||||
@@ -37,42 +75,11 @@ Register a new user. Will call the `on_after_register` [event handlers](../confi
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /login/{name}`
|
||||
|
||||
Login a user against the method named `name`. Check the corresponding [authentication method](../configuration/authentication/index.md) to view the success response.
|
||||
|
||||
!!! abstract "Payload (`application/x-www-form-urlencoded`)"
|
||||
```
|
||||
username=king.arthur@camelot.bt&password=guinevere
|
||||
```
|
||||
|
||||
!!! fail "`422 Validation Error`"
|
||||
|
||||
!!! fail "`400 Bad Request`"
|
||||
Bad credentials or the user is inactive.
|
||||
|
||||
```json
|
||||
{
|
||||
"detail": "LOGIN_BAD_CREDENTIALS"
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /logout/{name}`
|
||||
|
||||
Logout the authenticated user against the method named `name`. Check the corresponding [authentication method](../configuration/authentication/index.md) to view the success response.
|
||||
|
||||
!!! fail "`401 Unauthorized`"
|
||||
Missing token or inactive user.
|
||||
|
||||
!!! success "`200 OK`"
|
||||
The logout process was successful.
|
||||
|
||||
!!! success "`202 Accepted`"
|
||||
The logout process is not applicable for this authentication backend (e.g. JWT).
|
||||
## Reset password router
|
||||
|
||||
### `POST /forgot-password`
|
||||
|
||||
Request a reset password procedure. Will generate a temporary token and call the `on_after_forgot_password` [event handlers](../configuration/router.md#event-handlers) if the user exists.
|
||||
Request a reset password procedure. Will generate a temporary token and call the `after_forgot_password` [handlers](../configuration/routers/reset.md#after-forgot-password) if the user exists.
|
||||
|
||||
To prevent malicious users from guessing existing users in your databse, the route will always return a `202 Accepted` response, even if the user requested does not exist.
|
||||
|
||||
@@ -110,11 +117,11 @@ Reset a password. Requires the token generated by the `/forgot-password` route.
|
||||
}
|
||||
```
|
||||
|
||||
### OAuth routes
|
||||
## OAuth router
|
||||
|
||||
Each OAuth router you define will expose the two following routes.
|
||||
|
||||
#### `GET /authorize`
|
||||
### `GET /authorize`
|
||||
|
||||
Return the authorization URL for the OAuth service where you should redirect your user.
|
||||
|
||||
@@ -134,7 +141,7 @@ Return the authorization URL for the OAuth service where you should redirect you
|
||||
!!! fail "`400 Bad Request`"
|
||||
Unknown authentication backend.
|
||||
|
||||
#### `GET /callback`
|
||||
### `GET /callback`
|
||||
|
||||
Handle the OAuth callback.
|
||||
|
||||
@@ -155,7 +162,7 @@ Depending on the situation, several things can happen:
|
||||
* A new user is created and linked to the OAuth account.
|
||||
* The user is authenticated following the chosen [authentication method](../configuration/authentication/index.md).
|
||||
|
||||
## Authenticated
|
||||
## Users router
|
||||
|
||||
### `GET /me`
|
||||
|
||||
@@ -199,8 +206,6 @@ Update the current authenticated active user.
|
||||
!!! fail "`401 Unauthorized`"
|
||||
Missing token or inactive user.
|
||||
|
||||
## Superuser
|
||||
|
||||
### `GET /{user_id}`
|
||||
|
||||
Return the user with id `user_id`.
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import re
|
||||
from inspect import Parameter, Signature
|
||||
from typing import Sequence
|
||||
|
||||
from fastapi import HTTPException
|
||||
from starlette import status
|
||||
from starlette.requests import Request
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from makefun import with_signature
|
||||
|
||||
from fastapi_users.authentication.base import BaseAuthentication # noqa: F401
|
||||
from fastapi_users.authentication.cookie import CookieAuthentication # noqa: F401
|
||||
@@ -10,6 +11,20 @@ from fastapi_users.authentication.jwt import JWTAuthentication # noqa: F401
|
||||
from fastapi_users.db import BaseUserDatabase
|
||||
from fastapi_users.models import BaseUserDB
|
||||
|
||||
INVALID_CHARS_PATTERN = re.compile(r"[^0-9a-zA-Z_]")
|
||||
INVALID_LEADING_CHARS_PATTERN = re.compile(r"^[^a-zA-Z_]+")
|
||||
|
||||
|
||||
def name_to_variable_name(name: str) -> str:
|
||||
"""Transform a backend name string into a string safe to use as variable name."""
|
||||
name = re.sub(INVALID_CHARS_PATTERN, "", name)
|
||||
name = re.sub(INVALID_LEADING_CHARS_PATTERN, "", name)
|
||||
return name
|
||||
|
||||
|
||||
class DuplicateBackendNamesError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Authenticator:
|
||||
"""
|
||||
@@ -32,26 +47,52 @@ class Authenticator:
|
||||
self.backends = backends
|
||||
self.user_db = user_db
|
||||
|
||||
async def get_current_user(self, request: Request) -> BaseUserDB:
|
||||
return await self._authenticate(request)
|
||||
# Here comes some blood magic 🧙♂️
|
||||
# Thank to "makefun", we are able to generate callable
|
||||
# with a dynamic number of dependencies at runtime.
|
||||
# This way, each security schemes are detected by the OpenAPI generator.
|
||||
try:
|
||||
parameters = [
|
||||
Parameter(
|
||||
name=name_to_variable_name(backend.name),
|
||||
kind=Parameter.POSITIONAL_OR_KEYWORD,
|
||||
default=Depends(backend.scheme), # type: ignore
|
||||
)
|
||||
for backend in self.backends
|
||||
]
|
||||
signature = Signature(parameters)
|
||||
except ValueError:
|
||||
raise DuplicateBackendNamesError()
|
||||
|
||||
async def get_current_active_user(self, request: Request) -> BaseUserDB:
|
||||
user = await self.get_current_user(request)
|
||||
if not user.is_active:
|
||||
raise self._get_credentials_exception()
|
||||
return user
|
||||
@with_signature(signature, func_name="get_current_user")
|
||||
async def get_current_user(*args, **kwargs):
|
||||
return await self._authenticate(*args, **kwargs)
|
||||
|
||||
async def get_current_superuser(self, request: Request) -> BaseUserDB:
|
||||
user = await self.get_current_active_user(request)
|
||||
if not user.is_superuser:
|
||||
raise self._get_credentials_exception(status.HTTP_403_FORBIDDEN)
|
||||
return user
|
||||
@with_signature(signature, func_name="get_current_active_user")
|
||||
async def get_current_active_user(*args, **kwargs):
|
||||
user = await get_current_user(*args, **kwargs)
|
||||
if not user.is_active:
|
||||
raise self._get_credentials_exception()
|
||||
return user
|
||||
|
||||
async def _authenticate(self, request: Request) -> BaseUserDB:
|
||||
@with_signature(signature, func_name="get_current_superuser")
|
||||
async def get_current_superuser(*args, **kwargs):
|
||||
user = await get_current_active_user(*args, **kwargs)
|
||||
if not user.is_superuser:
|
||||
raise self._get_credentials_exception(status.HTTP_403_FORBIDDEN)
|
||||
return user
|
||||
|
||||
self.get_current_user = get_current_user
|
||||
self.get_current_active_user = get_current_active_user
|
||||
self.get_current_superuser = get_current_superuser
|
||||
|
||||
async def _authenticate(self, *args, **kwargs) -> BaseUserDB:
|
||||
for backend in self.backends:
|
||||
user = await backend(request, self.user_db)
|
||||
if user is not None:
|
||||
return user
|
||||
token: str = kwargs[name_to_variable_name(backend.name)]
|
||||
if token:
|
||||
user = await backend(token, self.user_db)
|
||||
if user is not None:
|
||||
return user
|
||||
raise self._get_credentials_exception()
|
||||
|
||||
def _get_credentials_exception(
|
||||
|
||||
@@ -1,28 +1,34 @@
|
||||
from typing import Any, Optional
|
||||
from typing import Any, Generic, Optional, TypeVar
|
||||
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
from fastapi import Response
|
||||
from fastapi.security.base import SecurityBase
|
||||
|
||||
from fastapi_users.db import BaseUserDatabase
|
||||
from fastapi_users.models import BaseUserDB
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class BaseAuthentication:
|
||||
|
||||
class BaseAuthentication(Generic[T]):
|
||||
"""
|
||||
Base authentication backend.
|
||||
|
||||
Every backend should derive from this class.
|
||||
|
||||
:param name: Name of the backend. It will be used to name the login route.
|
||||
:param name: Name of the backend.
|
||||
:param logout: Whether or not this backend has a logout process.
|
||||
"""
|
||||
|
||||
scheme: SecurityBase
|
||||
name: str
|
||||
logout: bool
|
||||
|
||||
def __init__(self, name: str = "base"):
|
||||
def __init__(self, name: str = "base", logout: bool = False):
|
||||
self.name = name
|
||||
self.logout = logout
|
||||
|
||||
async def __call__(
|
||||
self, request: Request, user_db: BaseUserDatabase
|
||||
self, credentials: Optional[T], user_db: BaseUserDatabase
|
||||
) -> Optional[BaseUserDB]:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
from typing import Any, Optional
|
||||
|
||||
import jwt
|
||||
from fastapi import Response
|
||||
from fastapi.security import APIKeyCookie
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
from pydantic import UUID4
|
||||
|
||||
from fastapi_users.authentication.jwt import JWTAuthentication
|
||||
from fastapi_users.authentication import BaseAuthentication
|
||||
from fastapi_users.db.base import BaseUserDatabase
|
||||
from fastapi_users.models import BaseUserDB
|
||||
from fastapi_users.utils import JWT_ALGORITHM, generate_jwt
|
||||
|
||||
|
||||
class CookieAuthentication(JWTAuthentication):
|
||||
class CookieAuthentication(BaseAuthentication[str]):
|
||||
"""
|
||||
Authentication backend using a cookie.
|
||||
|
||||
@@ -24,6 +27,9 @@ class CookieAuthentication(JWTAuthentication):
|
||||
:param name: Name of the backend. It will be used to name the login route.
|
||||
"""
|
||||
|
||||
scheme: APIKeyCookie
|
||||
token_audience: str = "fastapi-users:auth"
|
||||
secret: str
|
||||
lifetime_seconds: int
|
||||
cookie_name: str
|
||||
cookie_path: str
|
||||
@@ -42,14 +48,40 @@ class CookieAuthentication(JWTAuthentication):
|
||||
cookie_httponly: bool = True,
|
||||
name: str = "cookie",
|
||||
):
|
||||
super().__init__(secret, lifetime_seconds, name=name)
|
||||
super().__init__(name, logout=True)
|
||||
self.secret = secret
|
||||
self.lifetime_seconds = lifetime_seconds
|
||||
self.cookie_name = cookie_name
|
||||
self.cookie_path = cookie_path
|
||||
self.cookie_domain = cookie_domain
|
||||
self.cookie_secure = cookie_secure
|
||||
self.cookie_httponly = cookie_httponly
|
||||
self.api_key_cookie = APIKeyCookie(name=self.cookie_name, auto_error=False)
|
||||
self.scheme = APIKeyCookie(name=self.cookie_name, auto_error=False)
|
||||
|
||||
async def __call__(
|
||||
self, credentials: Optional[str], user_db: BaseUserDatabase,
|
||||
) -> Optional[BaseUserDB]:
|
||||
if credentials is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
data = jwt.decode(
|
||||
credentials,
|
||||
self.secret,
|
||||
audience=self.token_audience,
|
||||
algorithms=[JWT_ALGORITHM],
|
||||
)
|
||||
user_id = data.get("user_id")
|
||||
if user_id is None:
|
||||
return None
|
||||
except jwt.PyJWTError:
|
||||
return None
|
||||
|
||||
try:
|
||||
user_uiid = UUID4(user_id)
|
||||
return await user_db.get(user_uiid)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
async def get_login_response(self, user: BaseUserDB, response: Response) -> Any:
|
||||
token = await self._generate_token(user)
|
||||
@@ -72,5 +104,6 @@ class CookieAuthentication(JWTAuthentication):
|
||||
self.cookie_name, path=self.cookie_path, domain=self.cookie_domain
|
||||
)
|
||||
|
||||
async def _retrieve_token(self, request: Request) -> Optional[str]:
|
||||
return await self.api_key_cookie.__call__(request)
|
||||
async def _generate_token(self, user: BaseUserDB) -> str:
|
||||
data = {"user_id": str(user.id), "aud": self.token_audience}
|
||||
return generate_jwt(data, self.lifetime_seconds, self.secret, JWT_ALGORITHM)
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
from typing import Any, Optional
|
||||
|
||||
import jwt
|
||||
from fastapi import Response
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from pydantic import UUID4
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
|
||||
from fastapi_users.authentication.base import BaseAuthentication
|
||||
from fastapi_users.db.base import BaseUserDatabase
|
||||
@@ -12,7 +11,7 @@ from fastapi_users.models import BaseUserDB
|
||||
from fastapi_users.utils import JWT_ALGORITHM, generate_jwt
|
||||
|
||||
|
||||
class JWTAuthentication(BaseAuthentication):
|
||||
class JWTAuthentication(BaseAuthentication[str]):
|
||||
"""
|
||||
Authentication backend using a JWT in a Bearer header.
|
||||
|
||||
@@ -22,6 +21,7 @@ class JWTAuthentication(BaseAuthentication):
|
||||
:param name: Name of the backend. It will be used to name the login route.
|
||||
"""
|
||||
|
||||
scheme: OAuth2PasswordBearer
|
||||
token_audience: str = "fastapi-users:auth"
|
||||
secret: str
|
||||
lifetime_seconds: int
|
||||
@@ -33,21 +33,20 @@ class JWTAuthentication(BaseAuthentication):
|
||||
tokenUrl: str = "/users/login",
|
||||
name: str = "jwt",
|
||||
):
|
||||
super().__init__(name)
|
||||
super().__init__(name, logout=False)
|
||||
self.scheme = OAuth2PasswordBearer(tokenUrl, auto_error=False)
|
||||
self.secret = secret
|
||||
self.lifetime_seconds = lifetime_seconds
|
||||
self.scheme = OAuth2PasswordBearer(tokenUrl, auto_error=False)
|
||||
|
||||
async def __call__(
|
||||
self, request: Request, user_db: BaseUserDatabase,
|
||||
self, credentials: Optional[str], user_db: BaseUserDatabase,
|
||||
) -> Optional[BaseUserDB]:
|
||||
token = await self._retrieve_token(request)
|
||||
if token is None:
|
||||
if credentials is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
data = jwt.decode(
|
||||
token,
|
||||
credentials,
|
||||
self.secret,
|
||||
audience=self.token_audience,
|
||||
algorithms=[JWT_ALGORITHM],
|
||||
@@ -68,9 +67,6 @@ class JWTAuthentication(BaseAuthentication):
|
||||
token = await self._generate_token(user)
|
||||
return {"token": token}
|
||||
|
||||
async def _retrieve_token(self, request: Request) -> Optional[str]:
|
||||
return await self.scheme.__call__(request)
|
||||
|
||||
async def _generate_token(self, user: BaseUserDB) -> str:
|
||||
data = {"user_id": str(user.id), "aud": self.token_audience}
|
||||
return generate_jwt(data, self.lifetime_seconds, self.secret, JWT_ALGORITHM)
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
from collections import defaultdict
|
||||
from typing import Callable, DefaultDict, List, Sequence, Type
|
||||
from typing import Any, Callable, Dict, Optional, Sequence, Type
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from httpx_oauth.oauth2 import BaseOAuth2
|
||||
|
||||
from fastapi_users import models
|
||||
from fastapi_users.authentication import Authenticator, BaseAuthentication
|
||||
from fastapi_users.db import BaseUserDatabase
|
||||
from fastapi_users.router import (
|
||||
Event,
|
||||
EventHandlersRouter,
|
||||
get_auth_router,
|
||||
get_oauth_router,
|
||||
get_user_router,
|
||||
get_register_router,
|
||||
get_reset_password_router,
|
||||
get_users_router,
|
||||
)
|
||||
|
||||
|
||||
@@ -24,20 +25,18 @@ class FastAPIUsers:
|
||||
: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.
|
||||
:param reset_password_token_secret: Secret to encode reset password token.
|
||||
:param reset_password_token_lifetime_seconds: Lifetime of reset password token.
|
||||
|
||||
:attribute router: Router exposing authentication routes.
|
||||
:attribute oauth_routers: List of OAuth routers created through `get_oauth_router`.
|
||||
:attribute get_current_user: Dependency callable to inject authenticated user.
|
||||
:attribute get_current_active_user: Dependency callable to inject active user.
|
||||
:attribute get_current_superuser: Dependency callable to inject superuser.
|
||||
"""
|
||||
|
||||
db: BaseUserDatabase
|
||||
authenticator: Authenticator
|
||||
router: EventHandlersRouter
|
||||
oauth_routers: List[EventHandlersRouter]
|
||||
_user_model: Type[models.BaseUser]
|
||||
_user_create_model: Type[models.BaseUserCreate]
|
||||
_user_update_model: Type[models.BaseUserUpdate]
|
||||
_user_db_model: Type[models.BaseUserDB]
|
||||
_event_handlers: DefaultDict[Event, List[Callable]]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -47,44 +46,78 @@ class FastAPIUsers:
|
||||
user_create_model: Type[models.BaseUserCreate],
|
||||
user_update_model: Type[models.BaseUserUpdate],
|
||||
user_db_model: Type[models.BaseUserDB],
|
||||
reset_password_token_secret: str,
|
||||
reset_password_token_lifetime_seconds: int = 3600,
|
||||
):
|
||||
self.db = db
|
||||
self.authenticator = Authenticator(auth_backends, db)
|
||||
self.router = get_user_router(
|
||||
self.db,
|
||||
user_model,
|
||||
user_create_model,
|
||||
user_update_model,
|
||||
user_db_model,
|
||||
self.authenticator,
|
||||
reset_password_token_secret,
|
||||
reset_password_token_lifetime_seconds,
|
||||
self.router = get_users_router(
|
||||
self.db, user_model, user_update_model, user_db_model, self.authenticator,
|
||||
)
|
||||
self.oauth_routers = []
|
||||
|
||||
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._user_db_model = user_db_model
|
||||
self._event_handlers = defaultdict(list)
|
||||
|
||||
self.get_current_user = self.authenticator.get_current_user
|
||||
self.get_current_active_user = self.authenticator.get_current_active_user
|
||||
self.get_current_superuser = self.authenticator.get_current_superuser
|
||||
|
||||
def on_after_register(self) -> Callable:
|
||||
"""Add an event handler on successful registration."""
|
||||
return self._on_event(Event.ON_AFTER_REGISTER)
|
||||
def get_register_router(
|
||||
self, after_register: Optional[Callable[[models.UD, Request], None]] = None,
|
||||
) -> APIRouter:
|
||||
"""
|
||||
Return a router with a register route.
|
||||
|
||||
def on_after_forgot_password(self) -> Callable:
|
||||
"""Add an event handler on successful forgot password request."""
|
||||
return self._on_event(Event.ON_AFTER_FORGOT_PASSWORD)
|
||||
:param after_register: Optional function called
|
||||
after a successful registration.
|
||||
"""
|
||||
return get_register_router(
|
||||
self.db,
|
||||
self._user_model,
|
||||
self._user_create_model,
|
||||
self._user_db_model,
|
||||
after_register,
|
||||
)
|
||||
|
||||
def on_after_update(self) -> Callable:
|
||||
"""Add an event handler on successful update user request."""
|
||||
return self._on_event(Event.ON_AFTER_UPDATE)
|
||||
def get_reset_password_router(
|
||||
self,
|
||||
reset_password_token_secret: str,
|
||||
reset_password_token_lifetime_seconds: int = 3600,
|
||||
after_forgot_password: Optional[
|
||||
Callable[[models.UD, str, Request], None]
|
||||
] = None,
|
||||
) -> APIRouter:
|
||||
"""
|
||||
Return a reset password process router.
|
||||
|
||||
:param reset_password_token_secret: Secret to encode reset password token.
|
||||
:param reset_password_token_lifetime_seconds: Lifetime of reset password token.
|
||||
:param after_forgot_password: Optional function called after a successful
|
||||
forgot password request.
|
||||
"""
|
||||
return get_reset_password_router(
|
||||
self.db,
|
||||
reset_password_token_secret,
|
||||
reset_password_token_lifetime_seconds,
|
||||
after_forgot_password,
|
||||
)
|
||||
|
||||
def get_auth_router(self, backend: BaseAuthentication) -> APIRouter:
|
||||
"""
|
||||
Return an auth router for a given authentication backend.
|
||||
|
||||
:param backend: The authentication backend instance.
|
||||
"""
|
||||
return get_auth_router(backend, self.db, self.authenticator)
|
||||
|
||||
def get_oauth_router(
|
||||
self, oauth_client: BaseOAuth2, state_secret: str, redirect_url: str = None
|
||||
) -> EventHandlersRouter:
|
||||
self,
|
||||
oauth_client: BaseOAuth2,
|
||||
state_secret: str,
|
||||
redirect_url: str = None,
|
||||
after_register: Optional[Callable[[models.UD, Request], None]] = None,
|
||||
) -> APIRouter:
|
||||
"""
|
||||
Return an OAuth router for a given OAuth client.
|
||||
|
||||
@@ -92,30 +125,36 @@ class FastAPIUsers:
|
||||
:param state_secret: Secret used to encode the state JWT.
|
||||
:param redirect_url: Optional arbitrary redirect URL for the OAuth2 flow.
|
||||
If not given, the URL to the callback endpoint will be generated.
|
||||
:param after_register: Optional function called
|
||||
after a successful registration.
|
||||
"""
|
||||
oauth_router = get_oauth_router(
|
||||
return get_oauth_router(
|
||||
oauth_client,
|
||||
self.db,
|
||||
self._user_db_model,
|
||||
self.authenticator,
|
||||
state_secret,
|
||||
redirect_url,
|
||||
after_register,
|
||||
)
|
||||
|
||||
for event_type in self._event_handlers:
|
||||
for handler in self._event_handlers[event_type]:
|
||||
oauth_router.add_event_handler(event_type, handler)
|
||||
def get_users_router(
|
||||
self,
|
||||
after_update: Optional[
|
||||
Callable[[models.UD, Dict[str, Any], Request], None]
|
||||
] = None,
|
||||
) -> APIRouter:
|
||||
"""
|
||||
Return a router with routes to manage users.
|
||||
|
||||
self.oauth_routers.append(oauth_router)
|
||||
|
||||
return oauth_router
|
||||
|
||||
def _on_event(self, event_type: Event) -> Callable:
|
||||
def decorator(func: Callable) -> Callable:
|
||||
self._event_handlers[event_type].append(func)
|
||||
self.router.add_event_handler(event_type, func)
|
||||
for oauth_router in self.oauth_routers:
|
||||
oauth_router.add_event_handler(event_type, func)
|
||||
return func
|
||||
|
||||
return decorator
|
||||
:param after_update: Optional function called
|
||||
after a successful user update.
|
||||
"""
|
||||
return get_users_router(
|
||||
self.db,
|
||||
self._user_model,
|
||||
self._user_update_model,
|
||||
self._user_db_model,
|
||||
self.authenticator,
|
||||
after_update,
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from fastapi_users.router.common import ( # noqa: F401
|
||||
ErrorCode,
|
||||
Event,
|
||||
EventHandlersRouter,
|
||||
)
|
||||
from fastapi_users.router.auth import get_auth_router # noqa: F401
|
||||
from fastapi_users.router.common import ErrorCode # noqa: F401
|
||||
from fastapi_users.router.oauth import get_oauth_router # noqa: F401
|
||||
from fastapi_users.router.users import get_user_router # noqa: F401
|
||||
from fastapi_users.router.register import get_register_router # noqa: F401
|
||||
from fastapi_users.router.reset import get_reset_password_router # noqa: F401
|
||||
from fastapi_users.router.users import get_users_router # noqa: F401
|
||||
|
||||
40
fastapi_users/router/auth.py
Normal file
40
fastapi_users/router/auth.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
|
||||
from fastapi_users import models
|
||||
from fastapi_users.authentication import Authenticator, BaseAuthentication
|
||||
from fastapi_users.db import BaseUserDatabase
|
||||
from fastapi_users.router.common import ErrorCode
|
||||
|
||||
|
||||
def get_auth_router(
|
||||
backend: BaseAuthentication,
|
||||
user_db: BaseUserDatabase[models.BaseUserDB],
|
||||
authenticator: Authenticator,
|
||||
) -> APIRouter:
|
||||
"""Generate a router with login/logout routes for an authentication backend."""
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/login")
|
||||
async def login(
|
||||
response: Response, credentials: OAuth2PasswordRequestForm = Depends()
|
||||
):
|
||||
user = await user_db.authenticate(credentials)
|
||||
|
||||
if user is None or not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ErrorCode.LOGIN_BAD_CREDENTIALS,
|
||||
)
|
||||
|
||||
return await backend.get_login_response(user, response)
|
||||
|
||||
if backend.logout:
|
||||
|
||||
@router.post("/logout")
|
||||
async def logout(
|
||||
response: Response, user=Depends(authenticator.get_current_active_user)
|
||||
):
|
||||
return await backend.get_logout_response(user, response)
|
||||
|
||||
return router
|
||||
@@ -1,9 +1,5 @@
|
||||
import asyncio
|
||||
from collections import defaultdict
|
||||
from enum import Enum, auto
|
||||
from typing import Callable, DefaultDict, List
|
||||
|
||||
from fastapi import APIRouter
|
||||
from typing import Callable
|
||||
|
||||
|
||||
class ErrorCode:
|
||||
@@ -12,25 +8,8 @@ class ErrorCode:
|
||||
RESET_PASSWORD_BAD_TOKEN = "RESET_PASSWORD_BAD_TOKEN"
|
||||
|
||||
|
||||
class Event(Enum):
|
||||
ON_AFTER_REGISTER = auto()
|
||||
ON_AFTER_FORGOT_PASSWORD = auto()
|
||||
ON_AFTER_UPDATE = auto()
|
||||
|
||||
|
||||
class EventHandlersRouter(APIRouter):
|
||||
event_handlers: DefaultDict[Event, List[Callable]]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.event_handlers = defaultdict(list)
|
||||
|
||||
def add_event_handler(self, event_type: Event, func: Callable) -> None:
|
||||
self.event_handlers[event_type].append(func)
|
||||
|
||||
async def run_handlers(self, event_type: Event, *args, **kwargs) -> None:
|
||||
for handler in self.event_handlers[event_type]:
|
||||
if asyncio.iscoroutinefunction(handler):
|
||||
await handler(*args, **kwargs)
|
||||
else:
|
||||
handler(*args, **kwargs)
|
||||
async def run_handler(handler: Callable, *args, **kwargs):
|
||||
if asyncio.iscoroutinefunction(handler):
|
||||
await handler(*args, **kwargs)
|
||||
else:
|
||||
handler(*args, **kwargs)
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
from typing import Dict, List, Type, cast
|
||||
from typing import Callable, Dict, List, Optional, Type, cast
|
||||
|
||||
import jwt
|
||||
from fastapi import Depends, HTTPException, Query
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response, status
|
||||
from httpx_oauth.integrations.fastapi import OAuth2AuthorizeCallback
|
||||
from httpx_oauth.oauth2 import BaseOAuth2
|
||||
from starlette import status
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
|
||||
from fastapi_users import models
|
||||
from fastapi_users.authentication import Authenticator
|
||||
from fastapi_users.db import BaseUserDatabase
|
||||
from fastapi_users.password import generate_password, get_password_hash
|
||||
from fastapi_users.router.common import ErrorCode, Event, EventHandlersRouter
|
||||
from fastapi_users.router.common import ErrorCode, run_handler
|
||||
from fastapi_users.utils import JWT_ALGORITHM, generate_jwt
|
||||
|
||||
STATE_TOKEN_AUDIENCE = "fastapi-users:oauth-state"
|
||||
@@ -38,9 +35,10 @@ def get_oauth_router(
|
||||
authenticator: Authenticator,
|
||||
state_secret: str,
|
||||
redirect_url: str = None,
|
||||
) -> EventHandlersRouter:
|
||||
after_register: Optional[Callable[[models.UD, Request], None]] = None,
|
||||
) -> APIRouter:
|
||||
"""Generate a router with the OAuth routes."""
|
||||
router = EventHandlersRouter()
|
||||
router = APIRouter()
|
||||
callback_route_name = f"{oauth_client.name}-callback"
|
||||
|
||||
if redirect_url is not None:
|
||||
@@ -122,7 +120,8 @@ def get_oauth_router(
|
||||
oauth_accounts=[new_oauth_account],
|
||||
)
|
||||
await user_db.create(user)
|
||||
await router.run_handlers(Event.ON_AFTER_REGISTER, user, request)
|
||||
if after_register:
|
||||
await run_handler(after_register, user, request)
|
||||
else:
|
||||
# Update oauth
|
||||
updated_oauth_accounts = []
|
||||
|
||||
45
fastapi_users/router/register.py
Normal file
45
fastapi_users/router/register.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from typing import Callable, Optional, Type, cast
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Request, status
|
||||
|
||||
from fastapi_users import models
|
||||
from fastapi_users.db import BaseUserDatabase
|
||||
from fastapi_users.password import get_password_hash
|
||||
from fastapi_users.router.common import ErrorCode, run_handler
|
||||
|
||||
|
||||
def get_register_router(
|
||||
user_db: BaseUserDatabase[models.BaseUserDB],
|
||||
user_model: Type[models.BaseUser],
|
||||
user_create_model: Type[models.BaseUserCreate],
|
||||
user_db_model: Type[models.BaseUserDB],
|
||||
after_register: Optional[Callable[[models.UD, Request], None]] = None,
|
||||
) -> APIRouter:
|
||||
"""Generate a router with the register route."""
|
||||
router = APIRouter()
|
||||
|
||||
@router.post(
|
||||
"/register", response_model=user_model, status_code=status.HTTP_201_CREATED
|
||||
)
|
||||
async def register(request: Request, user: user_create_model): # type: ignore
|
||||
user = cast(models.BaseUserCreate, user) # Prevent mypy complain
|
||||
existing_user = await user_db.get_by_email(user.email)
|
||||
|
||||
if existing_user is not None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ErrorCode.REGISTER_USER_ALREADY_EXISTS,
|
||||
)
|
||||
|
||||
hashed_password = get_password_hash(user.password)
|
||||
db_user = user_db_model(
|
||||
**user.create_update_dict(), hashed_password=hashed_password
|
||||
)
|
||||
created_user = await user_db.create(db_user)
|
||||
|
||||
if after_register:
|
||||
await run_handler(after_register, created_user, request)
|
||||
|
||||
return created_user
|
||||
|
||||
return router
|
||||
82
fastapi_users/router/reset.py
Normal file
82
fastapi_users/router/reset.py
Normal file
@@ -0,0 +1,82 @@
|
||||
from typing import Callable, Optional
|
||||
|
||||
import jwt
|
||||
from fastapi import APIRouter, Body, HTTPException, Request, status
|
||||
from pydantic import UUID4, EmailStr
|
||||
|
||||
from fastapi_users import models
|
||||
from fastapi_users.db import BaseUserDatabase
|
||||
from fastapi_users.password import get_password_hash
|
||||
from fastapi_users.router.common import ErrorCode, run_handler
|
||||
from fastapi_users.utils import JWT_ALGORITHM, generate_jwt
|
||||
|
||||
RESET_PASSWORD_TOKEN_AUDIENCE = "fastapi-users:reset"
|
||||
|
||||
|
||||
def get_reset_password_router(
|
||||
user_db: BaseUserDatabase[models.BaseUserDB],
|
||||
reset_password_token_secret: str,
|
||||
reset_password_token_lifetime_seconds: int = 3600,
|
||||
after_forgot_password: Optional[Callable[[models.UD, str, Request], None]] = None,
|
||||
) -> APIRouter:
|
||||
"""Generate a router with the reset password routes."""
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/forgot-password", status_code=status.HTTP_202_ACCEPTED)
|
||||
async def forgot_password(
|
||||
request: Request, email: EmailStr = Body(..., embed=True)
|
||||
):
|
||||
user = await user_db.get_by_email(email)
|
||||
|
||||
if user is not None and user.is_active:
|
||||
token_data = {"user_id": str(user.id), "aud": RESET_PASSWORD_TOKEN_AUDIENCE}
|
||||
token = generate_jwt(
|
||||
token_data,
|
||||
reset_password_token_lifetime_seconds,
|
||||
reset_password_token_secret,
|
||||
)
|
||||
if after_forgot_password:
|
||||
await run_handler(after_forgot_password, user, token, request)
|
||||
|
||||
return None
|
||||
|
||||
@router.post("/reset-password")
|
||||
async def reset_password(token: str = Body(...), password: str = Body(...)):
|
||||
try:
|
||||
data = jwt.decode(
|
||||
token,
|
||||
reset_password_token_secret,
|
||||
audience=RESET_PASSWORD_TOKEN_AUDIENCE,
|
||||
algorithms=[JWT_ALGORITHM],
|
||||
)
|
||||
user_id = data.get("user_id")
|
||||
if user_id is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ErrorCode.RESET_PASSWORD_BAD_TOKEN,
|
||||
)
|
||||
|
||||
try:
|
||||
user_uiid = UUID4(user_id)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ErrorCode.RESET_PASSWORD_BAD_TOKEN,
|
||||
)
|
||||
|
||||
user = await user_db.get(user_uiid)
|
||||
if user is None or not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ErrorCode.RESET_PASSWORD_BAD_TOKEN,
|
||||
)
|
||||
|
||||
user.hashed_password = get_password_hash(password)
|
||||
await user_db.update(user)
|
||||
except jwt.PyJWTError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ErrorCode.RESET_PASSWORD_BAD_TOKEN,
|
||||
)
|
||||
|
||||
return router
|
||||
@@ -1,70 +1,25 @@
|
||||
from typing import Any, Dict, Type, cast
|
||||
from typing import Any, Callable, Dict, Optional, Type, cast
|
||||
|
||||
import jwt
|
||||
from fastapi import Body, Depends, HTTPException
|
||||
from fastapi.security import OAuth2PasswordRequestForm
|
||||
from pydantic import UUID4, EmailStr
|
||||
from starlette import status
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||||
from pydantic import UUID4
|
||||
|
||||
from fastapi_users import models
|
||||
from fastapi_users.authentication import Authenticator, BaseAuthentication
|
||||
from fastapi_users.authentication import Authenticator
|
||||
from fastapi_users.db import BaseUserDatabase
|
||||
from fastapi_users.password import get_password_hash
|
||||
from fastapi_users.router.common import ErrorCode, Event, EventHandlersRouter
|
||||
from fastapi_users.utils import JWT_ALGORITHM, generate_jwt
|
||||
from fastapi_users.router.common import run_handler
|
||||
|
||||
|
||||
def _add_login_route(
|
||||
router: EventHandlersRouter,
|
||||
user_db: BaseUserDatabase,
|
||||
auth_backend: BaseAuthentication,
|
||||
):
|
||||
@router.post(f"/login/{auth_backend.name}")
|
||||
async def login(
|
||||
response: Response, credentials: OAuth2PasswordRequestForm = Depends()
|
||||
):
|
||||
user = await user_db.authenticate(credentials)
|
||||
|
||||
if user is None or not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ErrorCode.LOGIN_BAD_CREDENTIALS,
|
||||
)
|
||||
|
||||
return await auth_backend.get_login_response(user, response)
|
||||
|
||||
|
||||
def _add_logout_route(
|
||||
router: EventHandlersRouter,
|
||||
authenticator: Authenticator,
|
||||
auth_backend: BaseAuthentication,
|
||||
):
|
||||
@router.post(f"/logout/{auth_backend.name}")
|
||||
async def logout(
|
||||
response: Response, user=Depends(authenticator.get_current_active_user)
|
||||
):
|
||||
try:
|
||||
return await auth_backend.get_logout_response(user, response)
|
||||
except NotImplementedError:
|
||||
response.status_code = status.HTTP_202_ACCEPTED
|
||||
|
||||
|
||||
def get_user_router(
|
||||
def get_users_router(
|
||||
user_db: BaseUserDatabase[models.BaseUserDB],
|
||||
user_model: Type[models.BaseUser],
|
||||
user_create_model: Type[models.BaseUserCreate],
|
||||
user_update_model: Type[models.BaseUserUpdate],
|
||||
user_db_model: Type[models.BaseUserDB],
|
||||
authenticator: Authenticator,
|
||||
reset_password_token_secret: str,
|
||||
reset_password_token_lifetime_seconds: int = 3600,
|
||||
) -> EventHandlersRouter:
|
||||
after_update: Optional[Callable[[models.UD, Dict[str, Any], Request], None]] = None,
|
||||
) -> APIRouter:
|
||||
"""Generate a router with the authentication routes."""
|
||||
router = EventHandlersRouter()
|
||||
|
||||
reset_password_token_audience = "fastapi-users:reset"
|
||||
router = APIRouter()
|
||||
|
||||
get_current_active_user = authenticator.get_current_active_user
|
||||
get_current_superuser = authenticator.get_current_superuser
|
||||
@@ -75,99 +30,19 @@ def get_user_router(
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
||||
return user
|
||||
|
||||
async def _update_user(user: models.BaseUserDB, update_dict: Dict[str, Any]):
|
||||
async def _update_user(
|
||||
user: models.BaseUserDB, update_dict: Dict[str, Any], request: Request
|
||||
):
|
||||
for field in update_dict:
|
||||
if field == "password":
|
||||
hashed_password = get_password_hash(update_dict[field])
|
||||
user.hashed_password = hashed_password
|
||||
else:
|
||||
setattr(user, field, update_dict[field])
|
||||
return await user_db.update(user)
|
||||
|
||||
for auth_backend in authenticator.backends:
|
||||
_add_login_route(router, user_db, auth_backend)
|
||||
_add_logout_route(router, authenticator, auth_backend)
|
||||
|
||||
@router.post(
|
||||
"/register", response_model=user_model, status_code=status.HTTP_201_CREATED
|
||||
)
|
||||
async def register(request: Request, user: user_create_model): # type: ignore
|
||||
user = cast(models.BaseUserCreate, user) # Prevent mypy complain
|
||||
existing_user = await user_db.get_by_email(user.email)
|
||||
|
||||
if existing_user is not None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ErrorCode.REGISTER_USER_ALREADY_EXISTS,
|
||||
)
|
||||
|
||||
hashed_password = get_password_hash(user.password)
|
||||
db_user = user_db_model(
|
||||
**user.create_update_dict(), hashed_password=hashed_password
|
||||
)
|
||||
created_user = await user_db.create(db_user)
|
||||
|
||||
await router.run_handlers(Event.ON_AFTER_REGISTER, created_user, request)
|
||||
|
||||
return created_user
|
||||
|
||||
@router.post("/forgot-password", status_code=status.HTTP_202_ACCEPTED)
|
||||
async def forgot_password(
|
||||
request: Request, email: EmailStr = Body(..., embed=True)
|
||||
):
|
||||
user = await user_db.get_by_email(email)
|
||||
|
||||
if user is not None and user.is_active:
|
||||
token_data = {"user_id": str(user.id), "aud": reset_password_token_audience}
|
||||
token = generate_jwt(
|
||||
token_data,
|
||||
reset_password_token_lifetime_seconds,
|
||||
reset_password_token_secret,
|
||||
)
|
||||
await router.run_handlers(
|
||||
Event.ON_AFTER_FORGOT_PASSWORD, user, token, request
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
@router.post("/reset-password")
|
||||
async def reset_password(token: str = Body(...), password: str = Body(...)):
|
||||
try:
|
||||
data = jwt.decode(
|
||||
token,
|
||||
reset_password_token_secret,
|
||||
audience=reset_password_token_audience,
|
||||
algorithms=[JWT_ALGORITHM],
|
||||
)
|
||||
user_id = data.get("user_id")
|
||||
if user_id is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ErrorCode.RESET_PASSWORD_BAD_TOKEN,
|
||||
)
|
||||
|
||||
try:
|
||||
user_uiid = UUID4(user_id)
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ErrorCode.RESET_PASSWORD_BAD_TOKEN,
|
||||
)
|
||||
|
||||
user = await user_db.get(user_uiid)
|
||||
if user is None or not user.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ErrorCode.RESET_PASSWORD_BAD_TOKEN,
|
||||
)
|
||||
|
||||
user.hashed_password = get_password_hash(password)
|
||||
await user_db.update(user)
|
||||
except jwt.PyJWTError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=ErrorCode.RESET_PASSWORD_BAD_TOKEN,
|
||||
)
|
||||
updated_user = await user_db.update(user)
|
||||
if after_update:
|
||||
await run_handler(after_update, updated_user, update_dict, request)
|
||||
return updated_user
|
||||
|
||||
@router.get("/me", response_model=user_model)
|
||||
async def me(
|
||||
@@ -185,11 +60,7 @@ def get_user_router(
|
||||
models.BaseUserUpdate, updated_user,
|
||||
) # Prevent mypy complain
|
||||
updated_user_data = updated_user.create_update_dict()
|
||||
updated_user = await _update_user(user, updated_user_data)
|
||||
|
||||
await router.run_handlers(
|
||||
Event.ON_AFTER_UPDATE, updated_user, updated_user_data, request
|
||||
)
|
||||
updated_user = await _update_user(user, updated_user_data, request)
|
||||
|
||||
return updated_user
|
||||
|
||||
@@ -207,14 +78,14 @@ def get_user_router(
|
||||
dependencies=[Depends(get_current_superuser)],
|
||||
)
|
||||
async def update_user(
|
||||
id: UUID4, updated_user: user_update_model, # type: ignore
|
||||
id: UUID4, updated_user: user_update_model, request: Request # type: ignore
|
||||
):
|
||||
updated_user = cast(
|
||||
models.BaseUserUpdate, updated_user,
|
||||
) # Prevent mypy complain
|
||||
user = await _get_or_404(id)
|
||||
updated_user_data = updated_user.create_update_dict_superuser()
|
||||
return await _update_user(user, updated_user_data)
|
||||
return await _update_user(user, updated_user_data, request)
|
||||
|
||||
@router.delete(
|
||||
"/{id}",
|
||||
|
||||
@@ -38,7 +38,12 @@ nav:
|
||||
- Introduction: configuration/authentication/index.md
|
||||
- configuration/authentication/jwt.md
|
||||
- configuration/authentication/cookie.md
|
||||
- configuration/router.md
|
||||
- Routers:
|
||||
- Introduction: configuration/routers/index.md
|
||||
- configuration/routers/auth.md
|
||||
- configuration/routers/register.md
|
||||
- configuration/routers/reset.md
|
||||
- configuration/routers/users.md
|
||||
- configuration/full_example.md
|
||||
- configuration/oauth.md
|
||||
- Usage:
|
||||
|
||||
@@ -26,6 +26,7 @@ requires = [
|
||||
"email-validator ==1.1.0",
|
||||
"pyjwt ==1.7.1",
|
||||
"python-multipart ==0.0.5",
|
||||
"makefun >=1.9.2,<1.10",
|
||||
]
|
||||
|
||||
[tool.flit.metadata.requires-extra]
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import asyncio
|
||||
from typing import Any, List, Mapping, Optional, Tuple
|
||||
from typing import List, Optional
|
||||
|
||||
import http.cookies
|
||||
import httpx
|
||||
import pytest
|
||||
from asgi_lifespan import LifespanManager
|
||||
from fastapi import Depends, FastAPI
|
||||
from fastapi import Depends, Response, FastAPI
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from httpx_oauth.oauth2 import OAuth2
|
||||
from pydantic import UUID4
|
||||
from starlette.applications import ASGIApp
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
|
||||
from fastapi_users import models
|
||||
from fastapi_users.authentication import Authenticator, BaseAuthentication
|
||||
@@ -229,16 +226,15 @@ def mock_user_db_oauth(
|
||||
return MockUserDatabase(UserDBOAuth)
|
||||
|
||||
|
||||
class MockAuthentication(BaseAuthentication):
|
||||
class MockAuthentication(BaseAuthentication[str]):
|
||||
def __init__(self, name: str = "mock"):
|
||||
super().__init__(name)
|
||||
super().__init__(name, logout=True)
|
||||
self.scheme = OAuth2PasswordBearer("/users/login", auto_error=False)
|
||||
|
||||
async def __call__(self, request: Request, user_db: BaseUserDatabase):
|
||||
token = await self.scheme.__call__(request)
|
||||
if token is not None:
|
||||
async def __call__(self, credentials: Optional[str], user_db: BaseUserDatabase):
|
||||
if credentials is not None:
|
||||
try:
|
||||
token_uuid = UUID4(token)
|
||||
token_uuid = UUID4(credentials)
|
||||
return await user_db.get(token_uuid)
|
||||
except ValueError:
|
||||
return None
|
||||
@@ -247,41 +243,15 @@ class MockAuthentication(BaseAuthentication):
|
||||
async def get_login_response(self, user: BaseUserDB, response: Response):
|
||||
return {"token": user.id}
|
||||
|
||||
async def get_logout_response(self, user: BaseUserDB, response: Response):
|
||||
return None
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_authentication():
|
||||
return MockAuthentication()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def request_builder():
|
||||
def _request_builder(
|
||||
headers: Mapping[str, Any] = None, cookies: Mapping[str, str] = None
|
||||
) -> Request:
|
||||
encoded_headers: List[Tuple[bytes, bytes]] = []
|
||||
|
||||
if headers is not None:
|
||||
encoded_headers += [
|
||||
(key.lower().encode("latin-1"), headers[key].encode("latin-1"))
|
||||
for key in headers
|
||||
]
|
||||
|
||||
if cookies is not None:
|
||||
for key in cookies:
|
||||
cookie = http.cookies.SimpleCookie() # type: http.cookies.BaseCookie
|
||||
cookie[key] = cookies[key]
|
||||
cookie_val = cookie.output(header="").strip()
|
||||
encoded_headers.append((b"cookie", cookie_val.encode("latin-1")))
|
||||
|
||||
scope = {
|
||||
"type": "http",
|
||||
"headers": encoded_headers,
|
||||
}
|
||||
return Request(scope)
|
||||
|
||||
return _request_builder
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def get_test_client():
|
||||
async def _get_test_client(app: ASGIApp) -> httpx.AsyncClient:
|
||||
@@ -304,7 +274,7 @@ def get_test_auth_client(mock_user_db, get_test_client):
|
||||
authenticator = Authenticator(backends, mock_user_db)
|
||||
|
||||
@app.get("/test-current-user")
|
||||
def test_current_user(user: UserDB = Depends(authenticator.get_current_user),):
|
||||
def test_current_user(user: UserDB = Depends(authenticator.get_current_user)):
|
||||
return user
|
||||
|
||||
@app.get("/test-current-active-user")
|
||||
|
||||
@@ -1,49 +1,63 @@
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
from starlette import status
|
||||
from starlette.requests import Request
|
||||
from fastapi import Request, status
|
||||
from fastapi.security.base import SecurityBase
|
||||
|
||||
from fastapi_users.authentication import BaseAuthentication
|
||||
from fastapi_users.authentication import (
|
||||
BaseAuthentication,
|
||||
DuplicateBackendNamesError,
|
||||
)
|
||||
from fastapi_users.db import BaseUserDatabase
|
||||
from fastapi_users.models import BaseUserDB
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def auth_backend_none():
|
||||
class BackendNone(BaseAuthentication):
|
||||
async def __call__(
|
||||
self, request: Request, user_db: BaseUserDatabase
|
||||
) -> Optional[BaseUserDB]:
|
||||
return None
|
||||
|
||||
return BackendNone()
|
||||
class MockSecurityScheme(SecurityBase):
|
||||
def __call__(self, request: Request) -> Optional[str]:
|
||||
return "mock"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def auth_backend_user(user):
|
||||
class BackendUser(BaseAuthentication):
|
||||
async def __call__(
|
||||
self, request: Request, user_db: BaseUserDatabase
|
||||
) -> Optional[BaseUserDB]:
|
||||
return user
|
||||
class BackendNone(BaseAuthentication[str]):
|
||||
def __init__(self, name="none"):
|
||||
super().__init__(name, logout=False)
|
||||
self.scheme = MockSecurityScheme()
|
||||
|
||||
return BackendUser()
|
||||
async def __call__(
|
||||
self, credentials: Optional[str], user_db: BaseUserDatabase
|
||||
) -> Optional[BaseUserDB]:
|
||||
return None
|
||||
|
||||
|
||||
class BackendUser(BaseAuthentication[str]):
|
||||
def __init__(self, user: BaseUserDB, name="user"):
|
||||
super().__init__(name, logout=False)
|
||||
self.scheme = MockSecurityScheme()
|
||||
self.user = user
|
||||
|
||||
async def __call__(
|
||||
self, credentials: Optional[str], user_db: BaseUserDatabase
|
||||
) -> Optional[BaseUserDB]:
|
||||
return self.user
|
||||
|
||||
|
||||
@pytest.mark.authentication
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticator(
|
||||
get_test_auth_client, auth_backend_none, auth_backend_user
|
||||
):
|
||||
client = await get_test_auth_client([auth_backend_none, auth_backend_user])
|
||||
async def test_authenticator(get_test_auth_client, user):
|
||||
client = await get_test_auth_client([BackendNone(), BackendUser(user)])
|
||||
response = await client.get("/test-current-user")
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
|
||||
@pytest.mark.authentication
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticator_none(get_test_auth_client, auth_backend_none):
|
||||
client = await get_test_auth_client([auth_backend_none, auth_backend_none])
|
||||
async def test_authenticator_none(get_test_auth_client):
|
||||
client = await get_test_auth_client([BackendNone(), BackendNone(name="none-bis")])
|
||||
response = await client.get("/test-current-user")
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
|
||||
@pytest.mark.authentication
|
||||
@pytest.mark.asyncio
|
||||
async def test_authenticators_with_same_name(get_test_auth_client):
|
||||
with pytest.raises(DuplicateBackendNamesError):
|
||||
await get_test_auth_client([BackendNone(), BackendNone()])
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import pytest
|
||||
from starlette.responses import Response
|
||||
from fastapi import Response
|
||||
|
||||
from fastapi_users.authentication import BaseAuthentication
|
||||
|
||||
@@ -12,12 +12,9 @@ def base_authentication():
|
||||
@pytest.mark.authentication
|
||||
class TestAuthenticate:
|
||||
@pytest.mark.asyncio
|
||||
async def test_not_implemented(
|
||||
self, base_authentication, mock_user_db, request_builder
|
||||
):
|
||||
request = request_builder({})
|
||||
async def test_not_implemented(self, base_authentication, mock_user_db):
|
||||
with pytest.raises(NotImplementedError):
|
||||
await base_authentication(request, mock_user_db)
|
||||
await base_authentication(None, mock_user_db)
|
||||
|
||||
|
||||
@pytest.mark.authentication
|
||||
|
||||
@@ -2,7 +2,7 @@ import re
|
||||
|
||||
import jwt
|
||||
import pytest
|
||||
from starlette.responses import Response
|
||||
from fastapi import Response
|
||||
|
||||
from fastapi_users.authentication.cookie import CookieAuthentication
|
||||
from fastapi_users.utils import JWT_ALGORITHM, generate_jwt
|
||||
@@ -28,10 +28,10 @@ cookie_authentication_httponly = CookieAuthentication(
|
||||
|
||||
@pytest.fixture
|
||||
def token():
|
||||
def _token(user=None, lifetime=LIFETIME):
|
||||
def _token(user_id=None, lifetime=LIFETIME):
|
||||
data = {"aud": "fastapi-users:auth"}
|
||||
if user is not None:
|
||||
data["user_id"] = str(user.id)
|
||||
if user_id is not None:
|
||||
data["user_id"] = str(user_id)
|
||||
return generate_jwt(data, lifetime, SECRET, JWT_ALGORITHM)
|
||||
|
||||
return _token
|
||||
@@ -45,35 +45,28 @@ def test_default_name():
|
||||
@pytest.mark.authentication
|
||||
class TestAuthenticate:
|
||||
@pytest.mark.asyncio
|
||||
async def test_missing_token(self, mock_user_db, request_builder):
|
||||
request = request_builder()
|
||||
authenticated_user = await cookie_authentication(request, mock_user_db)
|
||||
async def test_missing_token(self, mock_user_db):
|
||||
authenticated_user = await cookie_authentication(None, mock_user_db)
|
||||
assert authenticated_user is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invalid_token(self, mock_user_db, request_builder):
|
||||
cookies = {}
|
||||
cookies[COOKIE_NAME] = "foo"
|
||||
request = request_builder(cookies=cookies)
|
||||
authenticated_user = await cookie_authentication(request, mock_user_db)
|
||||
async def test_invalid_token(self, mock_user_db):
|
||||
authenticated_user = await cookie_authentication("foo", mock_user_db)
|
||||
assert authenticated_user is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_valid_token_missing_user_payload(
|
||||
self, mock_user_db, request_builder, token
|
||||
):
|
||||
cookies = {}
|
||||
cookies[COOKIE_NAME] = token()
|
||||
request = request_builder(cookies=cookies)
|
||||
authenticated_user = await cookie_authentication(request, mock_user_db)
|
||||
async def test_valid_token_missing_user_payload(self, mock_user_db, token):
|
||||
authenticated_user = await cookie_authentication(token(), mock_user_db)
|
||||
assert authenticated_user is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_valid_token(self, mock_user_db, request_builder, token, user):
|
||||
cookies = {}
|
||||
cookies[COOKIE_NAME] = token(user)
|
||||
request = request_builder(cookies=cookies)
|
||||
authenticated_user = await cookie_authentication(request, mock_user_db)
|
||||
async def test_valid_token_invalid_uuid(self, mock_user_db, token):
|
||||
authenticated_user = await cookie_authentication(token("foo"), mock_user_db)
|
||||
assert authenticated_user is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_valid_token(self, mock_user_db, token, user):
|
||||
authenticated_user = await cookie_authentication(token(user.id), mock_user_db)
|
||||
assert authenticated_user.id == user.id
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import jwt
|
||||
import pytest
|
||||
from starlette.responses import Response
|
||||
from fastapi import Response
|
||||
|
||||
from fastapi_users.authentication.jwt import JWTAuthentication
|
||||
from fastapi_users.utils import JWT_ALGORITHM, generate_jwt
|
||||
@@ -34,43 +34,32 @@ def test_default_name(jwt_authentication):
|
||||
@pytest.mark.authentication
|
||||
class TestAuthenticate:
|
||||
@pytest.mark.asyncio
|
||||
async def test_missing_token(
|
||||
self, jwt_authentication, mock_user_db, request_builder
|
||||
):
|
||||
request = request_builder(headers={})
|
||||
authenticated_user = await jwt_authentication(request, mock_user_db)
|
||||
async def test_missing_token(self, jwt_authentication, mock_user_db):
|
||||
authenticated_user = await jwt_authentication(None, mock_user_db)
|
||||
assert authenticated_user is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invalid_token(
|
||||
self, jwt_authentication, mock_user_db, request_builder
|
||||
):
|
||||
request = request_builder(headers={"Authorization": "Bearer foo"})
|
||||
authenticated_user = await jwt_authentication(request, mock_user_db)
|
||||
async def test_invalid_token(self, jwt_authentication, mock_user_db):
|
||||
authenticated_user = await jwt_authentication("foo", mock_user_db)
|
||||
assert authenticated_user is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_valid_token_missing_user_payload(
|
||||
self, jwt_authentication, mock_user_db, request_builder, token
|
||||
self, jwt_authentication, mock_user_db, token
|
||||
):
|
||||
request = request_builder(headers={"Authorization": f"Bearer {token()}"})
|
||||
authenticated_user = await jwt_authentication(request, mock_user_db)
|
||||
authenticated_user = await jwt_authentication(token(), mock_user_db)
|
||||
assert authenticated_user is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_valid_token_invalid_uuid(
|
||||
self, jwt_authentication, mock_user_db, request_builder, token
|
||||
self, jwt_authentication, mock_user_db, token
|
||||
):
|
||||
request = request_builder(headers={"Authorization": f"Bearer {token('foo')}"})
|
||||
authenticated_user = await jwt_authentication(request, mock_user_db)
|
||||
authenticated_user = await jwt_authentication(token("foo"), mock_user_db)
|
||||
assert authenticated_user is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_valid_token(
|
||||
self, jwt_authentication, mock_user_db, request_builder, token, user
|
||||
):
|
||||
request = request_builder(headers={"Authorization": f"Bearer {token(user.id)}"})
|
||||
authenticated_user = await jwt_authentication(request, mock_user_db)
|
||||
async def test_valid_token(self, jwt_authentication, mock_user_db, token, user):
|
||||
authenticated_user = await jwt_authentication(token(user.id), mock_user_db)
|
||||
assert authenticated_user.id == user.id
|
||||
|
||||
|
||||
|
||||
@@ -1,58 +1,26 @@
|
||||
import pytest
|
||||
import httpx
|
||||
from fastapi import Depends, FastAPI
|
||||
from httpx_oauth.oauth2 import OAuth2
|
||||
from starlette import status
|
||||
from fastapi import Depends, FastAPI, status
|
||||
|
||||
from fastapi_users import FastAPIUsers
|
||||
from fastapi_users.router import Event, EventHandlersRouter
|
||||
from tests.conftest import User, UserCreate, UserUpdate, UserDB
|
||||
|
||||
|
||||
def sync_event_handler():
|
||||
return None
|
||||
|
||||
|
||||
async def async_event_handler():
|
||||
return None
|
||||
|
||||
|
||||
@pytest.fixture(params=[sync_event_handler, async_event_handler])
|
||||
def fastapi_users(
|
||||
request, mock_user_db, mock_authentication, oauth_client
|
||||
) -> FastAPIUsers:
|
||||
fastapi_users = FastAPIUsers(
|
||||
mock_user_db,
|
||||
[mock_authentication],
|
||||
User,
|
||||
UserCreate,
|
||||
UserUpdate,
|
||||
UserDB,
|
||||
"SECRET",
|
||||
)
|
||||
|
||||
fastapi_users.get_oauth_router(oauth_client, "SECRET")
|
||||
|
||||
@fastapi_users.on_after_register()
|
||||
def on_after_register():
|
||||
return request.param()
|
||||
|
||||
@fastapi_users.on_after_forgot_password()
|
||||
def on_after_forgot_password():
|
||||
return request.param()
|
||||
|
||||
@fastapi_users.on_after_update()
|
||||
def on_after_update():
|
||||
return request.param()
|
||||
|
||||
return fastapi_users
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@pytest.mark.asyncio
|
||||
async def test_app_client(fastapi_users, get_test_client) -> httpx.AsyncClient:
|
||||
async def test_app_client(
|
||||
mock_user_db, mock_authentication, oauth_client, get_test_client
|
||||
) -> httpx.AsyncClient:
|
||||
fastapi_users = FastAPIUsers(
|
||||
mock_user_db, [mock_authentication], User, UserCreate, UserUpdate, UserDB,
|
||||
)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(fastapi_users.router, prefix="/users")
|
||||
app.include_router(fastapi_users.get_register_router())
|
||||
app.include_router(fastapi_users.get_reset_password_router("SECRET"))
|
||||
app.include_router(fastapi_users.get_auth_router(mock_authentication))
|
||||
app.include_router(fastapi_users.get_oauth_router(oauth_client, "SECRET"))
|
||||
app.include_router(fastapi_users.get_users_router(), prefix="/users")
|
||||
|
||||
@app.get("/current-user")
|
||||
def current_user(user=Depends(fastapi_users.get_current_user)):
|
||||
@@ -69,35 +37,51 @@ async def test_app_client(fastapi_users, get_test_client) -> httpx.AsyncClient:
|
||||
return await get_test_client(app)
|
||||
|
||||
|
||||
@pytest.mark.fastapi_users
|
||||
class TestFastAPIUsers:
|
||||
def test_event_handlers(self, fastapi_users):
|
||||
event_handlers = fastapi_users.router.event_handlers
|
||||
assert len(event_handlers[Event.ON_AFTER_REGISTER]) == 1
|
||||
assert len(event_handlers[Event.ON_AFTER_FORGOT_PASSWORD]) == 1
|
||||
|
||||
|
||||
@pytest.mark.fastapi_users
|
||||
@pytest.mark.asyncio
|
||||
class TestRouter:
|
||||
class TestRoutes:
|
||||
async def test_routes_exist(self, test_app_client: httpx.AsyncClient):
|
||||
response = await test_app_client.post("/users/register")
|
||||
assert response.status_code != status.HTTP_404_NOT_FOUND
|
||||
response = await test_app_client.post("/register")
|
||||
assert response.status_code not in (
|
||||
status.HTTP_404_NOT_FOUND,
|
||||
status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||
)
|
||||
|
||||
response = await test_app_client.post("/users/login")
|
||||
assert response.status_code != status.HTTP_404_NOT_FOUND
|
||||
response = await test_app_client.post("/forgot-password")
|
||||
assert response.status_code not in (
|
||||
status.HTTP_404_NOT_FOUND,
|
||||
status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||
)
|
||||
|
||||
response = await test_app_client.post("/users/forgot-password")
|
||||
assert response.status_code != status.HTTP_404_NOT_FOUND
|
||||
response = await test_app_client.post("/reset-password")
|
||||
assert response.status_code not in (
|
||||
status.HTTP_404_NOT_FOUND,
|
||||
status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||
)
|
||||
|
||||
response = await test_app_client.post("/users/reset-password")
|
||||
assert response.status_code != status.HTTP_404_NOT_FOUND
|
||||
response = await test_app_client.post("/login")
|
||||
assert response.status_code not in (
|
||||
status.HTTP_404_NOT_FOUND,
|
||||
status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||
)
|
||||
|
||||
response = await test_app_client.post("/logout")
|
||||
assert response.status_code not in (
|
||||
status.HTTP_404_NOT_FOUND,
|
||||
status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||
)
|
||||
|
||||
response = await test_app_client.get("/users/aaa")
|
||||
assert response.status_code != status.HTTP_404_NOT_FOUND
|
||||
assert response.status_code not in (
|
||||
status.HTTP_404_NOT_FOUND,
|
||||
status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||
)
|
||||
|
||||
response = await test_app_client.patch("/users/aaa")
|
||||
assert response.status_code != status.HTTP_404_NOT_FOUND
|
||||
assert response.status_code not in (
|
||||
status.HTTP_404_NOT_FOUND,
|
||||
status.HTTP_405_METHOD_NOT_ALLOWED,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.fastapi_users
|
||||
@@ -177,21 +161,3 @@ class TestGetCurrentSuperuser:
|
||||
"/current-superuser", headers={"Authorization": f"Bearer {superuser.id}"}
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
|
||||
@pytest.mark.fastapi_users
|
||||
def test_get_oauth_router(mocker, fastapi_users: FastAPIUsers, oauth_client: OAuth2):
|
||||
# Check that existing OAuth router declared
|
||||
# before the handlers decorators is correctly binded
|
||||
existing_oauth_router = fastapi_users.oauth_routers[0]
|
||||
event_handlers = existing_oauth_router.event_handlers
|
||||
assert len(event_handlers[Event.ON_AFTER_REGISTER]) == 1
|
||||
assert len(event_handlers[Event.ON_AFTER_FORGOT_PASSWORD]) == 1
|
||||
|
||||
# Check that OAuth router declared
|
||||
# after the handlers decorators is correctly binded
|
||||
oauth_router = fastapi_users.get_oauth_router(oauth_client, "SECRET")
|
||||
assert isinstance(oauth_router, EventHandlersRouter)
|
||||
event_handlers = oauth_router.event_handlers
|
||||
assert len(event_handlers[Event.ON_AFTER_REGISTER]) == 1
|
||||
assert len(event_handlers[Event.ON_AFTER_FORGOT_PASSWORD]) == 1
|
||||
|
||||
96
tests/test_router_auth.py
Normal file
96
tests/test_router_auth.py
Normal file
@@ -0,0 +1,96 @@
|
||||
from typing import cast, Dict, Any
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from fastapi import FastAPI, status
|
||||
|
||||
from fastapi_users.authentication import Authenticator
|
||||
from fastapi_users.router import ErrorCode, get_auth_router
|
||||
from tests.conftest import MockAuthentication, UserDB
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@pytest.mark.asyncio
|
||||
async def test_app_client(
|
||||
mock_user_db, mock_authentication, get_test_client
|
||||
) -> httpx.AsyncClient:
|
||||
mock_authentication_bis = MockAuthentication(name="mock-bis")
|
||||
authenticator = Authenticator(
|
||||
[mock_authentication, mock_authentication_bis], mock_user_db
|
||||
)
|
||||
|
||||
mock_auth_router = get_auth_router(mock_authentication, mock_user_db, authenticator)
|
||||
mock_bis_auth_router = get_auth_router(
|
||||
mock_authentication_bis, mock_user_db, authenticator
|
||||
)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(mock_auth_router, prefix="/mock")
|
||||
app.include_router(mock_bis_auth_router, prefix="/mock-bis")
|
||||
|
||||
return await get_test_client(app)
|
||||
|
||||
|
||||
@pytest.mark.router
|
||||
@pytest.mark.parametrize("path", ["/mock/login", "/mock-bis/login"])
|
||||
@pytest.mark.asyncio
|
||||
class TestLogin:
|
||||
async def test_empty_body(self, path, test_app_client: httpx.AsyncClient):
|
||||
response = await test_app_client.post(path, data={})
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
async def test_missing_username(self, path, test_app_client: httpx.AsyncClient):
|
||||
data = {"password": "guinevere"}
|
||||
response = await test_app_client.post(path, data=data)
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
async def test_missing_password(self, path, test_app_client: httpx.AsyncClient):
|
||||
data = {"username": "king.arthur@camelot.bt"}
|
||||
response = await test_app_client.post(path, data=data)
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
async def test_not_existing_user(self, path, test_app_client: httpx.AsyncClient):
|
||||
data = {"username": "lancelot@camelot.bt", "password": "guinevere"}
|
||||
response = await test_app_client.post(path, data=data)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["detail"] == ErrorCode.LOGIN_BAD_CREDENTIALS
|
||||
|
||||
async def test_wrong_password(self, path, test_app_client: httpx.AsyncClient):
|
||||
data = {"username": "king.arthur@camelot.bt", "password": "percival"}
|
||||
response = await test_app_client.post(path, data=data)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["detail"] == ErrorCode.LOGIN_BAD_CREDENTIALS
|
||||
|
||||
async def test_valid_credentials(
|
||||
self, path, test_app_client: httpx.AsyncClient, user: UserDB
|
||||
):
|
||||
data = {"username": "king.arthur@camelot.bt", "password": "guinevere"}
|
||||
response = await test_app_client.post(path, data=data)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == {"token": str(user.id)}
|
||||
|
||||
async def test_inactive_user(self, path, test_app_client: httpx.AsyncClient):
|
||||
data = {"username": "percival@camelot.bt", "password": "angharad"}
|
||||
response = await test_app_client.post(path, data=data)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["detail"] == ErrorCode.LOGIN_BAD_CREDENTIALS
|
||||
|
||||
|
||||
@pytest.mark.router
|
||||
@pytest.mark.parametrize("path", ["/mock/logout", "/mock-bis/logout"])
|
||||
@pytest.mark.asyncio
|
||||
class TestLogout:
|
||||
async def test_missing_token(self, path, test_app_client: httpx.AsyncClient):
|
||||
response = await test_app_client.post(path)
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
async def test_valid_credentials(
|
||||
self, mocker, path, test_app_client: httpx.AsyncClient, user: UserDB
|
||||
):
|
||||
response = await test_app_client.post(
|
||||
path, headers={"Authorization": f"Bearer {user.id}"}
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
@@ -4,12 +4,10 @@ from typing import Dict, Any, cast
|
||||
import asynctest
|
||||
import httpx
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from starlette import status
|
||||
from starlette.requests import Request
|
||||
from fastapi import FastAPI, status, Request
|
||||
|
||||
from fastapi_users.authentication import Authenticator
|
||||
from fastapi_users.router.common import ErrorCode, Event
|
||||
from fastapi_users.router.common import ErrorCode
|
||||
from fastapi_users.router.oauth import generate_state_token, get_oauth_router
|
||||
from tests.conftest import MockAuthentication, UserDB
|
||||
|
||||
@@ -17,16 +15,16 @@ from tests.conftest import MockAuthentication, UserDB
|
||||
SECRET = "SECRET"
|
||||
|
||||
|
||||
def event_handler_sync():
|
||||
def after_register_sync():
|
||||
return MagicMock(return_value=None)
|
||||
|
||||
|
||||
def event_handler_async():
|
||||
def after_register_async():
|
||||
return asynctest.CoroutineMock(return_value=None)
|
||||
|
||||
|
||||
@pytest.fixture(params=[event_handler_sync, event_handler_async])
|
||||
def event_handler(request):
|
||||
@pytest.fixture(params=[after_register_sync, after_register_async])
|
||||
def after_register(request):
|
||||
return request.param()
|
||||
|
||||
|
||||
@@ -35,7 +33,7 @@ def get_test_app_client(
|
||||
mock_user_db_oauth,
|
||||
mock_authentication,
|
||||
oauth_client,
|
||||
event_handler,
|
||||
after_register,
|
||||
get_test_client,
|
||||
):
|
||||
async def _get_test_app_client(redirect_url: str = None) -> httpx.AsyncClient:
|
||||
@@ -51,10 +49,9 @@ def get_test_app_client(
|
||||
authenticator,
|
||||
SECRET,
|
||||
redirect_url,
|
||||
after_register,
|
||||
)
|
||||
|
||||
oauth_router.add_event_handler(Event.ON_AFTER_REGISTER, event_handler)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(oauth_router)
|
||||
|
||||
@@ -151,7 +148,7 @@ class TestCallback:
|
||||
test_app_client: httpx.AsyncClient,
|
||||
oauth_client,
|
||||
user_oauth,
|
||||
event_handler,
|
||||
after_register,
|
||||
):
|
||||
with asynctest.patch.object(
|
||||
oauth_client, "get_access_token"
|
||||
@@ -171,7 +168,7 @@ class TestCallback:
|
||||
get_id_email_mock.assert_awaited_once_with("TOKEN")
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
assert event_handler.called is False
|
||||
assert after_register.called is False
|
||||
|
||||
async def test_existing_user_with_oauth(
|
||||
self,
|
||||
@@ -179,7 +176,7 @@ class TestCallback:
|
||||
test_app_client: httpx.AsyncClient,
|
||||
oauth_client,
|
||||
user_oauth,
|
||||
event_handler,
|
||||
after_register,
|
||||
):
|
||||
state_jwt = generate_state_token({"authentication_backend": "mock"}, "SECRET")
|
||||
with asynctest.patch.object(
|
||||
@@ -206,7 +203,7 @@ class TestCallback:
|
||||
|
||||
assert data["token"] == str(user_oauth.id)
|
||||
|
||||
assert event_handler.called is False
|
||||
assert after_register.called is False
|
||||
|
||||
async def test_existing_user_without_oauth(
|
||||
self,
|
||||
@@ -214,7 +211,7 @@ class TestCallback:
|
||||
test_app_client: httpx.AsyncClient,
|
||||
oauth_client,
|
||||
superuser_oauth,
|
||||
event_handler,
|
||||
after_register,
|
||||
):
|
||||
state_jwt = generate_state_token({"authentication_backend": "mock"}, "SECRET")
|
||||
with asynctest.patch.object(
|
||||
@@ -244,14 +241,14 @@ class TestCallback:
|
||||
|
||||
assert data["token"] == str(superuser_oauth.id)
|
||||
|
||||
assert event_handler.called is False
|
||||
assert after_register.called is False
|
||||
|
||||
async def test_unknown_user(
|
||||
self,
|
||||
mock_user_db_oauth,
|
||||
test_app_client: httpx.AsyncClient,
|
||||
oauth_client,
|
||||
event_handler,
|
||||
after_register,
|
||||
):
|
||||
state_jwt = generate_state_token({"authentication_backend": "mock"}, "SECRET")
|
||||
with asynctest.patch.object(
|
||||
@@ -281,10 +278,10 @@ class TestCallback:
|
||||
|
||||
assert "token" in data
|
||||
|
||||
assert event_handler.called is True
|
||||
actual_user = event_handler.call_args[0][0]
|
||||
assert after_register.called is True
|
||||
actual_user = after_register.call_args[0][0]
|
||||
assert str(actual_user.id) == data["token"]
|
||||
request = event_handler.call_args[0][1]
|
||||
request = after_register.call_args[0][1]
|
||||
assert isinstance(request, Request)
|
||||
|
||||
async def test_inactive_user(
|
||||
@@ -293,7 +290,7 @@ class TestCallback:
|
||||
test_app_client: httpx.AsyncClient,
|
||||
oauth_client,
|
||||
inactive_user_oauth,
|
||||
event_handler,
|
||||
after_register,
|
||||
):
|
||||
state_jwt = generate_state_token({"authentication_backend": "mock"}, "SECRET")
|
||||
with asynctest.patch.object(
|
||||
@@ -318,7 +315,7 @@ class TestCallback:
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["detail"] == ErrorCode.LOGIN_BAD_CREDENTIALS
|
||||
|
||||
assert event_handler.called is False
|
||||
assert after_register.called is False
|
||||
|
||||
async def test_redirect_url_router(
|
||||
self,
|
||||
|
||||
122
tests/test_router_register.py
Normal file
122
tests/test_router_register.py
Normal file
@@ -0,0 +1,122 @@
|
||||
from typing import cast, Dict, Any
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import asynctest
|
||||
import httpx
|
||||
import pytest
|
||||
from fastapi import FastAPI, status, Request
|
||||
|
||||
from fastapi_users.router import ErrorCode, get_register_router
|
||||
from tests.conftest import User, UserCreate, UserDB
|
||||
|
||||
SECRET = "SECRET"
|
||||
LIFETIME = 3600
|
||||
|
||||
|
||||
def after_register_sync():
|
||||
return MagicMock(return_value=None)
|
||||
|
||||
|
||||
def after_register_async():
|
||||
return asynctest.CoroutineMock(return_value=None)
|
||||
|
||||
|
||||
@pytest.fixture(params=[after_register_sync, after_register_async])
|
||||
def after_register(request):
|
||||
return request.param()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@pytest.mark.asyncio
|
||||
async def test_app_client(
|
||||
mock_user_db, mock_authentication, after_register, get_test_client
|
||||
) -> httpx.AsyncClient:
|
||||
register_router = get_register_router(
|
||||
mock_user_db, User, UserCreate, UserDB, after_register,
|
||||
)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(register_router)
|
||||
|
||||
return await get_test_client(app)
|
||||
|
||||
|
||||
@pytest.mark.router
|
||||
@pytest.mark.asyncio
|
||||
class TestRegister:
|
||||
async def test_empty_body(self, test_app_client: httpx.AsyncClient, after_register):
|
||||
response = await test_app_client.post("/register", json={})
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
assert after_register.called is False
|
||||
|
||||
async def test_missing_password(
|
||||
self, test_app_client: httpx.AsyncClient, after_register
|
||||
):
|
||||
json = {"email": "king.arthur@camelot.bt"}
|
||||
response = await test_app_client.post("/register", json=json)
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
assert after_register.called is False
|
||||
|
||||
async def test_wrong_email(
|
||||
self, test_app_client: httpx.AsyncClient, after_register
|
||||
):
|
||||
json = {"email": "king.arthur", "password": "guinevere"}
|
||||
response = await test_app_client.post("/register", json=json)
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
assert after_register.called is False
|
||||
|
||||
async def test_existing_user(
|
||||
self, test_app_client: httpx.AsyncClient, after_register
|
||||
):
|
||||
json = {"email": "king.arthur@camelot.bt", "password": "guinevere"}
|
||||
response = await test_app_client.post("/register", json=json)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["detail"] == ErrorCode.REGISTER_USER_ALREADY_EXISTS
|
||||
assert after_register.called is False
|
||||
|
||||
async def test_valid_body(self, test_app_client: httpx.AsyncClient, after_register):
|
||||
json = {"email": "lancelot@camelot.bt", "password": "guinevere"}
|
||||
response = await test_app_client.post("/register", json=json)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert after_register.called is True
|
||||
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert "hashed_password" not in data
|
||||
assert "password" not in data
|
||||
assert data["id"] is not None
|
||||
|
||||
actual_user = after_register.call_args[0][0]
|
||||
assert str(actual_user.id) == data["id"]
|
||||
request = after_register.call_args[0][1]
|
||||
assert isinstance(request, Request)
|
||||
|
||||
async def test_valid_body_is_superuser(
|
||||
self, test_app_client: httpx.AsyncClient, after_register
|
||||
):
|
||||
json = {
|
||||
"email": "lancelot@camelot.bt",
|
||||
"password": "guinevere",
|
||||
"is_superuser": True,
|
||||
}
|
||||
response = await test_app_client.post("/register", json=json)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert after_register.called is True
|
||||
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["is_superuser"] is False
|
||||
|
||||
async def test_valid_body_is_active(
|
||||
self, test_app_client: httpx.AsyncClient, after_register
|
||||
):
|
||||
json = {
|
||||
"email": "lancelot@camelot.bt",
|
||||
"password": "guinevere",
|
||||
"is_active": False,
|
||||
}
|
||||
response = await test_app_client.post("/register", json=json)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert after_register.called is True
|
||||
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["is_active"] is True
|
||||
198
tests/test_router_reset.py
Normal file
198
tests/test_router_reset.py
Normal file
@@ -0,0 +1,198 @@
|
||||
from typing import cast, Dict, Any
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import asynctest
|
||||
import httpx
|
||||
import jwt
|
||||
import pytest
|
||||
from fastapi import FastAPI, status, Request
|
||||
|
||||
from fastapi_users.router import ErrorCode, get_reset_password_router
|
||||
from fastapi_users.utils import JWT_ALGORITHM, generate_jwt
|
||||
from tests.conftest import UserDB
|
||||
|
||||
SECRET = "SECRET"
|
||||
LIFETIME = 3600
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def forgot_password_token():
|
||||
def _forgot_password_token(user_id=None, lifetime=LIFETIME):
|
||||
data = {"aud": "fastapi-users:reset"}
|
||||
if user_id is not None:
|
||||
data["user_id"] = str(user_id)
|
||||
return generate_jwt(data, lifetime, SECRET, JWT_ALGORITHM)
|
||||
|
||||
return _forgot_password_token
|
||||
|
||||
|
||||
def after_forgot_password_sync():
|
||||
return MagicMock(return_value=None)
|
||||
|
||||
|
||||
def after_forgot_password_async():
|
||||
return asynctest.CoroutineMock(return_value=None)
|
||||
|
||||
|
||||
@pytest.fixture(params=[after_forgot_password_sync, after_forgot_password_async])
|
||||
def after_forgot_password(request):
|
||||
return request.param()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@pytest.mark.asyncio
|
||||
async def test_app_client(
|
||||
mock_user_db, mock_authentication, after_forgot_password, get_test_client
|
||||
) -> httpx.AsyncClient:
|
||||
reset_router = get_reset_password_router(
|
||||
mock_user_db, SECRET, LIFETIME, after_forgot_password
|
||||
)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(reset_router)
|
||||
|
||||
return await get_test_client(app)
|
||||
|
||||
|
||||
@pytest.mark.router
|
||||
@pytest.mark.asyncio
|
||||
class TestForgotPassword:
|
||||
async def test_empty_body(
|
||||
self, test_app_client: httpx.AsyncClient, after_forgot_password
|
||||
):
|
||||
response = await test_app_client.post("/forgot-password", json={})
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
assert after_forgot_password.called is False
|
||||
|
||||
async def test_not_existing_user(
|
||||
self, test_app_client: httpx.AsyncClient, after_forgot_password
|
||||
):
|
||||
json = {"email": "lancelot@camelot.bt"}
|
||||
response = await test_app_client.post("/forgot-password", json=json)
|
||||
assert response.status_code == status.HTTP_202_ACCEPTED
|
||||
assert after_forgot_password.called is False
|
||||
|
||||
async def test_inactive_user(
|
||||
self, test_app_client: httpx.AsyncClient, after_forgot_password
|
||||
):
|
||||
json = {"email": "percival@camelot.bt"}
|
||||
response = await test_app_client.post("/forgot-password", json=json)
|
||||
assert response.status_code == status.HTTP_202_ACCEPTED
|
||||
assert after_forgot_password.called is False
|
||||
|
||||
async def test_existing_user(
|
||||
self, test_app_client: httpx.AsyncClient, after_forgot_password, user
|
||||
):
|
||||
json = {"email": "king.arthur@camelot.bt"}
|
||||
response = await test_app_client.post("/forgot-password", json=json)
|
||||
assert response.status_code == status.HTTP_202_ACCEPTED
|
||||
assert after_forgot_password.called is True
|
||||
|
||||
actual_user = after_forgot_password.call_args[0][0]
|
||||
assert actual_user.id == user.id
|
||||
actual_token = after_forgot_password.call_args[0][1]
|
||||
decoded_token = jwt.decode(
|
||||
actual_token,
|
||||
SECRET,
|
||||
audience="fastapi-users:reset",
|
||||
algorithms=[JWT_ALGORITHM],
|
||||
)
|
||||
assert decoded_token["user_id"] == str(user.id)
|
||||
request = after_forgot_password.call_args[0][2]
|
||||
assert isinstance(request, Request)
|
||||
|
||||
|
||||
@pytest.mark.router
|
||||
@pytest.mark.asyncio
|
||||
class TestResetPassword:
|
||||
async def test_empty_body(self, test_app_client: httpx.AsyncClient):
|
||||
response = await test_app_client.post("/reset-password", json={})
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
async def test_missing_token(self, test_app_client: httpx.AsyncClient):
|
||||
json = {"password": "guinevere"}
|
||||
response = await test_app_client.post("/reset-password", json=json)
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
async def test_missing_password(self, test_app_client: httpx.AsyncClient):
|
||||
json = {"token": "foo"}
|
||||
response = await test_app_client.post("/reset-password", json=json)
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
async def test_invalid_token(self, test_app_client: httpx.AsyncClient):
|
||||
json = {"token": "foo", "password": "guinevere"}
|
||||
response = await test_app_client.post("/reset-password", json=json)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["detail"] == ErrorCode.RESET_PASSWORD_BAD_TOKEN
|
||||
|
||||
async def test_valid_token_missing_user_id_payload(
|
||||
self,
|
||||
mocker,
|
||||
mock_user_db,
|
||||
test_app_client: httpx.AsyncClient,
|
||||
forgot_password_token,
|
||||
):
|
||||
mocker.spy(mock_user_db, "update")
|
||||
|
||||
json = {"token": forgot_password_token(), "password": "holygrail"}
|
||||
response = await test_app_client.post("/reset-password", json=json)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["detail"] == ErrorCode.RESET_PASSWORD_BAD_TOKEN
|
||||
assert mock_user_db.update.called is False
|
||||
|
||||
async def test_valid_token_invalid_uuid(
|
||||
self,
|
||||
mocker,
|
||||
mock_user_db,
|
||||
test_app_client: httpx.AsyncClient,
|
||||
forgot_password_token,
|
||||
):
|
||||
mocker.spy(mock_user_db, "update")
|
||||
|
||||
json = {"token": forgot_password_token("foo"), "password": "holygrail"}
|
||||
response = await test_app_client.post("/reset-password", json=json)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["detail"] == ErrorCode.RESET_PASSWORD_BAD_TOKEN
|
||||
assert mock_user_db.update.called is False
|
||||
|
||||
async def test_inactive_user(
|
||||
self,
|
||||
mocker,
|
||||
mock_user_db,
|
||||
test_app_client: httpx.AsyncClient,
|
||||
forgot_password_token,
|
||||
inactive_user: UserDB,
|
||||
):
|
||||
mocker.spy(mock_user_db, "update")
|
||||
|
||||
json = {
|
||||
"token": forgot_password_token(inactive_user.id),
|
||||
"password": "holygrail",
|
||||
}
|
||||
response = await test_app_client.post("/reset-password", json=json)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["detail"] == ErrorCode.RESET_PASSWORD_BAD_TOKEN
|
||||
assert mock_user_db.update.called is False
|
||||
|
||||
async def test_existing_user(
|
||||
self,
|
||||
mocker,
|
||||
mock_user_db,
|
||||
test_app_client: httpx.AsyncClient,
|
||||
forgot_password_token,
|
||||
user: UserDB,
|
||||
):
|
||||
mocker.spy(mock_user_db, "update")
|
||||
current_hashed_passord = user.hashed_password
|
||||
|
||||
json = {"token": forgot_password_token(user.id), "password": "holygrail"}
|
||||
response = await test_app_client.post("/reset-password", json=json)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert mock_user_db.update.called is True
|
||||
|
||||
updated_user = mock_user_db.update.call_args[0][0]
|
||||
assert updated_user.hashed_password != current_hashed_passord
|
||||
@@ -3,365 +3,50 @@ from unittest.mock import MagicMock
|
||||
|
||||
import asynctest
|
||||
import httpx
|
||||
import jwt
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from starlette import status
|
||||
from starlette.requests import Request
|
||||
from fastapi import FastAPI, status, Request
|
||||
|
||||
from fastapi_users.authentication import Authenticator
|
||||
from fastapi_users.router import ErrorCode, Event, get_user_router
|
||||
from fastapi_users.utils import JWT_ALGORITHM, generate_jwt
|
||||
from tests.conftest import MockAuthentication, User, UserCreate, UserUpdate, UserDB
|
||||
from fastapi_users.router import get_users_router
|
||||
from tests.conftest import MockAuthentication, User, UserUpdate, UserDB
|
||||
|
||||
SECRET = "SECRET"
|
||||
LIFETIME = 3600
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def forgot_password_token():
|
||||
def _forgot_password_token(user_id=None, lifetime=LIFETIME):
|
||||
data = {"aud": "fastapi-users:reset"}
|
||||
if user_id is not None:
|
||||
data["user_id"] = str(user_id)
|
||||
return generate_jwt(data, lifetime, SECRET, JWT_ALGORITHM)
|
||||
|
||||
return _forgot_password_token
|
||||
|
||||
|
||||
def event_handler_sync():
|
||||
def after_update_sync():
|
||||
return MagicMock(return_value=None)
|
||||
|
||||
|
||||
def event_handler_async():
|
||||
def after_update_async():
|
||||
return asynctest.CoroutineMock(return_value=None)
|
||||
|
||||
|
||||
@pytest.fixture(params=[event_handler_sync, event_handler_async])
|
||||
def event_handler(request):
|
||||
@pytest.fixture(params=[after_update_sync, after_update_async])
|
||||
def after_update(request):
|
||||
return request.param()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@pytest.mark.asyncio
|
||||
async def test_app_client(
|
||||
mock_user_db, mock_authentication, event_handler, get_test_client
|
||||
mock_user_db, mock_authentication, after_update, get_test_client
|
||||
) -> httpx.AsyncClient:
|
||||
mock_authentication_bis = MockAuthentication(name="mock-bis")
|
||||
authenticator = Authenticator(
|
||||
[mock_authentication, mock_authentication_bis], mock_user_db
|
||||
)
|
||||
|
||||
user_router = get_user_router(
|
||||
mock_user_db,
|
||||
User,
|
||||
UserCreate,
|
||||
UserUpdate,
|
||||
UserDB,
|
||||
authenticator,
|
||||
SECRET,
|
||||
LIFETIME,
|
||||
user_router = get_users_router(
|
||||
mock_user_db, User, UserUpdate, UserDB, authenticator, after_update,
|
||||
)
|
||||
|
||||
user_router.add_event_handler(Event.ON_AFTER_REGISTER, event_handler)
|
||||
user_router.add_event_handler(Event.ON_AFTER_FORGOT_PASSWORD, event_handler)
|
||||
user_router.add_event_handler(Event.ON_AFTER_UPDATE, event_handler)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(user_router)
|
||||
|
||||
return await get_test_client(app)
|
||||
|
||||
|
||||
@pytest.mark.router
|
||||
@pytest.mark.asyncio
|
||||
class TestRegister:
|
||||
async def test_empty_body(self, test_app_client: httpx.AsyncClient, event_handler):
|
||||
response = await test_app_client.post("/register", json={})
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
assert event_handler.called is False
|
||||
|
||||
async def test_missing_password(
|
||||
self, test_app_client: httpx.AsyncClient, event_handler
|
||||
):
|
||||
json = {"email": "king.arthur@camelot.bt"}
|
||||
response = await test_app_client.post("/register", json=json)
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
assert event_handler.called is False
|
||||
|
||||
async def test_wrong_email(self, test_app_client: httpx.AsyncClient, event_handler):
|
||||
json = {"email": "king.arthur", "password": "guinevere"}
|
||||
response = await test_app_client.post("/register", json=json)
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
assert event_handler.called is False
|
||||
|
||||
async def test_existing_user(
|
||||
self, test_app_client: httpx.AsyncClient, event_handler
|
||||
):
|
||||
json = {"email": "king.arthur@camelot.bt", "password": "guinevere"}
|
||||
response = await test_app_client.post("/register", json=json)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["detail"] == ErrorCode.REGISTER_USER_ALREADY_EXISTS
|
||||
assert event_handler.called is False
|
||||
|
||||
async def test_valid_body(self, test_app_client: httpx.AsyncClient, event_handler):
|
||||
json = {"email": "lancelot@camelot.bt", "password": "guinevere"}
|
||||
response = await test_app_client.post("/register", json=json)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert event_handler.called is True
|
||||
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert "hashed_password" not in data
|
||||
assert "password" not in data
|
||||
assert data["id"] is not None
|
||||
|
||||
actual_user = event_handler.call_args[0][0]
|
||||
assert str(actual_user.id) == data["id"]
|
||||
request = event_handler.call_args[0][1]
|
||||
assert isinstance(request, Request)
|
||||
|
||||
async def test_valid_body_is_superuser(
|
||||
self, test_app_client: httpx.AsyncClient, event_handler
|
||||
):
|
||||
json = {
|
||||
"email": "lancelot@camelot.bt",
|
||||
"password": "guinevere",
|
||||
"is_superuser": True,
|
||||
}
|
||||
response = await test_app_client.post("/register", json=json)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert event_handler.called is True
|
||||
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["is_superuser"] is False
|
||||
|
||||
async def test_valid_body_is_active(
|
||||
self, test_app_client: httpx.AsyncClient, event_handler
|
||||
):
|
||||
json = {
|
||||
"email": "lancelot@camelot.bt",
|
||||
"password": "guinevere",
|
||||
"is_active": False,
|
||||
}
|
||||
response = await test_app_client.post("/register", json=json)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert event_handler.called is True
|
||||
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["is_active"] is True
|
||||
|
||||
|
||||
@pytest.mark.router
|
||||
@pytest.mark.parametrize("path", ["/login/mock", "/login/mock-bis"])
|
||||
@pytest.mark.asyncio
|
||||
class TestLogin:
|
||||
async def test_empty_body(self, path, test_app_client: httpx.AsyncClient):
|
||||
response = await test_app_client.post(path, data={})
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
async def test_missing_username(self, path, test_app_client: httpx.AsyncClient):
|
||||
data = {"password": "guinevere"}
|
||||
response = await test_app_client.post(path, data=data)
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
async def test_missing_password(self, path, test_app_client: httpx.AsyncClient):
|
||||
data = {"username": "king.arthur@camelot.bt"}
|
||||
response = await test_app_client.post(path, data=data)
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
async def test_not_existing_user(self, path, test_app_client: httpx.AsyncClient):
|
||||
data = {"username": "lancelot@camelot.bt", "password": "guinevere"}
|
||||
response = await test_app_client.post(path, data=data)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["detail"] == ErrorCode.LOGIN_BAD_CREDENTIALS
|
||||
|
||||
async def test_wrong_password(self, path, test_app_client: httpx.AsyncClient):
|
||||
data = {"username": "king.arthur@camelot.bt", "password": "percival"}
|
||||
response = await test_app_client.post(path, data=data)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["detail"] == ErrorCode.LOGIN_BAD_CREDENTIALS
|
||||
|
||||
async def test_valid_credentials(
|
||||
self, path, test_app_client: httpx.AsyncClient, user: UserDB
|
||||
):
|
||||
data = {"username": "king.arthur@camelot.bt", "password": "guinevere"}
|
||||
response = await test_app_client.post(path, data=data)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == {"token": str(user.id)}
|
||||
|
||||
async def test_inactive_user(self, path, test_app_client: httpx.AsyncClient):
|
||||
data = {"username": "percival@camelot.bt", "password": "angharad"}
|
||||
response = await test_app_client.post(path, data=data)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["detail"] == ErrorCode.LOGIN_BAD_CREDENTIALS
|
||||
|
||||
|
||||
@pytest.mark.router
|
||||
@pytest.mark.parametrize("path", ["/logout/mock", "/logout/mock-bis"])
|
||||
@pytest.mark.asyncio
|
||||
class TestLogout:
|
||||
async def test_missing_token(self, path, test_app_client: httpx.AsyncClient):
|
||||
response = await test_app_client.post(path)
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
async def test_unimplemented_logout(
|
||||
self, mocker, path, test_app_client: httpx.AsyncClient, user: UserDB
|
||||
):
|
||||
get_logout_response_spy = mocker.spy(MockAuthentication, "get_logout_response")
|
||||
response = await test_app_client.post(
|
||||
path, headers={"Authorization": f"Bearer {user.id}"}
|
||||
)
|
||||
assert response.status_code == status.HTTP_202_ACCEPTED
|
||||
|
||||
get_logout_response_spy.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.router
|
||||
@pytest.mark.asyncio
|
||||
class TestForgotPassword:
|
||||
async def test_empty_body(self, test_app_client: httpx.AsyncClient, event_handler):
|
||||
response = await test_app_client.post("/forgot-password", json={})
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
assert event_handler.called is False
|
||||
|
||||
async def test_not_existing_user(
|
||||
self, test_app_client: httpx.AsyncClient, event_handler
|
||||
):
|
||||
json = {"email": "lancelot@camelot.bt"}
|
||||
response = await test_app_client.post("/forgot-password", json=json)
|
||||
assert response.status_code == status.HTTP_202_ACCEPTED
|
||||
assert event_handler.called is False
|
||||
|
||||
async def test_inactive_user(
|
||||
self, test_app_client: httpx.AsyncClient, event_handler
|
||||
):
|
||||
json = {"email": "percival@camelot.bt"}
|
||||
response = await test_app_client.post("/forgot-password", json=json)
|
||||
assert response.status_code == status.HTTP_202_ACCEPTED
|
||||
assert event_handler.called is False
|
||||
|
||||
async def test_existing_user(
|
||||
self, test_app_client: httpx.AsyncClient, event_handler, user
|
||||
):
|
||||
json = {"email": "king.arthur@camelot.bt"}
|
||||
response = await test_app_client.post("/forgot-password", json=json)
|
||||
assert response.status_code == status.HTTP_202_ACCEPTED
|
||||
assert event_handler.called is True
|
||||
|
||||
actual_user = event_handler.call_args[0][0]
|
||||
assert actual_user.id == user.id
|
||||
actual_token = event_handler.call_args[0][1]
|
||||
decoded_token = jwt.decode(
|
||||
actual_token,
|
||||
SECRET,
|
||||
audience="fastapi-users:reset",
|
||||
algorithms=[JWT_ALGORITHM],
|
||||
)
|
||||
assert decoded_token["user_id"] == str(user.id)
|
||||
request = event_handler.call_args[0][2]
|
||||
assert isinstance(request, Request)
|
||||
|
||||
|
||||
@pytest.mark.router
|
||||
@pytest.mark.asyncio
|
||||
class TestResetPassword:
|
||||
async def test_empty_body(self, test_app_client: httpx.AsyncClient):
|
||||
response = await test_app_client.post("/reset-password", json={})
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
async def test_missing_token(self, test_app_client: httpx.AsyncClient):
|
||||
json = {"password": "guinevere"}
|
||||
response = await test_app_client.post("/reset-password", json=json)
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
async def test_missing_password(self, test_app_client: httpx.AsyncClient):
|
||||
json = {"token": "foo"}
|
||||
response = await test_app_client.post("/reset-password", json=json)
|
||||
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
||||
|
||||
async def test_invalid_token(self, test_app_client: httpx.AsyncClient):
|
||||
json = {"token": "foo", "password": "guinevere"}
|
||||
response = await test_app_client.post("/reset-password", json=json)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["detail"] == ErrorCode.RESET_PASSWORD_BAD_TOKEN
|
||||
|
||||
async def test_valid_token_missing_user_id_payload(
|
||||
self,
|
||||
mocker,
|
||||
mock_user_db,
|
||||
test_app_client: httpx.AsyncClient,
|
||||
forgot_password_token,
|
||||
):
|
||||
mocker.spy(mock_user_db, "update")
|
||||
|
||||
json = {"token": forgot_password_token(), "password": "holygrail"}
|
||||
response = await test_app_client.post("/reset-password", json=json)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["detail"] == ErrorCode.RESET_PASSWORD_BAD_TOKEN
|
||||
assert mock_user_db.update.called is False
|
||||
|
||||
async def test_valid_token_invalid_uuid(
|
||||
self,
|
||||
mocker,
|
||||
mock_user_db,
|
||||
test_app_client: httpx.AsyncClient,
|
||||
forgot_password_token,
|
||||
):
|
||||
mocker.spy(mock_user_db, "update")
|
||||
|
||||
json = {"token": forgot_password_token("foo"), "password": "holygrail"}
|
||||
response = await test_app_client.post("/reset-password", json=json)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["detail"] == ErrorCode.RESET_PASSWORD_BAD_TOKEN
|
||||
assert mock_user_db.update.called is False
|
||||
|
||||
async def test_inactive_user(
|
||||
self,
|
||||
mocker,
|
||||
mock_user_db,
|
||||
test_app_client: httpx.AsyncClient,
|
||||
forgot_password_token,
|
||||
inactive_user: UserDB,
|
||||
):
|
||||
mocker.spy(mock_user_db, "update")
|
||||
|
||||
json = {
|
||||
"token": forgot_password_token(inactive_user.id),
|
||||
"password": "holygrail",
|
||||
}
|
||||
response = await test_app_client.post("/reset-password", json=json)
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["detail"] == ErrorCode.RESET_PASSWORD_BAD_TOKEN
|
||||
assert mock_user_db.update.called is False
|
||||
|
||||
async def test_existing_user(
|
||||
self,
|
||||
mocker,
|
||||
mock_user_db,
|
||||
test_app_client: httpx.AsyncClient,
|
||||
forgot_password_token,
|
||||
user: UserDB,
|
||||
):
|
||||
mocker.spy(mock_user_db, "update")
|
||||
current_hashed_passord = user.hashed_password
|
||||
|
||||
json = {"token": forgot_password_token(user.id), "password": "holygrail"}
|
||||
response = await test_app_client.post("/reset-password", json=json)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert mock_user_db.update.called is True
|
||||
|
||||
updated_user = mock_user_db.update.call_args[0][0]
|
||||
assert updated_user.hashed_password != current_hashed_passord
|
||||
|
||||
|
||||
@pytest.mark.router
|
||||
@pytest.mark.asyncio
|
||||
class TestMe:
|
||||
@@ -392,23 +77,23 @@ class TestMe:
|
||||
@pytest.mark.asyncio
|
||||
class TestUpdateMe:
|
||||
async def test_missing_token(
|
||||
self, test_app_client: httpx.AsyncClient, event_handler
|
||||
self, test_app_client: httpx.AsyncClient, after_update
|
||||
):
|
||||
response = await test_app_client.patch("/me")
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
assert event_handler.called is False
|
||||
assert after_update.called is False
|
||||
|
||||
async def test_inactive_user(
|
||||
self, test_app_client: httpx.AsyncClient, inactive_user: UserDB, event_handler
|
||||
self, test_app_client: httpx.AsyncClient, inactive_user: UserDB, after_update
|
||||
):
|
||||
response = await test_app_client.patch(
|
||||
"/me", headers={"Authorization": f"Bearer {inactive_user.id}"}
|
||||
)
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
assert event_handler.called is False
|
||||
assert after_update.called is False
|
||||
|
||||
async def test_empty_body(
|
||||
self, test_app_client: httpx.AsyncClient, user: UserDB, event_handler
|
||||
self, test_app_client: httpx.AsyncClient, user: UserDB, after_update
|
||||
):
|
||||
response = await test_app_client.patch(
|
||||
"/me", json={}, headers={"Authorization": f"Bearer {user.id}"}
|
||||
@@ -418,16 +103,16 @@ class TestUpdateMe:
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["email"] == user.email
|
||||
|
||||
assert event_handler.called is True
|
||||
actual_user = event_handler.call_args[0][0]
|
||||
assert after_update.called is True
|
||||
actual_user = after_update.call_args[0][0]
|
||||
assert actual_user.id == user.id
|
||||
updated_fields = event_handler.call_args[0][1]
|
||||
updated_fields = after_update.call_args[0][1]
|
||||
assert updated_fields == {}
|
||||
request = event_handler.call_args[0][2]
|
||||
request = after_update.call_args[0][2]
|
||||
assert isinstance(request, Request)
|
||||
|
||||
async def test_valid_body(
|
||||
self, test_app_client: httpx.AsyncClient, user: UserDB, event_handler
|
||||
self, test_app_client: httpx.AsyncClient, user: UserDB, after_update
|
||||
):
|
||||
json = {"email": "king.arthur@tintagel.bt"}
|
||||
response = await test_app_client.patch(
|
||||
@@ -438,16 +123,16 @@ class TestUpdateMe:
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["email"] == "king.arthur@tintagel.bt"
|
||||
|
||||
assert event_handler.called is True
|
||||
actual_user = event_handler.call_args[0][0]
|
||||
assert after_update.called is True
|
||||
actual_user = after_update.call_args[0][0]
|
||||
assert actual_user.id == user.id
|
||||
updated_fields = event_handler.call_args[0][1]
|
||||
updated_fields = after_update.call_args[0][1]
|
||||
assert updated_fields == {"email": "king.arthur@tintagel.bt"}
|
||||
request = event_handler.call_args[0][2]
|
||||
request = after_update.call_args[0][2]
|
||||
assert isinstance(request, Request)
|
||||
|
||||
async def test_valid_body_is_superuser(
|
||||
self, test_app_client: httpx.AsyncClient, user: UserDB, event_handler
|
||||
self, test_app_client: httpx.AsyncClient, user: UserDB, after_update
|
||||
):
|
||||
json = {"is_superuser": True}
|
||||
response = await test_app_client.patch(
|
||||
@@ -458,16 +143,16 @@ class TestUpdateMe:
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["is_superuser"] is False
|
||||
|
||||
assert event_handler.called is True
|
||||
actual_user = event_handler.call_args[0][0]
|
||||
assert after_update.called is True
|
||||
actual_user = after_update.call_args[0][0]
|
||||
assert actual_user.id == user.id
|
||||
updated_fields = event_handler.call_args[0][1]
|
||||
updated_fields = after_update.call_args[0][1]
|
||||
assert updated_fields == {}
|
||||
request = event_handler.call_args[0][2]
|
||||
request = after_update.call_args[0][2]
|
||||
assert isinstance(request, Request)
|
||||
|
||||
async def test_valid_body_is_active(
|
||||
self, test_app_client: httpx.AsyncClient, user: UserDB, event_handler
|
||||
self, test_app_client: httpx.AsyncClient, user: UserDB, after_update
|
||||
):
|
||||
json = {"is_active": False}
|
||||
response = await test_app_client.patch(
|
||||
@@ -478,12 +163,12 @@ class TestUpdateMe:
|
||||
data = cast(Dict[str, Any], response.json())
|
||||
assert data["is_active"] is True
|
||||
|
||||
assert event_handler.called is True
|
||||
actual_user = event_handler.call_args[0][0]
|
||||
assert after_update.called is True
|
||||
actual_user = after_update.call_args[0][0]
|
||||
assert actual_user.id == user.id
|
||||
updated_fields = event_handler.call_args[0][1]
|
||||
updated_fields = after_update.call_args[0][1]
|
||||
assert updated_fields == {}
|
||||
request = event_handler.call_args[0][2]
|
||||
request = after_update.call_args[0][2]
|
||||
assert isinstance(request, Request)
|
||||
|
||||
async def test_valid_body_password(
|
||||
@@ -492,7 +177,7 @@ class TestUpdateMe:
|
||||
mock_user_db,
|
||||
test_app_client: httpx.AsyncClient,
|
||||
user: UserDB,
|
||||
event_handler,
|
||||
after_update,
|
||||
):
|
||||
mocker.spy(mock_user_db, "update")
|
||||
current_hashed_passord = user.hashed_password
|
||||
@@ -507,12 +192,12 @@ class TestUpdateMe:
|
||||
updated_user = mock_user_db.update.call_args[0][0]
|
||||
assert updated_user.hashed_password != current_hashed_passord
|
||||
|
||||
assert event_handler.called is True
|
||||
actual_user = event_handler.call_args[0][0]
|
||||
assert after_update.called is True
|
||||
actual_user = after_update.call_args[0][0]
|
||||
assert actual_user.id == user.id
|
||||
updated_fields = event_handler.call_args[0][1]
|
||||
updated_fields = after_update.call_args[0][1]
|
||||
assert updated_fields == {"password": "merlin"}
|
||||
request = event_handler.call_args[0][2]
|
||||
request = after_update.call_args[0][2]
|
||||
assert isinstance(request, Request)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user