diff --git a/Pipfile b/Pipfile index ff7e55e0..2f1c5694 100644 --- a/Pipfile +++ b/Pipfile @@ -27,6 +27,7 @@ bumpversion = "*" httpx-oauth = "*" httpx = "*" asgi_lifespan = "*" +uvicorn = "*" [packages] fastapi = ">=0.54.0,<0.55.0" diff --git a/Pipfile.lock b/Pipfile.lock index 2fa03c05..cfd07858 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "a48a987916237d8921c987fc4212dfc0f32a71235f5cbce8fe078e9e3b311be6" + "sha256": "0867361a489e0c0b3a9267aa3e8a7cb89e378db6e0e0d6c831d454982f8272d8" }, "pipfile-spec": 6, "requires": { @@ -500,6 +500,24 @@ ], "version": "==2020.4.24" }, + "httptools": { + "hashes": [ + "sha256:0a4b1b2012b28e68306575ad14ad5e9120b34fccd02a81eb08838d7e3bbb48be", + "sha256:3592e854424ec94bd17dc3e0c96a64e459ec4147e6d53c0a42d0ebcef9cb9c5d", + "sha256:41b573cf33f64a8f8f3400d0a7faf48e1888582b6f6e02b82b9bd4f0bf7497ce", + "sha256:56b6393c6ac7abe632f2294da53f30d279130a92e8ae39d8d14ee2e1b05ad1f2", + "sha256:86c6acd66765a934e8730bf0e9dfaac6fdcf2a4334212bd4a0a1c78f16475ca6", + "sha256:96da81e1992be8ac2fd5597bf0283d832287e20cb3cfde8996d2b00356d4e17f", + "sha256:96eb359252aeed57ea5c7b3d79839aaa0382c9d3149f7d24dd7172b1bcecb009", + "sha256:a2719e1d7a84bb131c4f1e0cb79705034b48de6ae486eb5297a139d6a3296dce", + "sha256:ac0aa11e99454b6a66989aa2d44bca41d4e0f968e395a0a8f164b401fefe359a", + "sha256:bc3114b9edbca5a1eb7ae7db698c669eb53eb8afbbebdde116c174925260849c", + "sha256:fa3cd71e31436911a44620473e873a256851e1f53dee56669dae403ba41756a4", + "sha256:fea04e126014169384dee76a153d4573d90d0cbd1d12185da089f73c78390437" + ], + "markers": "sys_platform != 'win32' and sys_platform != 'cygwin' and platform_python_implementation != 'PyPy'", + "version": "==0.1.1" + }, "httpx": { "hashes": [ "sha256:405b4749f597b1f45cae5bffc17b23dc251cce30a0c4c8126f1007b9e728a615", @@ -971,6 +989,29 @@ ], "version": "==1.25.9" }, + "uvicorn": { + "hashes": [ + "sha256:0f58170165c4495f563d8224b2f415a0829af0412baa034d6f777904613087fd", + "sha256:6fdaf8e53bf1b2ddf0fe9ed06079b5348d7d1d87b3365fe2549e6de0d49e631c" + ], + "index": "pypi", + "version": "==0.11.3" + }, + "uvloop": { + "hashes": [ + "sha256:08b109f0213af392150e2fe6f81d33261bb5ce968a288eb698aad4f46eb711bd", + "sha256:123ac9c0c7dd71464f58f1b4ee0bbd81285d96cdda8bc3519281b8973e3a461e", + "sha256:4315d2ec3ca393dd5bc0b0089d23101276778c304d42faff5dc4579cb6caef09", + "sha256:4544dcf77d74f3a84f03dd6278174575c44c67d7165d4c42c71db3fdc3860726", + "sha256:afd5513c0ae414ec71d24f6f123614a80f3d27ca655a4fcf6cabe50994cc1891", + "sha256:b4f591aa4b3fa7f32fb51e2ee9fea1b495eb75b0b3c8d0ca52514ad675ae63f7", + "sha256:bcac356d62edd330080aed082e78d4b580ff260a677508718f88016333e2c9c5", + "sha256:e7514d7a48c063226b7d06617cbb12a14278d4323a065a8d46a7962686ce2e95", + "sha256:f07909cd9fc08c52d294b1570bba92186181ca01fe3dc9ffba68955273dd7362" + ], + "markers": "sys_platform != 'win32' and sys_platform != 'cygwin' and platform_python_implementation != 'PyPy'", + "version": "==0.14.0" + }, "wcwidth": { "hashes": [ "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1", @@ -978,6 +1019,33 @@ ], "version": "==0.1.9" }, + "websockets": { + "hashes": [ + "sha256:0e4fb4de42701340bd2353bb2eee45314651caa6ccee80dbd5f5d5978888fed5", + "sha256:1d3f1bf059d04a4e0eb4985a887d49195e15ebabc42364f4eb564b1d065793f5", + "sha256:20891f0dddade307ffddf593c733a3fdb6b83e6f9eef85908113e628fa5a8308", + "sha256:295359a2cc78736737dd88c343cd0747546b2174b5e1adc223824bcaf3e164cb", + "sha256:2db62a9142e88535038a6bcfea70ef9447696ea77891aebb730a333a51ed559a", + "sha256:3762791ab8b38948f0c4d281c8b2ddfa99b7e510e46bd8dfa942a5fff621068c", + "sha256:3db87421956f1b0779a7564915875ba774295cc86e81bc671631379371af1170", + "sha256:3ef56fcc7b1ff90de46ccd5a687bbd13a3180132268c4254fc0fa44ecf4fc422", + "sha256:4f9f7d28ce1d8f1295717c2c25b732c2bc0645db3215cf757551c392177d7cb8", + "sha256:5c01fd846263a75bc8a2b9542606927cfad57e7282965d96b93c387622487485", + "sha256:5c65d2da8c6bce0fca2528f69f44b2f977e06954c8512a952222cea50dad430f", + "sha256:751a556205d8245ff94aeef23546a1113b1dd4f6e4d102ded66c39b99c2ce6c8", + "sha256:7ff46d441db78241f4c6c27b3868c9ae71473fe03341340d2dfdbe8d79310acc", + "sha256:965889d9f0e2a75edd81a07592d0ced54daa5b0785f57dc429c378edbcffe779", + "sha256:9b248ba3dd8a03b1a10b19efe7d4f7fa41d158fdaa95e2cf65af5a7b95a4f989", + "sha256:9bef37ee224e104a413f0780e29adb3e514a5b698aabe0d969a6ba426b8435d1", + "sha256:c1ec8db4fac31850286b7cd3b9c0e1b944204668b8eb721674916d4e28744092", + "sha256:c8a116feafdb1f84607cb3b14aa1418424ae71fee131642fc568d21423b51824", + "sha256:ce85b06a10fc65e6143518b96d3dca27b081a740bae261c2fb20375801a9d56d", + "sha256:d705f8aeecdf3262379644e4b55107a3b55860eb812b673b28d0fbc347a60c55", + "sha256:e898a0863421650f0bebac8ba40840fc02258ef4714cb7e1fd76b6a6354bda36", + "sha256:f8a7bff6e8664afc4e6c28b983845c5bc14965030e3fb98789734d416af77c4b" + ], + "version": "==8.1" + }, "zipp": { "hashes": [ "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b", diff --git a/docs/usage/flow.md b/docs/usage/flow.md new file mode 100644 index 00000000..bf976b53 --- /dev/null +++ b/docs/usage/flow.md @@ -0,0 +1,406 @@ +# Flow + +This page will present you a complete registration and authentication flow once you've setup **FastAPI Users**. Each example will be presented with a `cURL` and an `axios` example. + +## 1. Registration + +First step, of course, is to register as a user. + +### Request + +=== "cURL" + ``` bash + curl \ + -H "Content-Type: application/json" \ + -X POST \ + -d "{\"email\": \"king.arthur@camelot.bt\",\"password\": \"guinevere\"}" \ + http://localhost:9000/users/register + ``` + +=== "axios" + ```ts + axios.post('http://localhost:9000/users/register', { + email: 'king.arthur@camelot.bt', + password: 'guinevere', + }) + .then((response) => console.log(response)) + .catch((error) => console.log(error)); + ``` + +### Response + +You'll get a JSON response looking like this: + +```json +{ + "id": "4fd3477b-eccf-4ee3-8f7d-68ad72261476", + "email": "king.arthur@camelot.bt", + "is_active": true, + "is_superuser": false +} +``` + +!!! info + Several things to bear in mind: + + * If you have defined other required fields in your `User` model (like a first name or a birthdate), you'll have to provide them in the payload. + * The user is active by default. + * The user cannot set `is_active` or `is_superuser` itself at registration. Only a superuser can do it by PATCHing the user. + +## 2. Login + +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. + +### JWT backend + +#### Request + +=== "cURL" + ``` bash + curl \ + -H "Content-Type: multipart/form-data" \ + -X POST \ + -F "username=king.arthur@camelot.bt" \ + -F "password=guinevere" \ + http://localhost:9000/users/login/jwt + ``` + +=== "axios" + ```ts + const formData = new FormData(); + formData.set('username', 'king.arthur@camelot.bt'); + formData.set('password', 'guinevere'); + axios.post( + 'http://localhost:9000/users/login/jwt', + formData, + { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }, + ) + .then((response) => console.log(response)) + .catch((error) => console.log(error)); + ``` + +!!! warning + Notice that we don't send it as a JSON payload here but with **form data** instead. Also, the email is provided by a field named **`username`**. + +#### Response + +You'll get a JSON response looking like this: + +```json +{ + "token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiNGZkMzQ3N2ItZWNjZi00ZWUzLThmN2QtNjhhZDcyMjYxNDc2IiwiYXVkIjoiZmFzdGFwaS11c2VyczphdXRoIiwiZXhwIjoxNTg3ODE4NDI5fQ.anO3JR8-WYCozZ4_2-PQ2Ov9O38RaLP2RAzQIiZhteM" +} +``` + +You can use this token to make authenticated requests as the user `king.arthur@camelot.bt`. We'll see how in the next section. + +### Cookie backend + +#### Request + +=== "cURL" + ``` bash + curl \ + -v \ + -H "Content-Type: multipart/form-data" \ + -X POST \ + -F "username=king.arthur@camelot.bt" \ + -F "password=guinevere" \ + http://localhost:9000/users/login/cookie + ``` + +=== "axios" + ```ts + const formData = new FormData(); + formData.set('username', 'king.arthur@camelot.bt'); + formData.set('password', 'guinevere'); + axios.post( + 'http://localhost:9000/users/login/cookie', + formData, + { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }, + ) + .then((response) => console.log(response)) + .catch((error) => console.log(error)); + ``` + +!!! warning + Notice that we don't send it as a JSON payload here but with **form data** instead. Also, the email is provided by a field named **`username`**. + +#### Response + +You'll get a empty response. However, the response will come with a `Set-Cookie` header (that's why we added the `-v` option in `cURL` to see them). + +``` +set-cookie: fastapiusersauth=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiYzYwNjBmMTEtNTM0OS00YTI0LThiNGEtYTJhODc1ZGM1Mzk1IiwiYXVkIjoiZmFzdGFwaS11c2VyczphdXRoIiwiZXhwIjoxNTg3ODE4OTQ3fQ.qNA4oPVYhoqrJIk-zvAyEfEVoEnP156G30H_SWEU0sU; HttpOnly; Max-Age=3600; Path=/; Secure +``` + +You can make authenticated requests as the user `king.arthur@camelot.bt` by setting a `Cookie` header with this cookie. + +!!! tip + The cookie backend is more suited for browsers, as they handle them automatically. This means that if you make a login request in the browser, it will automatically store the cookie and automatically send it in subsequent requests. + +## 3. Get my profile + +Now that we can authenticate, we can get our own profile data. Depending on your [authentication backend](../configuration/authentication/index.md), the method to authenticate the request will vary. We'll stick with JWT from now on. + +### Request + +=== "cURL" + ``` bash + export TOKEN="eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiNGZkMzQ3N2ItZWNjZi00ZWUzLThmN2QtNjhhZDcyMjYxNDc2IiwiYXVkIjoiZmFzdGFwaS11c2VyczphdXRoIiwiZXhwIjoxNTg3ODE4NDI5fQ.anO3JR8-WYCozZ4_2-PQ2Ov9O38RaLP2RAzQIiZhteM"; + curl \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -X GET \ + http://localhost:9000/users/me + ``` + +=== "axios" + ```ts + const TOKEN = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiNGZkMzQ3N2ItZWNjZi00ZWUzLThmN2QtNjhhZDcyMjYxNDc2IiwiYXVkIjoiZmFzdGFwaS11c2VyczphdXRoIiwiZXhwIjoxNTg3ODE4NDI5fQ.anO3JR8-WYCozZ4_2-PQ2Ov9O38RaLP2RAzQIiZhteM'; + axios.get( + 'http://localhost:9000/users/me', { + headers: { + 'Authorization': `Bearer ${TOKEN}`, + }, + }) + .then((response) => console.log(response)) + .catch((error) => console.log(error)); + ``` + +### Response + +You'll get a JSON response looking like this: + +```json +{ + "id": "4fd3477b-eccf-4ee3-8f7d-68ad72261476", + "email": "king.arthur@camelot.bt", + "is_active": true, + "is_superuser": false +} +``` + +!!! tip + If you use one of the [dependency callable](./dependency-callables.md) to protect one of your own endpoint, you'll have to authenticate exactly in the same way. + +## 4. Update my profile + +We can also update our own profile. For example, we can change our password like this. + +### Request + +=== "cURL" + ``` bash + curl \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -X PATCH \ + -d "{\"password\": \"lancelot\"}" \ + http://localhost:9000/users/me + ``` + +=== "axios" + ```ts + axios.patch( + 'http://localhost:9000/users/me', + { + password: 'lancelot', + }, + { + headers: { + 'Authorization': `Bearer ${TOKEN}`, + }, + }, + ) + .then((response) => console.log(response)) + .catch((error) => console.log(error)); + ``` + +### Response + +You'll get a JSON response looking like this: + +```json +{ + "id": "4fd3477b-eccf-4ee3-8f7d-68ad72261476", + "email": "king.arthur@camelot.bt", + "is_active": true, + "is_superuser": false +} +``` + +!!! info + Once again, the user cannot set `is_active` or `is_superuser` itself. Only a superuser can do it by PATCHing the user. + +## 5. Become a superuser πŸ¦ΈπŸ»β€β™‚οΈ + +If you want to manage the users of your application, you'll have to become a **superuser**. + +The very first superuser can only be set at **database level**: open it through a CLI or a GUI, find your user and set the `is_superuser` column/property to `true`. + +### 5.1. Get the profile of any user + +Now that you are a superuser, you can leverage the power of [superuser routes](./routes.md#superuser). You can for example get the profile of any user in the database given its id. + +#### Request + +=== "cURL" + ``` bash + curl \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -X GET \ + http://localhost:9000/users/4fd3477b-eccf-4ee3-8f7d-68ad72261476 + ``` + +=== "axios" + ```ts + axios.get( + 'http://localhost:9000/users/4fd3477b-eccf-4ee3-8f7d-68ad72261476', { + headers: { + 'Authorization': `Bearer ${TOKEN}`, + }, + }) + .then((response) => console.log(response)) + .catch((error) => console.log(error)); + ``` + +#### Response + +You'll get a JSON response looking like this: + +```json +{ + "id": "4fd3477b-eccf-4ee3-8f7d-68ad72261476", + "email": "king.arthur@camelot.bt", + "is_active": true, + "is_superuser": false +} +``` + +### 5.1. Update any user + +We can now update the profile of any user. For example, we can promote it as superuser. + +#### Request + +=== "cURL" + ``` bash + curl \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -X PATCH \ + -d "{\"is_superuser\": true}" \ + http://localhost:9000/users/4fd3477b-eccf-4ee3-8f7d-68ad72261476 + ``` + +=== "axios" + ```ts + axios.patch( + 'http://localhost:9000/users/4fd3477b-eccf-4ee3-8f7d-68ad72261476', + { + is_superuser: true, + }, + { + headers: { + 'Authorization': `Bearer ${TOKEN}`, + }, + }, + ) + .then((response) => console.log(response)) + .catch((error) => console.log(error)); + ``` + +#### Response + +You'll get a JSON response looking like this: + +```json +{ + "id": "4fd3477b-eccf-4ee3-8f7d-68ad72261476", + "email": "king.arthur@camelot.bt", + "is_active": true, + "is_superuser": true +} +``` + +### 5.2. Delete any user + +Finally, we can delete a user. + +#### Request + +=== "cURL" + ``` bash + curl \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -X DELETE \ + http://localhost:9000/users/4fd3477b-eccf-4ee3-8f7d-68ad72261476 + ``` + +=== "axios" + ```ts + axios.delete( + 'http://localhost:9000/users/4fd3477b-eccf-4ee3-8f7d-68ad72261476', + { + headers: { + 'Authorization': `Bearer ${TOKEN}`, + }, + }, + ) + .then((response) => console.log(response)) + .catch((error) => console.log(error)); + ``` + +#### Response + +You'll get an empty response. + +## 6. Logout + +We can also end the session. Note that it doesn't apply to every [authentication backends](../configuration/authentication/index.md). For JWT, it doesn't make sense to end the session, the token is valid until it expires. However, for Cookie backend, the server will clear the cookie. + +### Request + +=== "cURL" + ``` bash + curl \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -X POST \ + http://localhost:9000/users/logout/cookie + ``` + +=== "axios" + ```ts + axios.post('http://localhost:9000/users/logout/cookie', + null, + { + headers: { + 'Authorization': `Bearer ${TOKEN}`, + }, + } + ) + .then((response) => console.log(response)) + .catch((error) => console.log(error)); + ``` + +### Response + +You'll get an empty response. + +## Conclusion + +That's it! You now have a good overview of how you can manage the users through the API. Be sure to check the [Routes](./routes.md) page to have all the details about each endpoints. diff --git a/mkdocs.yml b/mkdocs.yml index 01149f68..32ce019d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -23,6 +23,7 @@ markdown_extensions: - codehilite - pymdownx.superfences - pymdownx.tasklist + - pymdownx.tabbed nav: - About: index.md @@ -41,5 +42,6 @@ nav: - configuration/full_example.md - configuration/oauth.md - Usage: + - usage/flow.md - usage/routes.md - usage/dependency-callables.md