mirror of
https://github.com/kevin-DL/full-stack-fastapi-postgresql.git
synced 2026-01-13 18:45:27 +00:00
Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a25f2c14e4 | ||
|
|
122d983415 | ||
|
|
d08d9314ce | ||
|
|
ff55b778ba | ||
|
|
8812ca6635 | ||
|
|
2e8da3a590 | ||
|
|
00297f974f | ||
|
|
c8bcc0ba0a | ||
|
|
0a194b3b00 | ||
|
|
94b2474438 | ||
|
|
af4e0cfe10 | ||
|
|
001dbda103 | ||
|
|
34f6f9ae54 | ||
|
|
0c8e682a90 | ||
|
|
67b384f308 | ||
|
|
4bd791c11d | ||
|
|
697b4da6b0 | ||
|
|
854cc709d1 | ||
|
|
21c4d11659 | ||
|
|
bcee2427b9 | ||
|
|
8a2252f654 | ||
|
|
8ff61e813e | ||
|
|
fb874fea35 | ||
|
|
2b9ed9333a | ||
|
|
45510b4f80 | ||
|
|
5a79f4e427 | ||
|
|
79631c7619 | ||
|
|
cd875e5bef | ||
|
|
1a92a0a6f1 | ||
|
|
2eb5b030bd | ||
|
|
7c2c2276d9 | ||
|
|
baf584a6cd | ||
|
|
970a182ec8 | ||
|
|
1d8678235d | ||
|
|
71f430616c | ||
|
|
43e508239c | ||
|
|
dc712ac4ec | ||
|
|
141f6cdb6e | ||
|
|
fc403c9bc1 | ||
|
|
4b93dc709f | ||
|
|
2db416d3c1 | ||
|
|
ab46165387 | ||
|
|
1c975c7f2d | ||
|
|
248ea56c6e |
19
.github/workflows/main.yml
vendored
Normal file
19
.github/workflows/main.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 0 * * *"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
issue-manager:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: tiangolo/issue-manager@master
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
config: >
|
||||||
|
{
|
||||||
|
"answered": {
|
||||||
|
"users": ["tiangolo"],
|
||||||
|
"delay": 864000,
|
||||||
|
"message": "Assuming the original issue was solved, it will be automatically closed now. But feel free to add more comments or create new issues."
|
||||||
|
}
|
||||||
|
}
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
.vscode
|
.vscode
|
||||||
testing-project
|
testing-project
|
||||||
.mypy_cache
|
.mypy_cache
|
||||||
|
poetry.lock
|
||||||
|
|||||||
42
README.md
42
README.md
@@ -27,23 +27,23 @@ Generate a backend and frontend stack using Python, including interactive API do
|
|||||||
|
|
||||||
* Full **Docker** integration (Docker based).
|
* Full **Docker** integration (Docker based).
|
||||||
* Docker Swarm Mode deployment.
|
* Docker Swarm Mode deployment.
|
||||||
* **Docker Compose** integration and optimization for local development
|
* **Docker Compose** integration and optimization for local development.
|
||||||
* **Production ready** Python web server using Uvicorn and Gunicorn.
|
* **Production ready** Python web server using Uvicorn and Gunicorn.
|
||||||
* Python **[FastAPI](https://github.com/tiangolo/fastapi)** backend:
|
* Python <a href="https://github.com/tiangolo/fastapi" class="external-link" target="_blank">**FastAPI**</a> backend:
|
||||||
* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic).
|
* **Fast**: Very high performance, on par with **NodeJS** and **Go** (thanks to Starlette and Pydantic).
|
||||||
* **Intuitive**: Great editor support. <abbr title="also known as auto-complete, autocompletion, IntelliSense">Completion</abbr> everywhere. Less time debugging.
|
* **Intuitive**: Great editor support. <abbr title="also known as auto-complete, autocompletion, IntelliSense">Completion</abbr> everywhere. Less time debugging.
|
||||||
* **Easy**: Designed to be easy to use and learn. Less time reading docs.
|
* **Easy**: Designed to be easy to use and learn. Less time reading docs.
|
||||||
* **Short**: Minimize code duplication. Multiple features from each parameter declaration.
|
* **Short**: Minimize code duplication. Multiple features from each parameter declaration.
|
||||||
* **Robust**: Get production-ready code. With automatic interactive documentation.
|
* **Robust**: Get production-ready code. With automatic interactive documentation.
|
||||||
* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: <a href="https://github.com/OAI/OpenAPI-Specification" target="_blank">OpenAPI</a> and <a href="http://json-schema.org/" target="_blank">JSON Schema</a>.
|
* **Standards-based**: Based on (and fully compatible with) the open standards for APIs: <a href="https://github.com/OAI/OpenAPI-Specification" class="external-link" target="_blank">OpenAPI</a> and <a href="http://json-schema.org/" class="external-link" target="_blank">JSON Schema</a>.
|
||||||
* [**Many other features**](https://github.com/tiangolo/fastapi) including automatic validation, serialization, interactive documentation, authentication with OAuth2 JWT tokens, etc.
|
* <a href="https://fastapi.tiangolo.com/features/" class="external-link" target="_blank">**Many other features**</a> including automatic validation, serialization, interactive documentation, authentication with OAuth2 JWT tokens, etc.
|
||||||
* **Secure password** hashing by default.
|
* **Secure password** hashing by default.
|
||||||
* **JWT token** authentication.
|
* **JWT token** authentication.
|
||||||
* **SQLAlchemy** models (independent of Flask extensions, so they can be used with Celery workers directly).
|
* **SQLAlchemy** models (independent of Flask extensions, so they can be used with Celery workers directly).
|
||||||
* Basic starting models for users (modify and remove as you need).
|
* Basic starting models for users (modify and remove as you need).
|
||||||
* **Alembic** migrations.
|
* **Alembic** migrations.
|
||||||
* **CORS** (Cross Origin Resource Sharing).
|
* **CORS** (Cross Origin Resource Sharing).
|
||||||
* **Celery** worker that can import and use models and code from the rest of the backend selectively (you don't have to install the complete app in each worker).
|
* **Celery** worker that can import and use models and code from the rest of the backend selectively.
|
||||||
* REST backend tests based on **Pytest**, integrated with Docker, so you can test the full API interaction, independent on the database. As it runs in Docker, it can build a new data store from scratch each time (so you can use ElasticSearch, MongoDB, CouchDB, or whatever you want, and just test that the API works).
|
* REST backend tests based on **Pytest**, integrated with Docker, so you can test the full API interaction, independent on the database. As it runs in Docker, it can build a new data store from scratch each time (so you can use ElasticSearch, MongoDB, CouchDB, or whatever you want, and just test that the API works).
|
||||||
* Easy Python integration with **Jupyter Kernels** for remote or in-Docker development with extensions like Atom Hydrogen or Visual Studio Code Jupyter.
|
* Easy Python integration with **Jupyter Kernels** for remote or in-Docker development with extensions like Atom Hydrogen or Visual Studio Code Jupyter.
|
||||||
* **Vue** frontend:
|
* **Vue** frontend:
|
||||||
@@ -69,7 +69,7 @@ Generate a backend and frontend stack using Python, including interactive API do
|
|||||||
|
|
||||||
## How to use it
|
## How to use it
|
||||||
|
|
||||||
Go to the directoy where you want to create your project and run:
|
Go to the directory where you want to create your project and run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install cookiecutter
|
pip install cookiecutter
|
||||||
@@ -105,7 +105,7 @@ The input variables, with their default values (some auto generated) are:
|
|||||||
* `secret_key`: Backend server secret key. Use the method above to generate it.
|
* `secret_key`: Backend server secret key. Use the method above to generate it.
|
||||||
* `first_superuser`: The first superuser generated, with it you will be able to create more users, etc. By default, based on the domain.
|
* `first_superuser`: The first superuser generated, with it you will be able to create more users, etc. By default, based on the domain.
|
||||||
* `first_superuser_password`: First superuser password. Use the method above to generate it.
|
* `first_superuser_password`: First superuser password. Use the method above to generate it.
|
||||||
* `backend_cors_origins`: Origins (domains, more or less) that are enabled for CORS (Cross Origin Resource Sharing). This allows a frontend in one domain (e.g. `https://dashboard.example.com`) to communicate with this backend, that could be living in another domain (e.g. `https://api.example.com`). It can also be used to allow your local frontend (with a custom `hosts` domain mapping, as described in the project's `README.md`) that could be living in `http://dev.example.com:8080` to cummunicate with the backend at `https://stag.example.com`. Notice the `http` vs `https` and the `dev.` prefix for local development vs the "staging" `stag.` prefix. By default, it includes origins for production, staging and development, with ports commonly used during local development by several popular frontend frameworks (Vue with `:8080`, React, Angular).
|
* `backend_cors_origins`: Origins (domains, more or less) that are enabled for CORS (Cross Origin Resource Sharing). This allows a frontend in one domain (e.g. `https://dashboard.example.com`) to communicate with this backend, that could be living in another domain (e.g. `https://api.example.com`). It can also be used to allow your local frontend (with a custom `hosts` domain mapping, as described in the project's `README.md`) that could be living in `http://dev.example.com:8080` to communicate with the backend at `https://stag.example.com`. Notice the `http` vs `https` and the `dev.` prefix for local development vs the "staging" `stag.` prefix. By default, it includes origins for production, staging and development, with ports commonly used during local development by several popular frontend frameworks (Vue with `:8080`, React, Angular).
|
||||||
* `smtp_port`: Port to use to send emails via SMTP. By default `587`.
|
* `smtp_port`: Port to use to send emails via SMTP. By default `587`.
|
||||||
* `smtp_host`: Host to use to send emails, it would be given by your email provider, like Mailgun, Sparkpost, etc.
|
* `smtp_host`: Host to use to send emails, it would be given by your email provider, like Mailgun, Sparkpost, etc.
|
||||||
* `smtp_user`: The user to use in the SMTP connection. The value will be given by your email provider.
|
* `smtp_user`: The user to use in the SMTP connection. The value will be given by your email provider.
|
||||||
@@ -118,7 +118,6 @@ The input variables, with their default values (some auto generated) are:
|
|||||||
|
|
||||||
* `traefik_constraint_tag`: The tag to be used by the internal Traefik load balancer (for example, to divide requests between backend and frontend) for production. Used to separate this stack from any other stack you might have. This should identify each stack in each environment (production, staging, etc).
|
* `traefik_constraint_tag`: The tag to be used by the internal Traefik load balancer (for example, to divide requests between backend and frontend) for production. Used to separate this stack from any other stack you might have. This should identify each stack in each environment (production, staging, etc).
|
||||||
* `traefik_constraint_tag_staging`: The Traefik tag to be used while on staging.
|
* `traefik_constraint_tag_staging`: The Traefik tag to be used while on staging.
|
||||||
* `traefik_public_network`: This assumes you have another separate publicly facing Traefik at the server / cluster level. This is the network that main Traefik lives in.
|
|
||||||
* `traefik_public_constraint_tag`: The tag that should be used by stack services that should communicate with the public.
|
* `traefik_public_constraint_tag`: The tag that should be used by stack services that should communicate with the public.
|
||||||
|
|
||||||
* `flower_auth`: Basic HTTP authentication for flower, in the form`user:password`. By default: "`root:changethis`".
|
* `flower_auth`: Basic HTTP authentication for flower, in the form`user:password`. By default: "`root:changethis`".
|
||||||
@@ -146,7 +145,32 @@ After using this generator, your new project (the directory created) will contai
|
|||||||
|
|
||||||
## Release Notes
|
## Release Notes
|
||||||
|
|
||||||
### Next release
|
### Latest Changes
|
||||||
|
|
||||||
|
### 0.5.0
|
||||||
|
|
||||||
|
* Make the Traefik public network a fixed default of `traefik-public` as done in DockerSwarm.rocks, to simplify development and iteration of the project generator. PR [#150](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/150).
|
||||||
|
* Update to PostgreSQL 12. PR [#148](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/148). by [@RCheese](https://github.com/RCheese).
|
||||||
|
* Use Poetry for package management. Initial PR [#144](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/144) by [@RCheese](https://github.com/RCheese).
|
||||||
|
* Fix Windows line endings for shell scripts after project generation with Cookiecutter hooks. PR [#149](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/149).
|
||||||
|
* Upgrade Vue CLI to version 4. PR [#120](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/120) by [@br3ndonland](https://github.com/br3ndonland).
|
||||||
|
* Remove duplicate `login` tag. PR [#135](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/135) by [@Nonameentered](https://github.com/Nonameentered).
|
||||||
|
* Fix showing email in dashboard when there's no user's full name. PR [#129](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/129) by [@rlonka](https://github.com/rlonka).
|
||||||
|
* Format code with Black and Flake8. PR [#121](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/121) by [@br3ndonland](https://github.com/br3ndonland).
|
||||||
|
* Simplify SQLAlchemy Base class. PR [#117](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/117) by [@airibarne](https://github.com/airibarne).
|
||||||
|
* Update CRUD utils for users, handling password hashing. PR [#106](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/106) by [@mocsar](https://github.com/mocsar).
|
||||||
|
* Use `.` instead of `source` for interoperability. PR [#98](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/98) by [@gucharbon](https://github.com/gucharbon).
|
||||||
|
* Use Pydantic's `BaseSettings` for settings/configs and env vars. PR [#87](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/87) by [@StephenBrown2](https://github.com/StephenBrown2).
|
||||||
|
* Remove `package-lock.json` to let everyone lock their own versions (depending on OS, etc).
|
||||||
|
* Simplify Traefik service labels PR [#139](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/139).
|
||||||
|
* Add email validation. PR [#40](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/40) by [@kedod](https://github.com/kedod).
|
||||||
|
* Fix typo in README. PR [#83](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/83) by [@ashears](https://github.com/ashears).
|
||||||
|
* Fix typo in README. PR [#80](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/80) by [@abjoker](https://github.com/abjoker).
|
||||||
|
* Fix function name `read_item` and response code. PR [#74](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/74) by [@jcaguirre89](https://github.com/jcaguirre89).
|
||||||
|
* Fix typo in comment. PR [#70](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/70) by [@daniel-butler](https://github.com/daniel-butler).
|
||||||
|
* Fix Flower Docker configuration. PR [#37](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/37) by [@dmontagu](https://github.com/dmontagu).
|
||||||
|
* Add new CRUD utils based on DB and Pydantic models. Initial PR [#23](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/23) by [@ebreton](https://github.com/ebreton).
|
||||||
|
* Add normal user testing Pytest fixture. PR [#20](https://github.com/tiangolo/full-stack-fastapi-postgresql/pull/20) by [@ebreton](https://github.com/ebreton).
|
||||||
|
|
||||||
### 0.4.0
|
### 0.4.0
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"secret_key": "changethis",
|
"secret_key": "changethis",
|
||||||
"first_superuser": "admin@{{cookiecutter.domain_main}}",
|
"first_superuser": "admin@{{cookiecutter.domain_main}}",
|
||||||
"first_superuser_password": "changethis",
|
"first_superuser_password": "changethis",
|
||||||
"backend_cors_origins": "http://localhost, http://localhost:4200, http://localhost:3000, http://localhost:8080, https://localhost, https://localhost:4200, https://localhost:3000, https://localhost:8080, http://dev.{{cookiecutter.domain_main}}, https://{{cookiecutter.domain_staging}}, https://{{cookiecutter.domain_main}}, http://local.dockertoolbox.tiangolo.com, http://localhost.tiangolo.com",
|
"backend_cors_origins": "[\"http://localhost\", \"http://localhost:4200\", \"http://localhost:3000\", \"http://localhost:8080\", \"https://localhost\", \"https://localhost:4200\", \"https://localhost:3000\", \"https://localhost:8080\", \"http://dev.{{cookiecutter.domain_main}}\", \"https://{{cookiecutter.domain_staging}}\", \"https://{{cookiecutter.domain_main}}\", \"http://local.dockertoolbox.tiangolo.com\", \"http://localhost.tiangolo.com\"]",
|
||||||
"smtp_port": "587",
|
"smtp_port": "587",
|
||||||
"smtp_host": "",
|
"smtp_host": "",
|
||||||
"smtp_user": "",
|
"smtp_user": "",
|
||||||
@@ -23,7 +23,6 @@
|
|||||||
|
|
||||||
"traefik_constraint_tag": "{{cookiecutter.domain_main}}",
|
"traefik_constraint_tag": "{{cookiecutter.domain_main}}",
|
||||||
"traefik_constraint_tag_staging": "{{cookiecutter.domain_staging}}",
|
"traefik_constraint_tag_staging": "{{cookiecutter.domain_staging}}",
|
||||||
"traefik_public_network": "traefik-public",
|
|
||||||
"traefik_public_constraint_tag": "traefik-public",
|
"traefik_public_constraint_tag": "traefik-public",
|
||||||
|
|
||||||
"flower_auth": "admin:{{cookiecutter.first_superuser_password}}",
|
"flower_auth": "admin:{{cookiecutter.first_superuser_password}}",
|
||||||
|
|||||||
8
hooks/post_gen_project.py
Normal file
8
hooks/post_gen_project.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
path: Path
|
||||||
|
for path in Path(".").glob("**/*.sh"):
|
||||||
|
data = path.read_bytes()
|
||||||
|
lf_data = data.replace(b"\r\n", b"\n")
|
||||||
|
path.write_bytes(lf_data)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
rm -rf \{\{cookiecutter.project_slug\}\}/.git
|
rm -rf \{\{cookiecutter.project_slug\}\}/.git
|
||||||
rm -rf \{\{cookiecutter.project_slug\}\}/backend/app/Pipfile.lock
|
rm -rf \{\{cookiecutter.project_slug\}\}/backend/app/poetry.lock
|
||||||
rm -rf \{\{cookiecutter.project_slug\}\}/frontend/node_modules
|
rm -rf \{\{cookiecutter.project_slug\}\}/frontend/node_modules
|
||||||
rm -rf \{\{cookiecutter.project_slug\}\}/frontend/dist
|
rm -rf \{\{cookiecutter.project_slug\}\}/frontend/dist
|
||||||
git checkout \{\{cookiecutter.project_slug\}\}/README.md
|
git checkout \{\{cookiecutter.project_slug\}\}/README.md
|
||||||
|
|||||||
2
test.sh
2
test.sh
@@ -9,6 +9,6 @@ cookiecutter --config-file ./testing-config.yml --no-input -f ./
|
|||||||
|
|
||||||
cd ./testing-project
|
cd ./testing-project
|
||||||
|
|
||||||
bash ./scripts/test.sh
|
bash ./scripts/test.sh "$@"
|
||||||
|
|
||||||
cd ../
|
cd ../
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ DOMAIN=localhost
|
|||||||
# DOMAIN=localhost.tiangolo.com
|
# DOMAIN=localhost.tiangolo.com
|
||||||
# DOMAIN=dev.{{cookiecutter.domain_main}}
|
# DOMAIN=dev.{{cookiecutter.domain_main}}
|
||||||
|
|
||||||
|
TRAEFIK_PUBLIC_NETWORK=traefik-public
|
||||||
TRAEFIK_TAG={{cookiecutter.traefik_constraint_tag}}
|
TRAEFIK_TAG={{cookiecutter.traefik_constraint_tag}}
|
||||||
TRAEFIK_PUBLIC_NETWORK={{cookiecutter.traefik_public_network}}
|
|
||||||
TRAEFIK_PUBLIC_TAG={{cookiecutter.traefik_public_constraint_tag}}
|
TRAEFIK_PUBLIC_TAG={{cookiecutter.traefik_public_constraint_tag}}
|
||||||
|
|
||||||
DOCKER_IMAGE_BACKEND={{cookiecutter.docker_image_backend}}
|
DOCKER_IMAGE_BACKEND={{cookiecutter.docker_image_backend}}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ If your Docker is not running in `localhost` (the URLs above wouldn't work) chec
|
|||||||
|
|
||||||
Open your editor at `./backend/app/` (instead of the project root: `./`), so that you see an `./app/` directory with your code inside. That way, your editor will be able to find all the imports, etc.
|
Open your editor at `./backend/app/` (instead of the project root: `./`), so that you see an `./app/` directory with your code inside. That way, your editor will be able to find all the imports, etc.
|
||||||
|
|
||||||
Modify or add SQLAlchemy models in `./backend/app/app/db_models/`, Pydantic models in `./backend/app/app/models/`, API endpoints in `./backend/app/app/api/`, CRUD (Create, Read, Update, Delete) utils in `./backend/app/app/crud/`. The easiest might be to copy the ones for Items (models, endpoints, and CRUD utils) and update them to your needs.
|
Modify or add SQLAlchemy models in `./backend/app/app/models/`, Pydantic schemas in `./backend/app/app/schemas/`, API endpoints in `./backend/app/app/api/`, CRUD (Create, Read, Update, Delete) utils in `./backend/app/app/crud/`. The easiest might be to copy the ones for Items (models, endpoints, and CRUD utils) and update them to your needs.
|
||||||
|
|
||||||
Add and modify tasks to the Celery worker in `./backend/app/app/worker.py`.
|
Add and modify tasks to the Celery worker in `./backend/app/app/worker.py`.
|
||||||
|
|
||||||
@@ -134,6 +134,20 @@ If you need to install any additional package for the tests, add it to the file
|
|||||||
|
|
||||||
If you use GitLab CI the tests will run automatically.
|
If you use GitLab CI the tests will run automatically.
|
||||||
|
|
||||||
|
#### Local tests
|
||||||
|
|
||||||
|
Start the stack with this command:
|
||||||
|
|
||||||
|
```Bash
|
||||||
|
DOMAIN=backend sh ./scripts/test-local.sh
|
||||||
|
```
|
||||||
|
The `./backend/app` directory is mounted as a "host volume" inside the docker container (set in the file `docker-compose.dev.volumes.yml`).
|
||||||
|
You can rerun the test on live code:
|
||||||
|
|
||||||
|
```Bash
|
||||||
|
docker-compose exec backend-tests /tests-start.sh
|
||||||
|
```
|
||||||
|
|
||||||
#### Test running stack
|
#### Test running stack
|
||||||
|
|
||||||
If your stack is already up and you just want to run the tests, you can use:
|
If your stack is already up and you just want to run the tests, you can use:
|
||||||
@@ -205,7 +219,7 @@ Make sure you create a "revision" of your models and that you "upgrade" your dat
|
|||||||
docker-compose exec backend bash
|
docker-compose exec backend bash
|
||||||
```
|
```
|
||||||
|
|
||||||
* If you created a new model in `./backend/app/app/db_models/`, make sure to import it in `./backend/app/app/db/base.py`, that Python module (`base.py`) that imports all the models will be used by Alembic.
|
* If you created a new model in `./backend/app/app/models/`, make sure to import it in `./backend/app/app/db/base.py`, that Python module (`base.py`) that imports all the models will be used by Alembic.
|
||||||
|
|
||||||
* After changing a model (for example, adding a column), inside the container, create a revision, e.g.:
|
* After changing a model (for example, adding a column), inside the container, create a revision, e.g.:
|
||||||
|
|
||||||
@@ -380,6 +394,24 @@ And you can use CI (continuous integration) systems to do it automatically.
|
|||||||
|
|
||||||
But you have to configure a couple things first.
|
But you have to configure a couple things first.
|
||||||
|
|
||||||
|
### Traefik network
|
||||||
|
|
||||||
|
This stack expects the public Traefik network to be named `traefik-public`, just as in the tutorial in <a href="https://dockerswarm.rocks" class="external-link" target="_blank">DockerSwarm.rocks</a>.
|
||||||
|
|
||||||
|
If you need to use a different Traefik public network name, update it in the `docker-compose.yml` files, in the section:
|
||||||
|
|
||||||
|
```YAML
|
||||||
|
networks:
|
||||||
|
traefik-public:
|
||||||
|
external: true
|
||||||
|
```
|
||||||
|
|
||||||
|
Change `traefik-public` to the name of the used Traefik network. And then update it in the file `.env`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
TRAEFIK_PUBLIC_NETWORK=traefik-public
|
||||||
|
```
|
||||||
|
|
||||||
### Persisting Docker named volumes
|
### Persisting Docker named volumes
|
||||||
|
|
||||||
You need to make sure that each service (Docker container) that uses a volume is always deployed to the same Docker "node" in the cluster, that way it will preserve the data. Otherwise, it could be deployed to a different node each time, and each time the volume would be created in that new node before starting the service. As a result, it would look like your service was starting from scratch every time, losing all the previous data.
|
You need to make sure that each service (Docker container) that uses a volume is always deployed to the same Docker "node" in the cluster, that way it will preserve the data. Otherwise, it could be deployed to a different node each time, and each time the volume would be created in that new node before starting the service. As a result, it would look like your service was starting from scratch every time, losing all the previous data.
|
||||||
@@ -388,7 +420,6 @@ That's specially important for a service running a database. But the same proble
|
|||||||
|
|
||||||
To solve that, you can put constraints in the services that use one or more data volumes (like databases) to make them be deployed to a Docker node with a specific label. And of course, you need to have that label assigned to one (only one) of your nodes.
|
To solve that, you can put constraints in the services that use one or more data volumes (like databases) to make them be deployed to a Docker node with a specific label. And of course, you need to have that label assigned to one (only one) of your nodes.
|
||||||
|
|
||||||
|
|
||||||
#### Adding services with volumes
|
#### Adding services with volumes
|
||||||
|
|
||||||
For each service that uses a volume (databases, services with uploaded files, etc) you should have a label constraint in your `docker-compose.deploy.volumes-placement.yml` file.
|
For each service that uses a volume (databases, services with uploaded files, etc) you should have a label constraint in your `docker-compose.deploy.volumes-placement.yml` file.
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
__pycache__
|
__pycache__
|
||||||
|
app.egg-info
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
[[source]]
|
|
||||||
name = "pypi"
|
|
||||||
url = "https://pypi.org/simple"
|
|
||||||
verify_ssl = true
|
|
||||||
|
|
||||||
[dev-packages]
|
|
||||||
mypy = "*"
|
|
||||||
black = "*"
|
|
||||||
jupyter = "*"
|
|
||||||
isort = "*"
|
|
||||||
autoflake = "*"
|
|
||||||
flake8 = "*"
|
|
||||||
pytest = "*"
|
|
||||||
vulture = "*"
|
|
||||||
|
|
||||||
[packages]
|
|
||||||
fastapi = "*"
|
|
||||||
uvicorn = "*"
|
|
||||||
pyjwt = "*"
|
|
||||||
python-multipart = "*"
|
|
||||||
email-validator = "*"
|
|
||||||
requests = "*"
|
|
||||||
celery = "*"
|
|
||||||
passlib = {extras = ["bcrypt"],version = "*"}
|
|
||||||
tenacity = "*"
|
|
||||||
pydantic = "*"
|
|
||||||
emails = "*"
|
|
||||||
raven = "*"
|
|
||||||
gunicorn = "*"
|
|
||||||
jinja2 = "*"
|
|
||||||
psycopg2-binary = "*"
|
|
||||||
alembic = "*"
|
|
||||||
sqlalchemy = "*"
|
|
||||||
|
|
||||||
[requires]
|
|
||||||
python_version = "3.6"
|
|
||||||
|
|
||||||
[pipenv]
|
|
||||||
allow_prereleases = true
|
|
||||||
@@ -67,11 +67,9 @@ def run_migrations_online():
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
configuration = config.get_section(config.config_ini_section)
|
configuration = config.get_section(config.config_ini_section)
|
||||||
configuration['sqlalchemy.url'] = get_url()
|
configuration["sqlalchemy.url"] = get_url()
|
||||||
connectable = engine_from_config(
|
connectable = engine_from_config(
|
||||||
configuration,
|
configuration, prefix="sqlalchemy.", poolclass=pool.NullPool,
|
||||||
prefix="sqlalchemy.",
|
|
||||||
poolclass=pool.NullPool,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
with connectable.connect() as connection:
|
with connectable.connect() as connection:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""First revision
|
"""First revision
|
||||||
|
|
||||||
Revision ID: d4867f3a4c0a
|
Revision ID: d4867f3a4c0a
|
||||||
Revises:
|
Revises:
|
||||||
Create Date: 2019-04-17 13:53:32.978401
|
Create Date: 2019-04-17 13:53:32.978401
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -10,7 +10,7 @@ import sqlalchemy as sa
|
|||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision = 'd4867f3a4c0a'
|
revision = "d4867f3a4c0a"
|
||||||
down_revision = None
|
down_revision = None
|
||||||
branch_labels = None
|
branch_labels = None
|
||||||
depends_on = None
|
depends_on = None
|
||||||
@@ -18,40 +18,42 @@ depends_on = None
|
|||||||
|
|
||||||
def upgrade():
|
def upgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.create_table('user',
|
op.create_table(
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
"user",
|
||||||
sa.Column('full_name', sa.String(), nullable=True),
|
sa.Column("id", sa.Integer(), nullable=False),
|
||||||
sa.Column('email', sa.String(), nullable=True),
|
sa.Column("full_name", sa.String(), nullable=True),
|
||||||
sa.Column('hashed_password', sa.String(), nullable=True),
|
sa.Column("email", sa.String(), nullable=True),
|
||||||
sa.Column('is_active', sa.Boolean(), nullable=True),
|
sa.Column("hashed_password", sa.String(), nullable=True),
|
||||||
sa.Column('is_superuser', sa.Boolean(), nullable=True),
|
sa.Column("is_active", sa.Boolean(), nullable=True),
|
||||||
sa.PrimaryKeyConstraint('id')
|
sa.Column("is_superuser", sa.Boolean(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
)
|
)
|
||||||
op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=True)
|
op.create_index(op.f("ix_user_email"), "user", ["email"], unique=True)
|
||||||
op.create_index(op.f('ix_user_full_name'), 'user', ['full_name'], unique=False)
|
op.create_index(op.f("ix_user_full_name"), "user", ["full_name"], unique=False)
|
||||||
op.create_index(op.f('ix_user_id'), 'user', ['id'], unique=False)
|
op.create_index(op.f("ix_user_id"), "user", ["id"], unique=False)
|
||||||
op.create_table('item',
|
op.create_table(
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
"item",
|
||||||
sa.Column('title', sa.String(), nullable=True),
|
sa.Column("id", sa.Integer(), nullable=False),
|
||||||
sa.Column('description', sa.String(), nullable=True),
|
sa.Column("title", sa.String(), nullable=True),
|
||||||
sa.Column('owner_id', sa.Integer(), nullable=True),
|
sa.Column("description", sa.String(), nullable=True),
|
||||||
sa.ForeignKeyConstraint(['owner_id'], ['user.id'], ),
|
sa.Column("owner_id", sa.Integer(), nullable=True),
|
||||||
sa.PrimaryKeyConstraint('id')
|
sa.ForeignKeyConstraint(["owner_id"], ["user.id"],),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
)
|
)
|
||||||
op.create_index(op.f('ix_item_description'), 'item', ['description'], unique=False)
|
op.create_index(op.f("ix_item_description"), "item", ["description"], unique=False)
|
||||||
op.create_index(op.f('ix_item_id'), 'item', ['id'], unique=False)
|
op.create_index(op.f("ix_item_id"), "item", ["id"], unique=False)
|
||||||
op.create_index(op.f('ix_item_title'), 'item', ['title'], unique=False)
|
op.create_index(op.f("ix_item_title"), "item", ["title"], unique=False)
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
def downgrade():
|
def downgrade():
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.drop_index(op.f('ix_item_title'), table_name='item')
|
op.drop_index(op.f("ix_item_title"), table_name="item")
|
||||||
op.drop_index(op.f('ix_item_id'), table_name='item')
|
op.drop_index(op.f("ix_item_id"), table_name="item")
|
||||||
op.drop_index(op.f('ix_item_description'), table_name='item')
|
op.drop_index(op.f("ix_item_description"), table_name="item")
|
||||||
op.drop_table('item')
|
op.drop_table("item")
|
||||||
op.drop_index(op.f('ix_user_id'), table_name='user')
|
op.drop_index(op.f("ix_user_id"), table_name="user")
|
||||||
op.drop_index(op.f('ix_user_full_name'), table_name='user')
|
op.drop_index(op.f("ix_user_full_name"), table_name="user")
|
||||||
op.drop_index(op.f('ix_user_email'), table_name='user')
|
op.drop_index(op.f("ix_user_email"), table_name="user")
|
||||||
op.drop_table('user')
|
op.drop_table("user")
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ from sqlalchemy.orm import Session
|
|||||||
from app import crud
|
from app import crud
|
||||||
from app.api.utils.db import get_db
|
from app.api.utils.db import get_db
|
||||||
from app.api.utils.security import get_current_active_user
|
from app.api.utils.security import get_current_active_user
|
||||||
from app.db_models.user import User as DBUser
|
from app.models.user import User as DBUser
|
||||||
from app.models.item import Item, ItemCreate, ItemUpdate
|
from app.schemas.item import Item, ItemCreate, ItemUpdate
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -41,7 +41,9 @@ def create_item(
|
|||||||
"""
|
"""
|
||||||
Create new item.
|
Create new item.
|
||||||
"""
|
"""
|
||||||
item = crud.item.create(db_session=db, item_in=item_in, owner_id=current_user.id)
|
item = crud.item.create_with_owner(
|
||||||
|
db_session=db, obj_in=item_in, owner_id=current_user.id
|
||||||
|
)
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
@@ -61,12 +63,12 @@ def update_item(
|
|||||||
raise HTTPException(status_code=404, detail="Item not found")
|
raise HTTPException(status_code=404, detail="Item not found")
|
||||||
if not crud.user.is_superuser(current_user) and (item.owner_id != current_user.id):
|
if not crud.user.is_superuser(current_user) and (item.owner_id != current_user.id):
|
||||||
raise HTTPException(status_code=400, detail="Not enough permissions")
|
raise HTTPException(status_code=400, detail="Not enough permissions")
|
||||||
item = crud.item.update(db_session=db, item=item, item_in=item_in)
|
item = crud.item.update(db_session=db, db_obj=item, obj_in=item_in)
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{id}", response_model=Item)
|
@router.get("/{id}", response_model=Item)
|
||||||
def read_user_me(
|
def read_item(
|
||||||
*,
|
*,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
id: int,
|
id: int,
|
||||||
@@ -77,7 +79,7 @@ def read_user_me(
|
|||||||
"""
|
"""
|
||||||
item = crud.item.get(db_session=db, id=id)
|
item = crud.item.get(db_session=db, id=id)
|
||||||
if not item:
|
if not item:
|
||||||
raise HTTPException(status_code=400, detail="Item not found")
|
raise HTTPException(status_code=404, detail="Item not found")
|
||||||
if not crud.user.is_superuser(current_user) and (item.owner_id != current_user.id):
|
if not crud.user.is_superuser(current_user) and (item.owner_id != current_user.id):
|
||||||
raise HTTPException(status_code=400, detail="Not enough permissions")
|
raise HTTPException(status_code=400, detail="Not enough permissions")
|
||||||
return item
|
return item
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ from sqlalchemy.orm import Session
|
|||||||
from app import crud
|
from app import crud
|
||||||
from app.api.utils.db import get_db
|
from app.api.utils.db import get_db
|
||||||
from app.api.utils.security import get_current_user
|
from app.api.utils.security import get_current_user
|
||||||
from app.core import config
|
from app.core.config import settings
|
||||||
from app.core.jwt import create_access_token
|
from app.core.jwt import create_access_token
|
||||||
from app.core.security import get_password_hash
|
from app.core.security import get_password_hash
|
||||||
from app.db_models.user import User as DBUser
|
from app.models.user import User as DBUser
|
||||||
from app.models.msg import Msg
|
from app.schemas.msg import Msg
|
||||||
from app.models.token import Token
|
from app.schemas.token import Token
|
||||||
from app.models.user import User
|
from app.schemas.user import User
|
||||||
from app.utils import (
|
from app.utils import (
|
||||||
generate_password_reset_token,
|
generate_password_reset_token,
|
||||||
send_reset_password_email,
|
send_reset_password_email,
|
||||||
@@ -23,7 +23,7 @@ from app.utils import (
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.post("/login/access-token", response_model=Token, tags=["login"])
|
@router.post("/login/access-token", response_model=Token)
|
||||||
def login_access_token(
|
def login_access_token(
|
||||||
db: Session = Depends(get_db), form_data: OAuth2PasswordRequestForm = Depends()
|
db: Session = Depends(get_db), form_data: OAuth2PasswordRequestForm = Depends()
|
||||||
):
|
):
|
||||||
@@ -37,7 +37,7 @@ def login_access_token(
|
|||||||
raise HTTPException(status_code=400, detail="Incorrect email or password")
|
raise HTTPException(status_code=400, detail="Incorrect email or password")
|
||||||
elif not crud.user.is_active(user):
|
elif not crud.user.is_active(user):
|
||||||
raise HTTPException(status_code=400, detail="Inactive user")
|
raise HTTPException(status_code=400, detail="Inactive user")
|
||||||
access_token_expires = timedelta(minutes=config.ACCESS_TOKEN_EXPIRE_MINUTES)
|
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||||
return {
|
return {
|
||||||
"access_token": create_access_token(
|
"access_token": create_access_token(
|
||||||
data={"user_id": user.id}, expires_delta=access_token_expires
|
data={"user_id": user.id}, expires_delta=access_token_expires
|
||||||
@@ -46,7 +46,7 @@ def login_access_token(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/login/test-token", tags=["login"], response_model=User)
|
@router.post("/login/test-token", response_model=User)
|
||||||
def test_token(current_user: DBUser = Depends(get_current_user)):
|
def test_token(current_user: DBUser = Depends(get_current_user)):
|
||||||
"""
|
"""
|
||||||
Test access token
|
Test access token
|
||||||
@@ -54,7 +54,7 @@ def test_token(current_user: DBUser = Depends(get_current_user)):
|
|||||||
return current_user
|
return current_user
|
||||||
|
|
||||||
|
|
||||||
@router.post("/password-recovery/{email}", tags=["login"], response_model=Msg)
|
@router.post("/password-recovery/{email}", response_model=Msg)
|
||||||
def recover_password(email: str, db: Session = Depends(get_db)):
|
def recover_password(email: str, db: Session = Depends(get_db)):
|
||||||
"""
|
"""
|
||||||
Password Recovery
|
Password Recovery
|
||||||
@@ -73,8 +73,10 @@ def recover_password(email: str, db: Session = Depends(get_db)):
|
|||||||
return {"msg": "Password recovery email sent"}
|
return {"msg": "Password recovery email sent"}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/reset-password/", tags=["login"], response_model=Msg)
|
@router.post("/reset-password/", response_model=Msg)
|
||||||
def reset_password(token: str = Body(...), new_password: str = Body(...), db: Session = Depends(get_db)):
|
def reset_password(
|
||||||
|
token: str = Body(...), new_password: str = Body(...), db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Reset password
|
Reset password
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -2,15 +2,15 @@ from typing import List
|
|||||||
|
|
||||||
from fastapi import APIRouter, Body, Depends, HTTPException
|
from fastapi import APIRouter, Body, Depends, HTTPException
|
||||||
from fastapi.encoders import jsonable_encoder
|
from fastapi.encoders import jsonable_encoder
|
||||||
from pydantic.types import EmailStr
|
from pydantic.networks import EmailStr
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app import crud
|
from app import crud
|
||||||
from app.api.utils.db import get_db
|
from app.api.utils.db import get_db
|
||||||
from app.api.utils.security import get_current_active_superuser, get_current_active_user
|
from app.api.utils.security import get_current_active_superuser, get_current_active_user
|
||||||
from app.core import config
|
from app.core.config import settings
|
||||||
from app.db_models.user import User as DBUser
|
from app.models.user import User as DBUser
|
||||||
from app.models.user import User, UserCreate, UserInDB, UserUpdate
|
from app.schemas.user import User, UserCreate, UserUpdate
|
||||||
from app.utils import send_new_account_email
|
from app.utils import send_new_account_email
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
@@ -46,8 +46,8 @@ def create_user(
|
|||||||
status_code=400,
|
status_code=400,
|
||||||
detail="The user with this username already exists in the system.",
|
detail="The user with this username already exists in the system.",
|
||||||
)
|
)
|
||||||
user = crud.user.create(db, user_in=user_in)
|
user = crud.user.create(db, obj_in=user_in)
|
||||||
if config.EMAILS_ENABLED and user_in.email:
|
if settings.EMAILS_ENABLED and user_in.email:
|
||||||
send_new_account_email(
|
send_new_account_email(
|
||||||
email_to=user_in.email, username=user_in.email, password=user_in.password
|
email_to=user_in.email, username=user_in.email, password=user_in.password
|
||||||
)
|
)
|
||||||
@@ -74,7 +74,7 @@ def update_user_me(
|
|||||||
user_in.full_name = full_name
|
user_in.full_name = full_name
|
||||||
if email is not None:
|
if email is not None:
|
||||||
user_in.email = email
|
user_in.email = email
|
||||||
user = crud.user.update(db, user=current_user, user_in=user_in)
|
user = crud.user.update(db, db_obj=current_user, obj_in=user_in)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
@@ -100,10 +100,10 @@ def create_user_open(
|
|||||||
"""
|
"""
|
||||||
Create new user without the need to be logged in.
|
Create new user without the need to be logged in.
|
||||||
"""
|
"""
|
||||||
if not config.USERS_OPEN_REGISTRATION:
|
if not settings.USERS_OPEN_REGISTRATION:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=403,
|
status_code=403,
|
||||||
detail="Open user resgistration is forbidden on this server",
|
detail="Open user registration is forbidden on this server",
|
||||||
)
|
)
|
||||||
user = crud.user.get_by_email(db, email=email)
|
user = crud.user.get_by_email(db, email=email)
|
||||||
if user:
|
if user:
|
||||||
@@ -112,7 +112,7 @@ def create_user_open(
|
|||||||
detail="The user with this username already exists in the system",
|
detail="The user with this username already exists in the system",
|
||||||
)
|
)
|
||||||
user_in = UserCreate(password=password, email=email, full_name=full_name)
|
user_in = UserCreate(password=password, email=email, full_name=full_name)
|
||||||
user = crud.user.create(db, user_in=user_in)
|
user = crud.user.create(db, obj_in=user_in)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@ def read_user_by_id(
|
|||||||
"""
|
"""
|
||||||
Get a specific user by id.
|
Get a specific user by id.
|
||||||
"""
|
"""
|
||||||
user = crud.user.get(db, user_id=user_id)
|
user = crud.user.get(db, id=user_id)
|
||||||
if user == current_user:
|
if user == current_user:
|
||||||
return user
|
return user
|
||||||
if not crud.user.is_superuser(current_user):
|
if not crud.user.is_superuser(current_user):
|
||||||
@@ -141,16 +141,16 @@ def update_user(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
user_id: int,
|
user_id: int,
|
||||||
user_in: UserUpdate,
|
user_in: UserUpdate,
|
||||||
current_user: UserInDB = Depends(get_current_active_superuser),
|
current_user: DBUser = Depends(get_current_active_superuser),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Update a user.
|
Update a user.
|
||||||
"""
|
"""
|
||||||
user = crud.user.get(db, user_id=user_id)
|
user = crud.user.get(db, id=user_id)
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=404,
|
status_code=404,
|
||||||
detail="The user with this username does not exist in the system",
|
detail="The user with this username does not exist in the system",
|
||||||
)
|
)
|
||||||
user = crud.user.update(db, user=user, user_in=user_in)
|
user = crud.user.update(db, db_obj=user, obj_in=user_in)
|
||||||
return user
|
return user
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
from pydantic.types import EmailStr
|
from pydantic.networks import EmailStr
|
||||||
|
|
||||||
from app.api.utils.security import get_current_active_superuser
|
from app.api.utils.security import get_current_active_superuser
|
||||||
from app.core.celery_app import celery_app
|
from app.core.celery_app import celery_app
|
||||||
from app.models.msg import Msg
|
from app.schemas.msg import Msg
|
||||||
from app.models.user import UserInDB
|
from app.schemas.user import User # noqa: F401
|
||||||
|
from app.models.user import User as DBUser
|
||||||
from app.utils import send_test_email
|
from app.utils import send_test_email
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.post("/test-celery/", response_model=Msg, status_code=201)
|
@router.post("/test-celery/", response_model=Msg, status_code=201)
|
||||||
def test_celery(
|
def test_celery(msg: Msg, current_user: DBUser = Depends(get_current_active_superuser)):
|
||||||
msg: Msg, current_user: UserInDB = Depends(get_current_active_superuser)
|
|
||||||
):
|
|
||||||
"""
|
"""
|
||||||
Test Celery worker.
|
Test Celery worker.
|
||||||
"""
|
"""
|
||||||
@@ -23,7 +22,7 @@ def test_celery(
|
|||||||
|
|
||||||
@router.post("/test-email/", response_model=Msg, status_code=201)
|
@router.post("/test-email/", response_model=Msg, status_code=201)
|
||||||
def test_email(
|
def test_email(
|
||||||
email_to: EmailStr, current_user: UserInDB = Depends(get_current_active_superuser)
|
email_to: EmailStr, current_user: DBUser = Depends(get_current_active_superuser)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Test emails.
|
Test emails.
|
||||||
|
|||||||
@@ -7,25 +7,25 @@ from starlette.status import HTTP_403_FORBIDDEN
|
|||||||
|
|
||||||
from app import crud
|
from app import crud
|
||||||
from app.api.utils.db import get_db
|
from app.api.utils.db import get_db
|
||||||
from app.core import config
|
from app.core.config import settings
|
||||||
from app.core.jwt import ALGORITHM
|
from app.core.jwt import ALGORITHM
|
||||||
from app.db_models.user import User
|
from app.models.user import User
|
||||||
from app.models.token import TokenPayload
|
from app.schemas.token import TokenPayload
|
||||||
|
|
||||||
reusable_oauth2 = OAuth2PasswordBearer(tokenUrl="/api/v1/login/access-token")
|
reusable_oauth2 = OAuth2PasswordBearer(tokenUrl=f"{settings.API_V1_STR}/login/access-token")
|
||||||
|
|
||||||
|
|
||||||
def get_current_user(
|
def get_current_user(
|
||||||
db: Session = Depends(get_db), token: str = Security(reusable_oauth2)
|
db: Session = Depends(get_db), token: str = Security(reusable_oauth2)
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
payload = jwt.decode(token, config.SECRET_KEY, algorithms=[ALGORITHM])
|
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[ALGORITHM])
|
||||||
token_data = TokenPayload(**payload)
|
token_data = TokenPayload(**payload)
|
||||||
except PyJWTError:
|
except PyJWTError:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTP_403_FORBIDDEN, detail="Could not validate credentials"
|
status_code=HTTP_403_FORBIDDEN, detail="Could not validate credentials"
|
||||||
)
|
)
|
||||||
user = crud.user.get(db, user_id=token_data.user_id)
|
user = crud.user.get(db, id=token_data.user_id)
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
return user
|
return user
|
||||||
|
|||||||
@@ -1,53 +1,92 @@
|
|||||||
import os
|
import secrets
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from pydantic import AnyHttpUrl, BaseSettings, EmailStr, HttpUrl, PostgresDsn, validator
|
||||||
|
|
||||||
|
|
||||||
def getenv_boolean(var_name, default_value=False):
|
class Settings(BaseSettings):
|
||||||
result = default_value
|
|
||||||
env_value = os.getenv(var_name)
|
API_V1_STR: str = "/api/v1"
|
||||||
if env_value is not None:
|
|
||||||
result = env_value.upper() in ("TRUE", "1")
|
SECRET_KEY: str = secrets.token_urlsafe(32)
|
||||||
return result
|
|
||||||
|
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8 # 60 minutes * 24 hours * 8 days = 8 days
|
||||||
|
|
||||||
|
SERVER_NAME: str
|
||||||
|
SERVER_HOST: AnyHttpUrl
|
||||||
|
# BACKEND_CORS_ORIGINS is a JSON-formatted list of origins
|
||||||
|
# e.g: '["http://localhost", "http://localhost:4200", "http://localhost:3000", \
|
||||||
|
# "http://localhost:8080", "http://local.dockertoolbox.tiangolo.com"]'
|
||||||
|
BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = []
|
||||||
|
|
||||||
|
@validator("BACKEND_CORS_ORIGINS", pre=True)
|
||||||
|
def assemble_cors_origins(cls, v):
|
||||||
|
if isinstance(v, str) and not v.startswith("["):
|
||||||
|
return [i.strip() for i in v.split(",")]
|
||||||
|
return v
|
||||||
|
|
||||||
|
PROJECT_NAME: str
|
||||||
|
SENTRY_DSN: HttpUrl = None
|
||||||
|
|
||||||
|
@validator("SENTRY_DSN", pre=True)
|
||||||
|
def sentry_dsn_can_be_blank(cls, v):
|
||||||
|
if len(v) == 0:
|
||||||
|
return None
|
||||||
|
return v
|
||||||
|
|
||||||
|
POSTGRES_SERVER: str
|
||||||
|
POSTGRES_USER: str
|
||||||
|
POSTGRES_PASSWORD: str
|
||||||
|
POSTGRES_DB: str
|
||||||
|
SQLALCHEMY_DATABASE_URI: PostgresDsn = None
|
||||||
|
|
||||||
|
@validator("SQLALCHEMY_DATABASE_URI", pre=True)
|
||||||
|
def assemble_db_connection(cls, v, values):
|
||||||
|
if isinstance(v, str):
|
||||||
|
return v
|
||||||
|
return PostgresDsn.build(
|
||||||
|
scheme="postgresql",
|
||||||
|
user=values.get("POSTGRES_USER"),
|
||||||
|
password=values.get("POSTGRES_PASSWORD"),
|
||||||
|
host=values.get("POSTGRES_SERVER"),
|
||||||
|
path=f"/{values.get('POSTGRES_DB') or ''}",
|
||||||
|
)
|
||||||
|
|
||||||
|
SMTP_TLS: bool = True
|
||||||
|
SMTP_PORT: int = None
|
||||||
|
SMTP_HOST: str = None
|
||||||
|
SMTP_USER: str = None
|
||||||
|
SMTP_PASSWORD: str = None
|
||||||
|
EMAILS_FROM_EMAIL: EmailStr = None
|
||||||
|
EMAILS_FROM_NAME: str = None
|
||||||
|
|
||||||
|
@validator("EMAILS_FROM_NAME")
|
||||||
|
def get_project_name(cls, v, values):
|
||||||
|
if not v:
|
||||||
|
return values["PROJECT_NAME"]
|
||||||
|
return v
|
||||||
|
|
||||||
|
EMAIL_RESET_TOKEN_EXPIRE_HOURS: int = 48
|
||||||
|
EMAIL_TEMPLATES_DIR: str = "/app/app/email-templates/build"
|
||||||
|
EMAILS_ENABLED: bool = False
|
||||||
|
|
||||||
|
@validator("EMAILS_ENABLED", pre=True)
|
||||||
|
def get_emails_enabled(cls, v, values):
|
||||||
|
return bool(
|
||||||
|
values.get("SMTP_HOST")
|
||||||
|
and values.get("SMTP_PORT")
|
||||||
|
and values.get("EMAILS_FROM_EMAIL")
|
||||||
|
)
|
||||||
|
|
||||||
|
EMAIL_TEST_USER: EmailStr = "test@example.com"
|
||||||
|
|
||||||
|
FIRST_SUPERUSER: EmailStr
|
||||||
|
FIRST_SUPERUSER_PASSWORD: str
|
||||||
|
|
||||||
|
USERS_OPEN_REGISTRATION: bool = False
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
case_sensitive = True
|
||||||
|
|
||||||
|
|
||||||
API_V1_STR = "/api/v1"
|
settings = Settings()
|
||||||
|
|
||||||
SECRET_KEY = os.getenvb(b"SECRET_KEY")
|
|
||||||
if not SECRET_KEY:
|
|
||||||
SECRET_KEY = os.urandom(32)
|
|
||||||
|
|
||||||
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 8 # 60 minutes * 24 hours * 8 days = 8 days
|
|
||||||
|
|
||||||
SERVER_NAME = os.getenv("SERVER_NAME")
|
|
||||||
SERVER_HOST = os.getenv("SERVER_HOST")
|
|
||||||
BACKEND_CORS_ORIGINS = os.getenv(
|
|
||||||
"BACKEND_CORS_ORIGINS"
|
|
||||||
) # a string of origins separated by commas, e.g: "http://localhost, http://localhost:4200, http://localhost:3000, http://localhost:8080, http://local.dockertoolbox.tiangolo.com"
|
|
||||||
PROJECT_NAME = os.getenv("PROJECT_NAME")
|
|
||||||
SENTRY_DSN = os.getenv("SENTRY_DSN")
|
|
||||||
|
|
||||||
POSTGRES_SERVER = os.getenv("POSTGRES_SERVER")
|
|
||||||
POSTGRES_USER = os.getenv("POSTGRES_USER")
|
|
||||||
POSTGRES_PASSWORD = os.getenv("POSTGRES_PASSWORD")
|
|
||||||
POSTGRES_DB = os.getenv("POSTGRES_DB")
|
|
||||||
SQLALCHEMY_DATABASE_URI = (
|
|
||||||
f"postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_SERVER}/{POSTGRES_DB}"
|
|
||||||
)
|
|
||||||
|
|
||||||
SMTP_TLS = getenv_boolean("SMTP_TLS", True)
|
|
||||||
SMTP_PORT = None
|
|
||||||
_SMTP_PORT = os.getenv("SMTP_PORT")
|
|
||||||
if _SMTP_PORT is not None:
|
|
||||||
SMTP_PORT = int(_SMTP_PORT)
|
|
||||||
SMTP_HOST = os.getenv("SMTP_HOST")
|
|
||||||
SMTP_USER = os.getenv("SMTP_USER")
|
|
||||||
SMTP_PASSWORD = os.getenv("SMTP_PASSWORD")
|
|
||||||
EMAILS_FROM_EMAIL = os.getenv("EMAILS_FROM_EMAIL")
|
|
||||||
EMAILS_FROM_NAME = PROJECT_NAME
|
|
||||||
EMAIL_RESET_TOKEN_EXPIRE_HOURS = 48
|
|
||||||
EMAIL_TEMPLATES_DIR = "/app/app/email-templates/build"
|
|
||||||
EMAILS_ENABLED = SMTP_HOST and SMTP_PORT and EMAILS_FROM_EMAIL
|
|
||||||
|
|
||||||
FIRST_SUPERUSER = os.getenv("FIRST_SUPERUSER")
|
|
||||||
FIRST_SUPERUSER_PASSWORD = os.getenv("FIRST_SUPERUSER_PASSWORD")
|
|
||||||
|
|
||||||
USERS_OPEN_REGISTRATION = getenv_boolean("USERS_OPEN_REGISTRATION")
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from datetime import datetime, timedelta
|
|||||||
|
|
||||||
import jwt
|
import jwt
|
||||||
|
|
||||||
from app.core import config
|
from app.core.config import settings
|
||||||
|
|
||||||
ALGORITHM = "HS256"
|
ALGORITHM = "HS256"
|
||||||
access_token_jwt_subject = "access"
|
access_token_jwt_subject = "access"
|
||||||
@@ -15,5 +15,5 @@ def create_access_token(*, data: dict, expires_delta: timedelta = None):
|
|||||||
else:
|
else:
|
||||||
expire = datetime.utcnow() + timedelta(minutes=15)
|
expire = datetime.utcnow() + timedelta(minutes=15)
|
||||||
to_encode.update({"exp": expire, "sub": access_token_jwt_subject})
|
to_encode.update({"exp": expire, "sub": access_token_jwt_subject})
|
||||||
encoded_jwt = jwt.encode(to_encode, config.SECRET_KEY, algorithm=ALGORITHM)
|
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM)
|
||||||
return encoded_jwt
|
return encoded_jwt
|
||||||
|
|||||||
@@ -1 +1,10 @@
|
|||||||
from . import item, user
|
from .crud_user import user # noqa: F401
|
||||||
|
from .crud_item import item # noqa: F401
|
||||||
|
|
||||||
|
# For a new basic set of CRUD operations you could just do
|
||||||
|
|
||||||
|
# from .base import CRUDBase
|
||||||
|
# from app.models.item import Item
|
||||||
|
# from app.schemas.item import ItemCreate, ItemUpdate
|
||||||
|
|
||||||
|
# item = CRUDBase[Item, ItemCreate, ItemUpdate](Item)
|
||||||
|
|||||||
57
{{cookiecutter.project_slug}}/backend/app/app/crud/base.py
Normal file
57
{{cookiecutter.project_slug}}/backend/app/app/crud/base.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
from typing import List, Optional, Generic, TypeVar, Type
|
||||||
|
|
||||||
|
from fastapi.encoders import jsonable_encoder
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.db.base_class import Base
|
||||||
|
|
||||||
|
ModelType = TypeVar("ModelType", bound=Base)
|
||||||
|
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
|
||||||
|
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)
|
||||||
|
|
||||||
|
|
||||||
|
class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
|
||||||
|
def __init__(self, model: Type[ModelType]):
|
||||||
|
"""
|
||||||
|
CRUD object with default methods to Create, Read, Update, Delete (CRUD).
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
* `model`: A SQLAlchemy model class
|
||||||
|
* `schema`: A Pydantic model (schema) class
|
||||||
|
"""
|
||||||
|
self.model = model
|
||||||
|
|
||||||
|
def get(self, db_session: Session, id: int) -> Optional[ModelType]:
|
||||||
|
return db_session.query(self.model).filter(self.model.id == id).first()
|
||||||
|
|
||||||
|
def get_multi(self, db_session: Session, *, skip=0, limit=100) -> List[ModelType]:
|
||||||
|
return db_session.query(self.model).offset(skip).limit(limit).all()
|
||||||
|
|
||||||
|
def create(self, db_session: Session, *, obj_in: CreateSchemaType) -> ModelType:
|
||||||
|
obj_in_data = jsonable_encoder(obj_in)
|
||||||
|
db_obj = self.model(**obj_in_data)
|
||||||
|
db_session.add(db_obj)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(db_obj)
|
||||||
|
return db_obj
|
||||||
|
|
||||||
|
def update(
|
||||||
|
self, db_session: Session, *, db_obj: ModelType, obj_in: UpdateSchemaType
|
||||||
|
) -> ModelType:
|
||||||
|
obj_data = jsonable_encoder(db_obj)
|
||||||
|
update_data = obj_in.dict(exclude_unset=True)
|
||||||
|
for field in obj_data:
|
||||||
|
if field in update_data:
|
||||||
|
setattr(db_obj, field, update_data[field])
|
||||||
|
db_session.add(db_obj)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(db_obj)
|
||||||
|
return db_obj
|
||||||
|
|
||||||
|
def remove(self, db_session: Session, *, id: int) -> ModelType:
|
||||||
|
obj = db_session.query(self.model).get(id)
|
||||||
|
db_session.delete(obj)
|
||||||
|
db_session.commit()
|
||||||
|
return obj
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
from typing import List
|
||||||
|
|
||||||
|
from fastapi.encoders import jsonable_encoder
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.models.item import Item
|
||||||
|
from app.schemas.item import ItemCreate, ItemUpdate
|
||||||
|
from app.crud.base import CRUDBase
|
||||||
|
|
||||||
|
|
||||||
|
class CRUDItem(CRUDBase[Item, ItemCreate, ItemUpdate]):
|
||||||
|
def create_with_owner(
|
||||||
|
self, db_session: Session, *, obj_in: ItemCreate, owner_id: int
|
||||||
|
) -> Item:
|
||||||
|
obj_in_data = jsonable_encoder(obj_in)
|
||||||
|
db_obj = self.model(**obj_in_data, owner_id=owner_id)
|
||||||
|
db_session.add(db_obj)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(db_obj)
|
||||||
|
return db_obj
|
||||||
|
|
||||||
|
def get_multi_by_owner(
|
||||||
|
self, db_session: Session, *, owner_id: int, skip=0, limit=100
|
||||||
|
) -> List[Item]:
|
||||||
|
return (
|
||||||
|
db_session.query(self.model)
|
||||||
|
.filter(Item.owner_id == owner_id)
|
||||||
|
.offset(skip)
|
||||||
|
.limit(limit)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
item = CRUDItem(Item)
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.models.user import User
|
||||||
|
from app.schemas.user import UserCreate, UserUpdate, UserInDB
|
||||||
|
from app.core.security import verify_password, get_password_hash
|
||||||
|
from app.crud.base import CRUDBase
|
||||||
|
|
||||||
|
|
||||||
|
class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]):
|
||||||
|
def get_by_email(self, db_session: Session, *, email: str) -> Optional[User]:
|
||||||
|
return db_session.query(User).filter(User.email == email).first()
|
||||||
|
|
||||||
|
def create(self, db_session: Session, *, obj_in: UserCreate) -> User:
|
||||||
|
db_obj = User(
|
||||||
|
email=obj_in.email,
|
||||||
|
hashed_password=get_password_hash(obj_in.password),
|
||||||
|
full_name=obj_in.full_name,
|
||||||
|
is_superuser=obj_in.is_superuser,
|
||||||
|
)
|
||||||
|
db_session.add(db_obj)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(db_obj)
|
||||||
|
return db_obj
|
||||||
|
|
||||||
|
def update(self, db_session: Session, *, db_obj: User, obj_in: UserUpdate) -> User:
|
||||||
|
if obj_in.password:
|
||||||
|
update_data = obj_in.dict(exclude_unset=True)
|
||||||
|
hashed_password = get_password_hash(obj_in.password)
|
||||||
|
del update_data["password"]
|
||||||
|
update_data["hashed_password"] = hashed_password
|
||||||
|
use_obj_in = UserInDB.parse_obj(update_data)
|
||||||
|
return super().update(db_session, db_obj=db_obj, obj_in=use_obj_in)
|
||||||
|
|
||||||
|
def authenticate(
|
||||||
|
self, db_session: Session, *, email: str, password: str
|
||||||
|
) -> Optional[User]:
|
||||||
|
user = self.get_by_email(db_session, email=email)
|
||||||
|
if not user:
|
||||||
|
return None
|
||||||
|
if not verify_password(password, user.hashed_password):
|
||||||
|
return None
|
||||||
|
return user
|
||||||
|
|
||||||
|
def is_active(self, user: User) -> bool:
|
||||||
|
return user.is_active
|
||||||
|
|
||||||
|
def is_superuser(self, user: User) -> bool:
|
||||||
|
return user.is_superuser
|
||||||
|
|
||||||
|
|
||||||
|
user = CRUDUser(User)
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
from typing import List, Optional
|
|
||||||
|
|
||||||
from fastapi.encoders import jsonable_encoder
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
|
|
||||||
from app.db_models.item import Item
|
|
||||||
from app.models.item import ItemCreate, ItemUpdate
|
|
||||||
|
|
||||||
|
|
||||||
def get(db_session: Session, *, id: int) -> Optional[Item]:
|
|
||||||
return db_session.query(Item).filter(Item.id == id).first()
|
|
||||||
|
|
||||||
|
|
||||||
def get_multi(db_session: Session, *, skip=0, limit=100) -> List[Optional[Item]]:
|
|
||||||
return db_session.query(Item).offset(skip).limit(limit).all()
|
|
||||||
|
|
||||||
|
|
||||||
def get_multi_by_owner(
|
|
||||||
db_session: Session, *, owner_id: int, skip=0, limit=100
|
|
||||||
) -> List[Optional[Item]]:
|
|
||||||
return (
|
|
||||||
db_session.query(Item)
|
|
||||||
.filter(Item.owner_id == owner_id)
|
|
||||||
.offset(skip)
|
|
||||||
.limit(limit)
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def create(db_session: Session, *, item_in: ItemCreate, owner_id: int) -> Item:
|
|
||||||
item_in_data = jsonable_encoder(item_in)
|
|
||||||
item = Item(**item_in_data, owner_id=owner_id)
|
|
||||||
db_session.add(item)
|
|
||||||
db_session.commit()
|
|
||||||
db_session.refresh(item)
|
|
||||||
return item
|
|
||||||
|
|
||||||
|
|
||||||
def update(db_session: Session, *, item: Item, item_in: ItemUpdate) -> Item:
|
|
||||||
item_data = jsonable_encoder(item)
|
|
||||||
update_data = item_in.dict(skip_defaults=True)
|
|
||||||
for field in item_data:
|
|
||||||
if field in update_data:
|
|
||||||
setattr(item, field, update_data[field])
|
|
||||||
db_session.add(item)
|
|
||||||
db_session.commit()
|
|
||||||
db_session.refresh(item)
|
|
||||||
return item
|
|
||||||
|
|
||||||
|
|
||||||
def remove(db_session: Session, *, id: int):
|
|
||||||
item = db_session.query(Item).filter(Item.id == id).first()
|
|
||||||
db_session.delete(item)
|
|
||||||
db_session.commit()
|
|
||||||
return item
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
from typing import List, Optional
|
|
||||||
|
|
||||||
from fastapi.encoders import jsonable_encoder
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
|
|
||||||
from app.core.security import get_password_hash, verify_password
|
|
||||||
from app.db_models.user import User
|
|
||||||
from app.models.user import UserCreate, UserUpdate
|
|
||||||
|
|
||||||
|
|
||||||
def get(db_session: Session, *, user_id: int) -> Optional[User]:
|
|
||||||
return db_session.query(User).filter(User.id == user_id).first()
|
|
||||||
|
|
||||||
|
|
||||||
def get_by_email(db_session: Session, *, email: str) -> Optional[User]:
|
|
||||||
return db_session.query(User).filter(User.email == email).first()
|
|
||||||
|
|
||||||
|
|
||||||
def authenticate(db_session: Session, *, email: str, password: str) -> Optional[User]:
|
|
||||||
user = get_by_email(db_session, email=email)
|
|
||||||
if not user:
|
|
||||||
return None
|
|
||||||
if not verify_password(password, user.hashed_password):
|
|
||||||
return None
|
|
||||||
return user
|
|
||||||
|
|
||||||
|
|
||||||
def is_active(user) -> bool:
|
|
||||||
return user.is_active
|
|
||||||
|
|
||||||
|
|
||||||
def is_superuser(user) -> bool:
|
|
||||||
return user.is_superuser
|
|
||||||
|
|
||||||
|
|
||||||
def get_multi(db_session: Session, *, skip=0, limit=100) -> List[Optional[User]]:
|
|
||||||
return db_session.query(User).offset(skip).limit(limit).all()
|
|
||||||
|
|
||||||
|
|
||||||
def create(db_session: Session, *, user_in: UserCreate) -> User:
|
|
||||||
user = User(
|
|
||||||
email=user_in.email,
|
|
||||||
hashed_password=get_password_hash(user_in.password),
|
|
||||||
full_name=user_in.full_name,
|
|
||||||
is_superuser=user_in.is_superuser,
|
|
||||||
)
|
|
||||||
db_session.add(user)
|
|
||||||
db_session.commit()
|
|
||||||
db_session.refresh(user)
|
|
||||||
return user
|
|
||||||
|
|
||||||
|
|
||||||
def update(db_session: Session, *, user: User, user_in: UserUpdate) -> User:
|
|
||||||
user_data = jsonable_encoder(user)
|
|
||||||
update_data = user_in.dict(skip_defaults=True)
|
|
||||||
for field in user_data:
|
|
||||||
if field in update_data:
|
|
||||||
setattr(user, field, update_data[field])
|
|
||||||
if user_in.password:
|
|
||||||
passwordhash = get_password_hash(user_in.password)
|
|
||||||
user.hashed_password = passwordhash
|
|
||||||
db_session.add(user)
|
|
||||||
db_session.commit()
|
|
||||||
db_session.refresh(user)
|
|
||||||
return user
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# Import all the models, so that Base has them before being
|
# Import all the models, so that Base has them before being
|
||||||
# imported by Alembic
|
# imported by Alembic
|
||||||
from app.db.base_class import Base # noqa
|
from app.db.base_class import Base # noqa
|
||||||
from app.db_models.user import User # noqa
|
from app.models.user import User # noqa
|
||||||
from app.db_models.item import Item # noqa
|
from app.models.item import Item # noqa
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
from sqlalchemy.ext.declarative import declarative_base, declared_attr
|
from sqlalchemy.ext.declarative import as_declarative, declared_attr
|
||||||
|
|
||||||
|
|
||||||
class CustomBase(object):
|
@as_declarative()
|
||||||
|
class Base:
|
||||||
# Generate __tablename__ automatically
|
# Generate __tablename__ automatically
|
||||||
@declared_attr
|
@declared_attr
|
||||||
def __tablename__(cls):
|
def __tablename__(cls):
|
||||||
return cls.__name__.lower()
|
return cls.__name__.lower()
|
||||||
|
|
||||||
|
|
||||||
Base = declarative_base(cls=CustomBase)
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
from app import crud
|
from app import crud
|
||||||
from app.core import config
|
from app.core.config import settings
|
||||||
from app.models.user import UserCreate
|
from app.schemas.user import UserCreate
|
||||||
|
|
||||||
# make sure all SQL Alchemy models are imported before initializing DB
|
# make sure all SQL Alchemy models are imported before initializing DB
|
||||||
# otherwise, SQL Alchemy might fail to initialize properly relationships
|
# otherwise, SQL Alchemy might fail to initialize relationships properly
|
||||||
# for more details: https://github.com/tiangolo/full-stack-fastapi-postgresql/issues/28
|
# for more details: https://github.com/tiangolo/full-stack-fastapi-postgresql/issues/28
|
||||||
from app.db import base
|
from app.db import base # noqa: F401
|
||||||
|
|
||||||
|
|
||||||
def init_db(db_session):
|
def init_db(db_session):
|
||||||
@@ -14,11 +14,11 @@ def init_db(db_session):
|
|||||||
# the tables un-commenting the next line
|
# the tables un-commenting the next line
|
||||||
# Base.metadata.create_all(bind=engine)
|
# Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
user = crud.user.get_by_email(db_session, email=config.FIRST_SUPERUSER)
|
user = crud.user.get_by_email(db_session, email=settings.FIRST_SUPERUSER)
|
||||||
if not user:
|
if not user:
|
||||||
user_in = UserCreate(
|
user_in = UserCreate(
|
||||||
email=config.FIRST_SUPERUSER,
|
email=settings.FIRST_SUPERUSER,
|
||||||
password=config.FIRST_SUPERUSER_PASSWORD,
|
password=settings.FIRST_SUPERUSER_PASSWORD,
|
||||||
is_superuser=True,
|
is_superuser=True,
|
||||||
)
|
)
|
||||||
user = crud.user.create(db_session, user_in=user_in)
|
user = crud.user.create(db_session, obj_in=user_in) # noqa: F841
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.orm import scoped_session, sessionmaker
|
from sqlalchemy.orm import scoped_session, sessionmaker
|
||||||
|
|
||||||
from app.core import config
|
from app.core.config import settings
|
||||||
|
|
||||||
engine = create_engine(config.SQLALCHEMY_DATABASE_URI, pool_pre_ping=True)
|
engine = create_engine(settings.SQLALCHEMY_DATABASE_URI, pool_pre_ping=True)
|
||||||
db_session = scoped_session(
|
db_session = scoped_session(
|
||||||
sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
from sqlalchemy import Column, ForeignKey, Integer, String
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
|
|
||||||
from app.db.base_class import Base
|
|
||||||
|
|
||||||
|
|
||||||
class Item(Base):
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
|
||||||
title = Column(String, index=True)
|
|
||||||
description = Column(String, index=True)
|
|
||||||
owner_id = Column(Integer, ForeignKey("user.id"))
|
|
||||||
owner = relationship("User", back_populates="items")
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
from sqlalchemy import Boolean, Column, Integer, String
|
|
||||||
from sqlalchemy.orm import relationship
|
|
||||||
|
|
||||||
from app.db.base_class import Base
|
|
||||||
|
|
||||||
|
|
||||||
class User(Base):
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
|
||||||
full_name = Column(String, index=True)
|
|
||||||
email = Column(String, unique=True, index=True)
|
|
||||||
hashed_password = Column(String)
|
|
||||||
is_active = Column(Boolean(), default=True)
|
|
||||||
is_superuser = Column(Boolean(), default=False)
|
|
||||||
items = relationship("Item", back_populates="owner")
|
|
||||||
@@ -3,29 +3,22 @@ from starlette.middleware.cors import CORSMiddleware
|
|||||||
from starlette.requests import Request
|
from starlette.requests import Request
|
||||||
|
|
||||||
from app.api.api_v1.api import api_router
|
from app.api.api_v1.api import api_router
|
||||||
from app.core import config
|
from app.core.config import settings
|
||||||
from app.db.session import Session
|
from app.db.session import Session
|
||||||
|
|
||||||
app = FastAPI(title=config.PROJECT_NAME, openapi_url="/api/v1/openapi.json")
|
app = FastAPI(title=settings.PROJECT_NAME, openapi_url=f"{settings.API_V1_STR}/openapi.json")
|
||||||
|
|
||||||
# CORS
|
|
||||||
origins = []
|
|
||||||
|
|
||||||
# Set all CORS enabled origins
|
# Set all CORS enabled origins
|
||||||
if config.BACKEND_CORS_ORIGINS:
|
if settings.BACKEND_CORS_ORIGINS:
|
||||||
origins_raw = config.BACKEND_CORS_ORIGINS.split(",")
|
|
||||||
for origin in origins_raw:
|
|
||||||
use_origin = origin.strip()
|
|
||||||
origins.append(use_origin)
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=origins,
|
allow_origins=[str(origin) for origin in settings.BACKEND_CORS_ORIGINS],
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["*"],
|
allow_methods=["*"],
|
||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
),
|
),
|
||||||
|
|
||||||
app.include_router(api_router, prefix=config.API_V1_STR)
|
app.include_router(api_router, prefix=settings.API_V1_STR)
|
||||||
|
|
||||||
|
|
||||||
@app.middleware("http")
|
@app.middleware("http")
|
||||||
|
|||||||
42
{{cookiecutter.project_slug}}/backend/app/app/models/item.py
Normal file → Executable file
42
{{cookiecutter.project_slug}}/backend/app/app/models/item.py
Normal file → Executable file
@@ -1,34 +1,12 @@
|
|||||||
from pydantic import BaseModel
|
from sqlalchemy import Column, ForeignKey, Integer, String
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
|
from app.db.base_class import Base
|
||||||
|
|
||||||
|
|
||||||
# Shared properties
|
class Item(Base):
|
||||||
class ItemBase(BaseModel):
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
title: str = None
|
title = Column(String, index=True)
|
||||||
description: str = None
|
description = Column(String, index=True)
|
||||||
|
owner_id = Column(Integer, ForeignKey("user.id"))
|
||||||
|
owner = relationship("User", back_populates="items")
|
||||||
# Properties to receive on item creation
|
|
||||||
class ItemCreate(ItemBase):
|
|
||||||
title: str
|
|
||||||
|
|
||||||
|
|
||||||
# Properties to receive on item update
|
|
||||||
class ItemUpdate(ItemBase):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# Properties shared by models stored in DB
|
|
||||||
class ItemInDBBase(ItemBase):
|
|
||||||
id: int
|
|
||||||
title: str
|
|
||||||
owner_id: int
|
|
||||||
|
|
||||||
|
|
||||||
# Properties to return to client
|
|
||||||
class Item(ItemInDBBase):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# Properties properties stored in DB
|
|
||||||
class ItemInDB(ItemInDBBase):
|
|
||||||
pass
|
|
||||||
|
|||||||
44
{{cookiecutter.project_slug}}/backend/app/app/models/user.py
Normal file → Executable file
44
{{cookiecutter.project_slug}}/backend/app/app/models/user.py
Normal file → Executable file
@@ -1,36 +1,14 @@
|
|||||||
from typing import Optional
|
from sqlalchemy import Boolean, Column, Integer, String
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from app.db.base_class import Base
|
||||||
|
|
||||||
|
|
||||||
# Shared properties
|
class User(Base):
|
||||||
class UserBase(BaseModel):
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
email: Optional[str] = None
|
full_name = Column(String, index=True)
|
||||||
is_active: Optional[bool] = True
|
email = Column(String, unique=True, index=True)
|
||||||
is_superuser: Optional[bool] = False
|
hashed_password = Column(String)
|
||||||
full_name: Optional[str] = None
|
is_active = Column(Boolean(), default=True)
|
||||||
|
is_superuser = Column(Boolean(), default=False)
|
||||||
|
items = relationship("Item", back_populates="owner")
|
||||||
class UserBaseInDB(UserBase):
|
|
||||||
id: int = None
|
|
||||||
|
|
||||||
|
|
||||||
# Properties to receive via API on creation
|
|
||||||
class UserCreate(UserBaseInDB):
|
|
||||||
email: str
|
|
||||||
password: str
|
|
||||||
|
|
||||||
|
|
||||||
# Properties to receive via API on update
|
|
||||||
class UserUpdate(UserBaseInDB):
|
|
||||||
password: Optional[str] = None
|
|
||||||
|
|
||||||
|
|
||||||
# Additional properties to return via API
|
|
||||||
class User(UserBaseInDB):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# Additional properties stored in DB
|
|
||||||
class UserInDB(UserBaseInDB):
|
|
||||||
hashed_password: str
|
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from .user import User # noqa: F401
|
||||||
|
|
||||||
|
|
||||||
|
# Shared properties
|
||||||
|
class ItemBase(BaseModel):
|
||||||
|
title: str = None
|
||||||
|
description: str = None
|
||||||
|
|
||||||
|
|
||||||
|
# Properties to receive on item creation
|
||||||
|
class ItemCreate(ItemBase):
|
||||||
|
title: str
|
||||||
|
|
||||||
|
|
||||||
|
# Properties to receive on item update
|
||||||
|
class ItemUpdate(ItemBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Properties shared by models stored in DB
|
||||||
|
class ItemInDBBase(ItemBase):
|
||||||
|
id: int
|
||||||
|
title: str
|
||||||
|
owner_id: int
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
# Properties to return to client
|
||||||
|
class Item(ItemInDBBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Properties properties stored in DB
|
||||||
|
class ItemInDB(ItemInDBBase):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel, EmailStr
|
||||||
|
|
||||||
|
|
||||||
|
# Shared properties
|
||||||
|
class UserBase(BaseModel):
|
||||||
|
email: Optional[EmailStr] = None
|
||||||
|
is_active: Optional[bool] = True
|
||||||
|
is_superuser: Optional[bool] = False
|
||||||
|
full_name: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
# Properties to receive via API on creation
|
||||||
|
class UserCreate(UserBase):
|
||||||
|
email: EmailStr
|
||||||
|
password: str
|
||||||
|
|
||||||
|
|
||||||
|
# Properties to receive via API on update
|
||||||
|
class UserUpdate(UserBase):
|
||||||
|
password: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class UserInDBBase(UserBase):
|
||||||
|
id: int = None
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
# Additional properties to return via API
|
||||||
|
class User(UserInDBBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Additional properties stored in DB
|
||||||
|
class UserInDB(UserInDBBase):
|
||||||
|
hashed_password: str
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
from app.core import config
|
from app.core.config import settings
|
||||||
from app.tests.utils.utils import get_server_api
|
from app.tests.utils.utils import get_server_api
|
||||||
|
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ def test_celery_worker_test(superuser_token_headers):
|
|||||||
server_api = get_server_api()
|
server_api = get_server_api()
|
||||||
data = {"msg": "test"}
|
data = {"msg": "test"}
|
||||||
r = requests.post(
|
r = requests.post(
|
||||||
f"{server_api}{config.API_V1_STR}/utils/test-celery/",
|
f"{server_api}{settings.API_V1_STR}/utils/test-celery/",
|
||||||
json=data,
|
json=data,
|
||||||
headers=superuser_token_headers,
|
headers=superuser_token_headers,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
from app.core import config
|
from app.core.config import settings
|
||||||
from app.tests.utils.item import create_random_item
|
from app.tests.utils.item import create_random_item
|
||||||
from app.tests.utils.utils import get_server_api
|
from app.tests.utils.utils import get_server_api
|
||||||
|
from app.tests.utils.user import create_random_user # noqa: F401
|
||||||
|
|
||||||
|
|
||||||
def test_create_item(superuser_token_headers):
|
def test_create_item(superuser_token_headers):
|
||||||
server_api = get_server_api()
|
server_api = get_server_api()
|
||||||
data = {"title": "Foo", "description": "Fighters"}
|
data = {"title": "Foo", "description": "Fighters"}
|
||||||
response = requests.post(
|
response = requests.post(
|
||||||
f"{server_api}{config.API_V1_STR}/items/",
|
f"{server_api}{settings.API_V1_STR}/items/",
|
||||||
headers=superuser_token_headers,
|
headers=superuser_token_headers,
|
||||||
json=data,
|
json=data,
|
||||||
)
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
content = response.json()
|
content = response.json()
|
||||||
assert content["title"] == data["title"]
|
assert content["title"] == data["title"]
|
||||||
assert content["description"] == data["description"]
|
assert content["description"] == data["description"]
|
||||||
@@ -24,9 +26,10 @@ def test_read_item(superuser_token_headers):
|
|||||||
item = create_random_item()
|
item = create_random_item()
|
||||||
server_api = get_server_api()
|
server_api = get_server_api()
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
f"{server_api}{config.API_V1_STR}/items/{item.id}",
|
f"{server_api}{settings.API_V1_STR}/items/{item.id}",
|
||||||
headers=superuser_token_headers,
|
headers=superuser_token_headers,
|
||||||
)
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
content = response.json()
|
content = response.json()
|
||||||
assert content["title"] == item.title
|
assert content["title"] == item.title
|
||||||
assert content["description"] == item.description
|
assert content["description"] == item.description
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
from app.core import config
|
from app.core.config import settings
|
||||||
from app.tests.utils.utils import get_server_api
|
from app.tests.utils.utils import get_server_api
|
||||||
|
|
||||||
|
|
||||||
def test_get_access_token():
|
def test_get_access_token():
|
||||||
server_api = get_server_api()
|
server_api = get_server_api()
|
||||||
login_data = {
|
login_data = {
|
||||||
"username": config.FIRST_SUPERUSER,
|
"username": settings.FIRST_SUPERUSER,
|
||||||
"password": config.FIRST_SUPERUSER_PASSWORD,
|
"password": settings.FIRST_SUPERUSER_PASSWORD,
|
||||||
}
|
}
|
||||||
r = requests.post(
|
r = requests.post(
|
||||||
f"{server_api}{config.API_V1_STR}/login/access-token", data=login_data
|
f"{server_api}{settings.API_V1_STR}/login/access-token", data=login_data
|
||||||
)
|
)
|
||||||
tokens = r.json()
|
tokens = r.json()
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
@@ -22,7 +22,7 @@ def test_get_access_token():
|
|||||||
def test_use_access_token(superuser_token_headers):
|
def test_use_access_token(superuser_token_headers):
|
||||||
server_api = get_server_api()
|
server_api = get_server_api()
|
||||||
r = requests.post(
|
r = requests.post(
|
||||||
f"{server_api}{config.API_V1_STR}/login/test-token",
|
f"{server_api}{settings.API_V1_STR}/login/test-token",
|
||||||
headers=superuser_token_headers,
|
headers=superuser_token_headers,
|
||||||
)
|
)
|
||||||
result = r.json()
|
result = r.json()
|
||||||
|
|||||||
@@ -1,32 +1,43 @@
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
from app import crud
|
from app import crud
|
||||||
from app.core import config
|
from app.core.config import settings
|
||||||
from app.db.session import db_session
|
from app.db.session import db_session
|
||||||
from app.models.user import UserCreate
|
from app.schemas.user import UserCreate
|
||||||
from app.tests.utils.user import user_authentication_headers
|
from app.tests.utils.utils import get_server_api, random_lower_string, random_email
|
||||||
from app.tests.utils.utils import get_server_api, random_lower_string
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_users_superuser_me(superuser_token_headers):
|
def test_get_users_superuser_me(superuser_token_headers):
|
||||||
server_api = get_server_api()
|
server_api = get_server_api()
|
||||||
r = requests.get(
|
r = requests.get(
|
||||||
f"{server_api}{config.API_V1_STR}/users/me", headers=superuser_token_headers
|
f"{server_api}{settings.API_V1_STR}/users/me", headers=superuser_token_headers
|
||||||
)
|
)
|
||||||
current_user = r.json()
|
current_user = r.json()
|
||||||
assert current_user
|
assert current_user
|
||||||
assert current_user["is_active"] is True
|
assert current_user["is_active"] is True
|
||||||
assert current_user["is_superuser"]
|
assert current_user["is_superuser"]
|
||||||
assert current_user["email"] == config.FIRST_SUPERUSER
|
assert current_user["email"] == settings.FIRST_SUPERUSER
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_users_normal_user_me(normal_user_token_headers):
|
||||||
|
server_api = get_server_api()
|
||||||
|
r = requests.get(
|
||||||
|
f"{server_api}{settings.API_V1_STR}/users/me", headers=normal_user_token_headers
|
||||||
|
)
|
||||||
|
current_user = r.json()
|
||||||
|
assert current_user
|
||||||
|
assert current_user["is_active"] is True
|
||||||
|
assert current_user["is_superuser"] is False
|
||||||
|
assert current_user["email"] == settings.EMAIL_TEST_USER
|
||||||
|
|
||||||
|
|
||||||
def test_create_user_new_email(superuser_token_headers):
|
def test_create_user_new_email(superuser_token_headers):
|
||||||
server_api = get_server_api()
|
server_api = get_server_api()
|
||||||
username = random_lower_string()
|
username = random_email()
|
||||||
password = random_lower_string()
|
password = random_lower_string()
|
||||||
data = {"email": username, "password": password}
|
data = {"email": username, "password": password}
|
||||||
r = requests.post(
|
r = requests.post(
|
||||||
f"{server_api}{config.API_V1_STR}/users/",
|
f"{server_api}{settings.API_V1_STR}/users/",
|
||||||
headers=superuser_token_headers,
|
headers=superuser_token_headers,
|
||||||
json=data,
|
json=data,
|
||||||
)
|
)
|
||||||
@@ -38,13 +49,13 @@ def test_create_user_new_email(superuser_token_headers):
|
|||||||
|
|
||||||
def test_get_existing_user(superuser_token_headers):
|
def test_get_existing_user(superuser_token_headers):
|
||||||
server_api = get_server_api()
|
server_api = get_server_api()
|
||||||
username = random_lower_string()
|
username = random_email()
|
||||||
password = random_lower_string()
|
password = random_lower_string()
|
||||||
user_in = UserCreate(email=username, password=password)
|
user_in = UserCreate(email=username, password=password)
|
||||||
user = crud.user.create(db_session, user_in=user_in)
|
user = crud.user.create(db_session, obj_in=user_in)
|
||||||
user_id = user.id
|
user_id = user.id
|
||||||
r = requests.get(
|
r = requests.get(
|
||||||
f"{server_api}{config.API_V1_STR}/users/{user_id}",
|
f"{server_api}{settings.API_V1_STR}/users/{user_id}",
|
||||||
headers=superuser_token_headers,
|
headers=superuser_token_headers,
|
||||||
)
|
)
|
||||||
assert 200 <= r.status_code < 300
|
assert 200 <= r.status_code < 300
|
||||||
@@ -55,14 +66,14 @@ def test_get_existing_user(superuser_token_headers):
|
|||||||
|
|
||||||
def test_create_user_existing_username(superuser_token_headers):
|
def test_create_user_existing_username(superuser_token_headers):
|
||||||
server_api = get_server_api()
|
server_api = get_server_api()
|
||||||
username = random_lower_string()
|
username = random_email()
|
||||||
# username = email
|
# username = email
|
||||||
password = random_lower_string()
|
password = random_lower_string()
|
||||||
user_in = UserCreate(email=username, password=password)
|
user_in = UserCreate(email=username, password=password)
|
||||||
user = crud.user.create(db_session, user_in=user_in)
|
crud.user.create(db_session, obj_in=user_in)
|
||||||
data = {"email": username, "password": password}
|
data = {"email": username, "password": password}
|
||||||
r = requests.post(
|
r = requests.post(
|
||||||
f"{server_api}{config.API_V1_STR}/users/",
|
f"{server_api}{settings.API_V1_STR}/users/",
|
||||||
headers=superuser_token_headers,
|
headers=superuser_token_headers,
|
||||||
json=data,
|
json=data,
|
||||||
)
|
)
|
||||||
@@ -71,34 +82,33 @@ def test_create_user_existing_username(superuser_token_headers):
|
|||||||
assert "_id" not in created_user
|
assert "_id" not in created_user
|
||||||
|
|
||||||
|
|
||||||
def test_create_user_by_normal_user():
|
def test_create_user_by_normal_user(normal_user_token_headers):
|
||||||
server_api = get_server_api()
|
server_api = get_server_api()
|
||||||
username = random_lower_string()
|
username = random_email()
|
||||||
password = random_lower_string()
|
password = random_lower_string()
|
||||||
user_in = UserCreate(email=username, password=password)
|
|
||||||
user = crud.user.create(db_session, user_in=user_in)
|
|
||||||
user_token_headers = user_authentication_headers(server_api, username, password)
|
|
||||||
data = {"email": username, "password": password}
|
data = {"email": username, "password": password}
|
||||||
r = requests.post(
|
r = requests.post(
|
||||||
f"{server_api}{config.API_V1_STR}/users/", headers=user_token_headers, json=data
|
f"{server_api}{settings.API_V1_STR}/users/",
|
||||||
|
headers=normal_user_token_headers,
|
||||||
|
json=data,
|
||||||
)
|
)
|
||||||
assert r.status_code == 400
|
assert r.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
def test_retrieve_users(superuser_token_headers):
|
def test_retrieve_users(superuser_token_headers):
|
||||||
server_api = get_server_api()
|
server_api = get_server_api()
|
||||||
username = random_lower_string()
|
username = random_email()
|
||||||
password = random_lower_string()
|
password = random_lower_string()
|
||||||
user_in = UserCreate(email=username, password=password)
|
user_in = UserCreate(email=username, password=password)
|
||||||
user = crud.user.create(db_session, user_in=user_in)
|
user = crud.user.create(db_session, obj_in=user_in)
|
||||||
|
|
||||||
username2 = random_lower_string()
|
username2 = random_email()
|
||||||
password2 = random_lower_string()
|
password2 = random_lower_string()
|
||||||
user_in2 = UserCreate(email=username2, password=password2)
|
user_in2 = UserCreate(email=username2, password=password2)
|
||||||
user2 = crud.user.create(db_session, user_in=user_in2)
|
crud.user.create(db_session, obj_in=user_in2)
|
||||||
|
|
||||||
r = requests.get(
|
r = requests.get(
|
||||||
f"{server_api}{config.API_V1_STR}/users/", headers=superuser_token_headers
|
f"{server_api}{settings.API_V1_STR}/users/", headers=superuser_token_headers
|
||||||
)
|
)
|
||||||
all_users = r.json()
|
all_users = r.json()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
from app.tests.utils.utils import get_server_api, get_superuser_token_headers
|
from app.tests.utils.utils import get_server_api, get_superuser_token_headers
|
||||||
|
from app.tests.utils.user import authentication_token_from_email
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
@@ -11,3 +13,8 @@ def server_api():
|
|||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def superuser_token_headers():
|
def superuser_token_headers():
|
||||||
return get_superuser_token_headers()
|
return get_superuser_token_headers()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def normal_user_token_headers():
|
||||||
|
return authentication_token_from_email(settings.EMAIL_TEST_USER)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from app import crud
|
from app import crud
|
||||||
from app.models.item import ItemCreate, ItemUpdate
|
from app.schemas.item import ItemCreate, ItemUpdate
|
||||||
from app.tests.utils.user import create_random_user
|
from app.tests.utils.user import create_random_user
|
||||||
from app.tests.utils.utils import random_lower_string
|
from app.tests.utils.utils import random_lower_string
|
||||||
from app.db.session import db_session
|
from app.db.session import db_session
|
||||||
@@ -10,7 +10,9 @@ def test_create_item():
|
|||||||
description = random_lower_string()
|
description = random_lower_string()
|
||||||
item_in = ItemCreate(title=title, description=description)
|
item_in = ItemCreate(title=title, description=description)
|
||||||
user = create_random_user()
|
user = create_random_user()
|
||||||
item = crud.item.create(db_session=db_session, item_in=item_in, owner_id=user.id)
|
item = crud.item.create_with_owner(
|
||||||
|
db_session=db_session, obj_in=item_in, owner_id=user.id
|
||||||
|
)
|
||||||
assert item.title == title
|
assert item.title == title
|
||||||
assert item.description == description
|
assert item.description == description
|
||||||
assert item.owner_id == user.id
|
assert item.owner_id == user.id
|
||||||
@@ -21,7 +23,9 @@ def test_get_item():
|
|||||||
description = random_lower_string()
|
description = random_lower_string()
|
||||||
item_in = ItemCreate(title=title, description=description)
|
item_in = ItemCreate(title=title, description=description)
|
||||||
user = create_random_user()
|
user = create_random_user()
|
||||||
item = crud.item.create(db_session=db_session, item_in=item_in, owner_id=user.id)
|
item = crud.item.create_with_owner(
|
||||||
|
db_session=db_session, obj_in=item_in, owner_id=user.id
|
||||||
|
)
|
||||||
stored_item = crud.item.get(db_session=db_session, id=item.id)
|
stored_item = crud.item.get(db_session=db_session, id=item.id)
|
||||||
assert item.id == stored_item.id
|
assert item.id == stored_item.id
|
||||||
assert item.title == stored_item.title
|
assert item.title == stored_item.title
|
||||||
@@ -34,12 +38,12 @@ def test_update_item():
|
|||||||
description = random_lower_string()
|
description = random_lower_string()
|
||||||
item_in = ItemCreate(title=title, description=description)
|
item_in = ItemCreate(title=title, description=description)
|
||||||
user = create_random_user()
|
user = create_random_user()
|
||||||
item = crud.item.create(db_session=db_session, item_in=item_in, owner_id=user.id)
|
item = crud.item.create_with_owner(
|
||||||
|
db_session=db_session, obj_in=item_in, owner_id=user.id
|
||||||
|
)
|
||||||
description2 = random_lower_string()
|
description2 = random_lower_string()
|
||||||
item_update = ItemUpdate(description=description2)
|
item_update = ItemUpdate(description=description2)
|
||||||
item2 = crud.item.update(
|
item2 = crud.item.update(db_session=db_session, db_obj=item, obj_in=item_update)
|
||||||
db_session=db_session, item=item, item_in=item_update
|
|
||||||
)
|
|
||||||
assert item.id == item2.id
|
assert item.id == item2.id
|
||||||
assert item.title == item2.title
|
assert item.title == item2.title
|
||||||
assert item2.description == description2
|
assert item2.description == description2
|
||||||
@@ -51,7 +55,9 @@ def test_delete_item():
|
|||||||
description = random_lower_string()
|
description = random_lower_string()
|
||||||
item_in = ItemCreate(title=title, description=description)
|
item_in = ItemCreate(title=title, description=description)
|
||||||
user = create_random_user()
|
user = create_random_user()
|
||||||
item = crud.item.create(db_session=db_session, item_in=item_in, owner_id=user.id)
|
item = crud.item.create_with_owner(
|
||||||
|
db_session=db_session, obj_in=item_in, owner_id=user.id
|
||||||
|
)
|
||||||
item2 = crud.item.remove(db_session=db_session, id=item.id)
|
item2 = crud.item.remove(db_session=db_session, id=item.id)
|
||||||
item3 = crud.item.get(db_session=db_session, id=item.id)
|
item3 = crud.item.get(db_session=db_session, id=item.id)
|
||||||
assert item3 is None
|
assert item3 is None
|
||||||
|
|||||||
@@ -1,25 +1,26 @@
|
|||||||
from fastapi.encoders import jsonable_encoder
|
from fastapi.encoders import jsonable_encoder
|
||||||
|
|
||||||
from app import crud
|
from app import crud
|
||||||
|
from app.core.security import get_password_hash, verify_password
|
||||||
from app.db.session import db_session
|
from app.db.session import db_session
|
||||||
from app.models.user import UserCreate
|
from app.schemas.user import UserCreate, UserUpdate
|
||||||
from app.tests.utils.utils import random_lower_string
|
from app.tests.utils.utils import random_lower_string, random_email
|
||||||
|
|
||||||
|
|
||||||
def test_create_user():
|
def test_create_user():
|
||||||
email = random_lower_string()
|
email = random_email()
|
||||||
password = random_lower_string()
|
password = random_lower_string()
|
||||||
user_in = UserCreate(email=email, password=password)
|
user_in = UserCreate(email=email, password=password)
|
||||||
user = crud.user.create(db_session, user_in=user_in)
|
user = crud.user.create(db_session, obj_in=user_in)
|
||||||
assert user.email == email
|
assert user.email == email
|
||||||
assert hasattr(user, "hashed_password")
|
assert hasattr(user, "hashed_password")
|
||||||
|
|
||||||
|
|
||||||
def test_authenticate_user():
|
def test_authenticate_user():
|
||||||
email = random_lower_string()
|
email = random_email()
|
||||||
password = random_lower_string()
|
password = random_lower_string()
|
||||||
user_in = UserCreate(email=email, password=password)
|
user_in = UserCreate(email=email, password=password)
|
||||||
user = crud.user.create(db_session, user_in=user_in)
|
user = crud.user.create(db_session, obj_in=user_in)
|
||||||
authenticated_user = crud.user.authenticate(
|
authenticated_user = crud.user.authenticate(
|
||||||
db_session, email=email, password=password
|
db_session, email=email, password=password
|
||||||
)
|
)
|
||||||
@@ -28,56 +29,66 @@ def test_authenticate_user():
|
|||||||
|
|
||||||
|
|
||||||
def test_not_authenticate_user():
|
def test_not_authenticate_user():
|
||||||
email = random_lower_string()
|
email = random_email()
|
||||||
password = random_lower_string()
|
password = random_lower_string()
|
||||||
user = crud.user.authenticate(db_session, email=email, password=password)
|
user = crud.user.authenticate(db_session, email=email, password=password)
|
||||||
assert user is None
|
assert user is None
|
||||||
|
|
||||||
|
|
||||||
def test_check_if_user_is_active():
|
def test_check_if_user_is_active():
|
||||||
email = random_lower_string()
|
email = random_email()
|
||||||
password = random_lower_string()
|
password = random_lower_string()
|
||||||
user_in = UserCreate(email=email, password=password)
|
user_in = UserCreate(email=email, password=password)
|
||||||
user = crud.user.create(db_session, user_in=user_in)
|
user = crud.user.create(db_session, obj_in=user_in)
|
||||||
is_active = crud.user.is_active(user)
|
is_active = crud.user.is_active(user)
|
||||||
assert is_active is True
|
assert is_active is True
|
||||||
|
|
||||||
|
|
||||||
def test_check_if_user_is_active_inactive():
|
def test_check_if_user_is_active_inactive():
|
||||||
email = random_lower_string()
|
email = random_email()
|
||||||
password = random_lower_string()
|
password = random_lower_string()
|
||||||
user_in = UserCreate(email=email, password=password, disabled=True)
|
user_in = UserCreate(email=email, password=password, disabled=True)
|
||||||
print(user_in)
|
user = crud.user.create(db_session, obj_in=user_in)
|
||||||
user = crud.user.create(db_session, user_in=user_in)
|
|
||||||
print(user)
|
|
||||||
is_active = crud.user.is_active(user)
|
is_active = crud.user.is_active(user)
|
||||||
print(is_active)
|
|
||||||
assert is_active
|
assert is_active
|
||||||
|
|
||||||
|
|
||||||
def test_check_if_user_is_superuser():
|
def test_check_if_user_is_superuser():
|
||||||
email = random_lower_string()
|
email = random_email()
|
||||||
password = random_lower_string()
|
password = random_lower_string()
|
||||||
user_in = UserCreate(email=email, password=password, is_superuser=True)
|
user_in = UserCreate(email=email, password=password, is_superuser=True)
|
||||||
user = crud.user.create(db_session, user_in=user_in)
|
user = crud.user.create(db_session, obj_in=user_in)
|
||||||
is_superuser = crud.user.is_superuser(user)
|
is_superuser = crud.user.is_superuser(user)
|
||||||
assert is_superuser is True
|
assert is_superuser is True
|
||||||
|
|
||||||
|
|
||||||
def test_check_if_user_is_superuser_normal_user():
|
def test_check_if_user_is_superuser_normal_user():
|
||||||
username = random_lower_string()
|
username = random_email()
|
||||||
password = random_lower_string()
|
password = random_lower_string()
|
||||||
user_in = UserCreate(email=username, password=password)
|
user_in = UserCreate(email=username, password=password)
|
||||||
user = crud.user.create(db_session, user_in=user_in)
|
user = crud.user.create(db_session, obj_in=user_in)
|
||||||
is_superuser = crud.user.is_superuser(user)
|
is_superuser = crud.user.is_superuser(user)
|
||||||
assert is_superuser is False
|
assert is_superuser is False
|
||||||
|
|
||||||
|
|
||||||
def test_get_user():
|
def test_get_user():
|
||||||
password = random_lower_string()
|
password = random_lower_string()
|
||||||
username = random_lower_string()
|
username = random_email()
|
||||||
user_in = UserCreate(email=username, password=password, is_superuser=True)
|
user_in = UserCreate(email=username, password=password, is_superuser=True)
|
||||||
user = crud.user.create(db_session, user_in=user_in)
|
user = crud.user.create(db_session, obj_in=user_in)
|
||||||
user_2 = crud.user.get(db_session, user_id=user.id)
|
user_2 = crud.user.get(db_session, id=user.id)
|
||||||
assert user.email == user_2.email
|
assert user.email == user_2.email
|
||||||
assert jsonable_encoder(user) == jsonable_encoder(user_2)
|
assert jsonable_encoder(user) == jsonable_encoder(user_2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_user():
|
||||||
|
password = random_lower_string()
|
||||||
|
email = random_email()
|
||||||
|
user_in = UserCreate(email=email, password=password, is_superuser=True)
|
||||||
|
user = crud.user.create(db_session, obj_in=user_in)
|
||||||
|
new_password = random_lower_string()
|
||||||
|
user_in = UserUpdate(password=new_password, is_superuser=True)
|
||||||
|
crud.user.update(db_session, db_obj=user, obj_in=user_in)
|
||||||
|
user_2 = crud.user.get(db_session, id=user.id)
|
||||||
|
assert user.email == user_2.email
|
||||||
|
assert verify_password(new_password, user_2.hashed_password)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from app import crud
|
from app import crud
|
||||||
from app.db.session import db_session
|
from app.db.session import db_session
|
||||||
from app.models.item import ItemCreate
|
from app.schemas.item import ItemCreate
|
||||||
from app.tests.utils.user import create_random_user
|
from app.tests.utils.user import create_random_user
|
||||||
from app.tests.utils.utils import random_lower_string
|
from app.tests.utils.utils import random_lower_string
|
||||||
|
|
||||||
@@ -12,6 +12,6 @@ def create_random_item(owner_id: int = None):
|
|||||||
title = random_lower_string()
|
title = random_lower_string()
|
||||||
description = random_lower_string()
|
description = random_lower_string()
|
||||||
item_in = ItemCreate(title=title, description=description, id=id)
|
item_in = ItemCreate(title=title, description=description, id=id)
|
||||||
return crud.item.create(
|
return crud.item.create_with_owner(
|
||||||
db_session=db_session, item_in=item_in, owner_id=owner_id
|
db_session=db_session, obj_in=item_in, owner_id=owner_id
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
from app import crud
|
from app import crud
|
||||||
from app.core import config
|
from app.core.config import settings
|
||||||
from app.db.session import db_session
|
from app.db.session import db_session
|
||||||
from app.models.user import UserCreate
|
from app.schemas.user import UserCreate, UserUpdate
|
||||||
from app.tests.utils.utils import random_lower_string
|
from app.tests.utils.utils import get_server_api, random_lower_string, random_email
|
||||||
|
|
||||||
|
|
||||||
def user_authentication_headers(server_api, email, password):
|
def user_authentication_headers(server_api, email, password):
|
||||||
data = {"username": email, "password": password}
|
data = {"username": email, "password": password}
|
||||||
|
|
||||||
r = requests.post(f"{server_api}{config.API_V1_STR}/login/access-token", data=data)
|
r = requests.post(f"{server_api}{settings.API_V1_STR}/login/access-token", data=data)
|
||||||
response = r.json()
|
response = r.json()
|
||||||
auth_token = response["access_token"]
|
auth_token = response["access_token"]
|
||||||
headers = {"Authorization": f"Bearer {auth_token}"}
|
headers = {"Authorization": f"Bearer {auth_token}"}
|
||||||
@@ -18,8 +18,26 @@ def user_authentication_headers(server_api, email, password):
|
|||||||
|
|
||||||
|
|
||||||
def create_random_user():
|
def create_random_user():
|
||||||
email = random_lower_string()
|
email = random_email()
|
||||||
password = random_lower_string()
|
password = random_lower_string()
|
||||||
user_in = UserCreate(username=email, email=email, password=password)
|
user_in = UserCreate(username=email, email=email, password=password)
|
||||||
user = crud.user.create(db_session=db_session, user_in=user_in)
|
user = crud.user.create(db_session=db_session, obj_in=user_in)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def authentication_token_from_email(email):
|
||||||
|
"""
|
||||||
|
Return a valid token for the user with given email.
|
||||||
|
|
||||||
|
If the user doesn't exist it is created first.
|
||||||
|
"""
|
||||||
|
password = random_lower_string()
|
||||||
|
user = crud.user.get_by_email(db_session, email=email)
|
||||||
|
if not user:
|
||||||
|
user_in = UserCreate(username=email, email=email, password=password)
|
||||||
|
user = crud.user.create(db_session=db_session, obj_in=user_in)
|
||||||
|
else:
|
||||||
|
user_in = UserUpdate(password=password)
|
||||||
|
user = crud.user.update(db_session, db_obj=user, obj_in=user_in)
|
||||||
|
|
||||||
|
return user_authentication_headers(get_server_api(), email, password)
|
||||||
|
|||||||
@@ -3,26 +3,30 @@ import string
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
from app.core import config
|
from app.core.config import settings
|
||||||
|
|
||||||
|
|
||||||
def random_lower_string():
|
def random_lower_string():
|
||||||
return "".join(random.choices(string.ascii_lowercase, k=32))
|
return "".join(random.choices(string.ascii_lowercase, k=32))
|
||||||
|
|
||||||
|
|
||||||
|
def random_email():
|
||||||
|
return f"{random_lower_string()}@{random_lower_string()}.com"
|
||||||
|
|
||||||
|
|
||||||
def get_server_api():
|
def get_server_api():
|
||||||
server_name = f"http://{config.SERVER_NAME}"
|
server_name = f"http://{settings.SERVER_NAME}"
|
||||||
return server_name
|
return server_name
|
||||||
|
|
||||||
|
|
||||||
def get_superuser_token_headers():
|
def get_superuser_token_headers():
|
||||||
server_api = get_server_api()
|
server_api = get_server_api()
|
||||||
login_data = {
|
login_data = {
|
||||||
"username": config.FIRST_SUPERUSER,
|
"username": settings.FIRST_SUPERUSER,
|
||||||
"password": config.FIRST_SUPERUSER_PASSWORD,
|
"password": settings.FIRST_SUPERUSER_PASSWORD,
|
||||||
}
|
}
|
||||||
r = requests.post(
|
r = requests.post(
|
||||||
f"{server_api}{config.API_V1_STR}/login/access-token", data=login_data
|
f"{server_api}{settings.API_V1_STR}/login/access-token", data=login_data
|
||||||
)
|
)
|
||||||
tokens = r.json()
|
tokens = r.json()
|
||||||
a_token = tokens["access_token"]
|
a_token = tokens["access_token"]
|
||||||
|
|||||||
@@ -8,79 +8,79 @@ import jwt
|
|||||||
from emails.template import JinjaTemplate
|
from emails.template import JinjaTemplate
|
||||||
from jwt.exceptions import InvalidTokenError
|
from jwt.exceptions import InvalidTokenError
|
||||||
|
|
||||||
from app.core import config
|
from app.core.config import settings
|
||||||
|
|
||||||
password_reset_jwt_subject = "preset"
|
password_reset_jwt_subject = "preset"
|
||||||
|
|
||||||
|
|
||||||
def send_email(email_to: str, subject_template="", html_template="", environment={}):
|
def send_email(email_to: str, subject_template="", html_template="", environment={}):
|
||||||
assert config.EMAILS_ENABLED, "no provided configuration for email variables"
|
assert settings.EMAILS_ENABLED, "no provided configuration for email variables"
|
||||||
message = emails.Message(
|
message = emails.Message(
|
||||||
subject=JinjaTemplate(subject_template),
|
subject=JinjaTemplate(subject_template),
|
||||||
html=JinjaTemplate(html_template),
|
html=JinjaTemplate(html_template),
|
||||||
mail_from=(config.EMAILS_FROM_NAME, config.EMAILS_FROM_EMAIL),
|
mail_from=(settings.EMAILS_FROM_NAME, settings.EMAILS_FROM_EMAIL),
|
||||||
)
|
)
|
||||||
smtp_options = {"host": config.SMTP_HOST, "port": config.SMTP_PORT}
|
smtp_options = {"host": settings.SMTP_HOST, "port": settings.SMTP_PORT}
|
||||||
if config.SMTP_TLS:
|
if settings.SMTP_TLS:
|
||||||
smtp_options["tls"] = True
|
smtp_options["tls"] = True
|
||||||
if config.SMTP_USER:
|
if settings.SMTP_USER:
|
||||||
smtp_options["user"] = config.SMTP_USER
|
smtp_options["user"] = settings.SMTP_USER
|
||||||
if config.SMTP_PASSWORD:
|
if settings.SMTP_PASSWORD:
|
||||||
smtp_options["password"] = config.SMTP_PASSWORD
|
smtp_options["password"] = settings.SMTP_PASSWORD
|
||||||
response = message.send(to=email_to, render=environment, smtp=smtp_options)
|
response = message.send(to=email_to, render=environment, smtp=smtp_options)
|
||||||
logging.info(f"send email result: {response}")
|
logging.info(f"send email result: {response}")
|
||||||
|
|
||||||
|
|
||||||
def send_test_email(email_to: str):
|
def send_test_email(email_to: str):
|
||||||
project_name = config.PROJECT_NAME
|
project_name = settings.PROJECT_NAME
|
||||||
subject = f"{project_name} - Test email"
|
subject = f"{project_name} - Test email"
|
||||||
with open(Path(config.EMAIL_TEMPLATES_DIR) / "test_email.html") as f:
|
with open(Path(settings.EMAIL_TEMPLATES_DIR) / "test_email.html") as f:
|
||||||
template_str = f.read()
|
template_str = f.read()
|
||||||
send_email(
|
send_email(
|
||||||
email_to=email_to,
|
email_to=email_to,
|
||||||
subject_template=subject,
|
subject_template=subject,
|
||||||
html_template=template_str,
|
html_template=template_str,
|
||||||
environment={"project_name": config.PROJECT_NAME, "email": email_to},
|
environment={"project_name": settings.PROJECT_NAME, "email": email_to},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def send_reset_password_email(email_to: str, email: str, token: str):
|
def send_reset_password_email(email_to: str, email: str, token: str):
|
||||||
project_name = config.PROJECT_NAME
|
project_name = settings.PROJECT_NAME
|
||||||
subject = f"{project_name} - Password recovery for user {email}"
|
subject = f"{project_name} - Password recovery for user {email}"
|
||||||
with open(Path(config.EMAIL_TEMPLATES_DIR) / "reset_password.html") as f:
|
with open(Path(settings.EMAIL_TEMPLATES_DIR) / "reset_password.html") as f:
|
||||||
template_str = f.read()
|
template_str = f.read()
|
||||||
if hasattr(token, "decode"):
|
if hasattr(token, "decode"):
|
||||||
use_token = token.decode()
|
use_token = token.decode()
|
||||||
else:
|
else:
|
||||||
use_token = token
|
use_token = token
|
||||||
server_host = config.SERVER_HOST
|
server_host = settings.SERVER_HOST
|
||||||
link = f"{server_host}/reset-password?token={use_token}"
|
link = f"{server_host}/reset-password?token={use_token}"
|
||||||
send_email(
|
send_email(
|
||||||
email_to=email_to,
|
email_to=email_to,
|
||||||
subject_template=subject,
|
subject_template=subject,
|
||||||
html_template=template_str,
|
html_template=template_str,
|
||||||
environment={
|
environment={
|
||||||
"project_name": config.PROJECT_NAME,
|
"project_name": settings.PROJECT_NAME,
|
||||||
"username": email,
|
"username": email,
|
||||||
"email": email_to,
|
"email": email_to,
|
||||||
"valid_hours": config.EMAIL_RESET_TOKEN_EXPIRE_HOURS,
|
"valid_hours": settings.EMAIL_RESET_TOKEN_EXPIRE_HOURS,
|
||||||
"link": link,
|
"link": link,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def send_new_account_email(email_to: str, username: str, password: str):
|
def send_new_account_email(email_to: str, username: str, password: str):
|
||||||
project_name = config.PROJECT_NAME
|
project_name = settings.PROJECT_NAME
|
||||||
subject = f"{project_name} - New account for user {username}"
|
subject = f"{project_name} - New account for user {username}"
|
||||||
with open(Path(config.EMAIL_TEMPLATES_DIR) / "new_account.html") as f:
|
with open(Path(settings.EMAIL_TEMPLATES_DIR) / "new_account.html") as f:
|
||||||
template_str = f.read()
|
template_str = f.read()
|
||||||
link = config.SERVER_HOST
|
link = settings.SERVER_HOST
|
||||||
send_email(
|
send_email(
|
||||||
email_to=email_to,
|
email_to=email_to,
|
||||||
subject_template=subject,
|
subject_template=subject,
|
||||||
html_template=template_str,
|
html_template=template_str,
|
||||||
environment={
|
environment={
|
||||||
"project_name": config.PROJECT_NAME,
|
"project_name": settings.PROJECT_NAME,
|
||||||
"username": username,
|
"username": username,
|
||||||
"password": password,
|
"password": password,
|
||||||
"email": email_to,
|
"email": email_to,
|
||||||
@@ -90,13 +90,13 @@ def send_new_account_email(email_to: str, username: str, password: str):
|
|||||||
|
|
||||||
|
|
||||||
def generate_password_reset_token(email):
|
def generate_password_reset_token(email):
|
||||||
delta = timedelta(hours=config.EMAIL_RESET_TOKEN_EXPIRE_HOURS)
|
delta = timedelta(hours=settings.EMAIL_RESET_TOKEN_EXPIRE_HOURS)
|
||||||
now = datetime.utcnow()
|
now = datetime.utcnow()
|
||||||
expires = now + delta
|
expires = now + delta
|
||||||
exp = expires.timestamp()
|
exp = expires.timestamp()
|
||||||
encoded_jwt = jwt.encode(
|
encoded_jwt = jwt.encode(
|
||||||
{"exp": exp, "nbf": now, "sub": password_reset_jwt_subject, "email": email},
|
{"exp": exp, "nbf": now, "sub": password_reset_jwt_subject, "email": email},
|
||||||
config.SECRET_KEY,
|
settings.SECRET_KEY,
|
||||||
algorithm="HS256",
|
algorithm="HS256",
|
||||||
)
|
)
|
||||||
return encoded_jwt
|
return encoded_jwt
|
||||||
@@ -104,7 +104,7 @@ def generate_password_reset_token(email):
|
|||||||
|
|
||||||
def verify_password_reset_token(token) -> Optional[str]:
|
def verify_password_reset_token(token) -> Optional[str]:
|
||||||
try:
|
try:
|
||||||
decoded_token = jwt.decode(token, config.SECRET_KEY, algorithms=["HS256"])
|
decoded_token = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
|
||||||
assert decoded_token["sub"] == password_reset_jwt_subject
|
assert decoded_token["sub"] == password_reset_jwt_subject
|
||||||
return decoded_token["email"]
|
return decoded_token["email"]
|
||||||
except InvalidTokenError:
|
except InvalidTokenError:
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
from raven import Client
|
from raven import Client
|
||||||
|
|
||||||
from app.core import config
|
from app.core.config import settings
|
||||||
from app.core.celery_app import celery_app
|
from app.core.celery_app import celery_app
|
||||||
|
|
||||||
client_sentry = Client(config.SENTRY_DSN)
|
client_sentry = Client(settings.SENTRY_DSN)
|
||||||
|
|
||||||
|
|
||||||
@celery_app.task(acks_late=True)
|
@celery_app.task(acks_late=True)
|
||||||
|
|||||||
40
{{cookiecutter.project_slug}}/backend/app/pyproject.toml
Normal file
40
{{cookiecutter.project_slug}}/backend/app/pyproject.toml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
[tool.poetry]
|
||||||
|
name = "app"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = ""
|
||||||
|
authors = ["Admin <admin@example.com>"]
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.7"
|
||||||
|
uvicorn = "^0.11.3"
|
||||||
|
fastapi = "^0.54.1"
|
||||||
|
pyjwt = "^1.7.1"
|
||||||
|
python-multipart = "^0.0.5"
|
||||||
|
email-validator = "^1.0.5"
|
||||||
|
requests = "^2.23.0"
|
||||||
|
celery = "^4.4.2"
|
||||||
|
passlib = {extras = ["bcrypt"], version = "^1.7.2"}
|
||||||
|
tenacity = "^6.1.0"
|
||||||
|
pydantic = "^1.4"
|
||||||
|
emails = "^0.5.15"
|
||||||
|
raven = "^6.10.0"
|
||||||
|
gunicorn = "^20.0.4"
|
||||||
|
jinja2 = "^2.11.2"
|
||||||
|
psycopg2-binary = "^2.8.5"
|
||||||
|
alembic = "^1.4.2"
|
||||||
|
sqlalchemy = "^1.3.16"
|
||||||
|
pytest = "^5.4.1"
|
||||||
|
|
||||||
|
[tool.poetry.dev-dependencies]
|
||||||
|
mypy = "^0.770"
|
||||||
|
black = "^19.10b0"
|
||||||
|
isort = "^4.3.21"
|
||||||
|
autoflake = "^1.3.1"
|
||||||
|
flake8 = "^3.7.9"
|
||||||
|
pytest = "^5.4.1"
|
||||||
|
jupyter = "^1.0.0"
|
||||||
|
vulture = "^1.4"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry>=0.12"]
|
||||||
|
build-backend = "poetry.masonry.api"
|
||||||
@@ -1,6 +1,16 @@
|
|||||||
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7
|
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7
|
||||||
|
|
||||||
RUN pip install celery~=4.3 passlib[bcrypt] tenacity requests emails "fastapi>=0.16.0" uvicorn gunicorn pyjwt python-multipart email_validator jinja2 psycopg2-binary alembic SQLAlchemy
|
WORKDIR /app/
|
||||||
|
|
||||||
|
# Install Poetry
|
||||||
|
RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | POETRY_HOME=/opt/poetry python && \
|
||||||
|
cd /usr/local/bin && \
|
||||||
|
ln -s /opt/poetry/bin/poetry && \
|
||||||
|
poetry config virtualenvs.create false
|
||||||
|
|
||||||
|
# Copy poetry.lock* in case it doesn't exist in the repo
|
||||||
|
COPY ./app/pyproject.toml ./app/poetry.lock* /app/
|
||||||
|
RUN poetry install --no-dev --no-root
|
||||||
|
|
||||||
# For development, Jupyter remote kernel, Hydrogen
|
# For development, Jupyter remote kernel, Hydrogen
|
||||||
# Using inside the container:
|
# Using inside the container:
|
||||||
@@ -10,7 +20,6 @@ RUN bash -c "if [ $env == 'dev' ] ; then pip install jupyterlab ; fi"
|
|||||||
EXPOSE 8888
|
EXPOSE 8888
|
||||||
|
|
||||||
COPY ./app /app
|
COPY ./app /app
|
||||||
WORKDIR /app/
|
|
||||||
|
|
||||||
ENV PYTHONPATH=/app
|
ENV PYTHONPATH=/app
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
FROM python:3.7
|
FROM python:3.7
|
||||||
|
|
||||||
RUN pip install raven celery~=4.3 passlib[bcrypt] tenacity requests "fastapi>=0.16.0" emails pyjwt email_validator jinja2 psycopg2-binary alembic SQLAlchemy
|
WORKDIR /app/
|
||||||
|
|
||||||
|
# Install Poetry
|
||||||
|
RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | POETRY_HOME=/opt/poetry python && \
|
||||||
|
cd /usr/local/bin && \
|
||||||
|
ln -s /opt/poetry/bin/poetry && \
|
||||||
|
poetry config virtualenvs.create false
|
||||||
|
|
||||||
|
# Copy poetry.lock* in case it doesn't exist in the repo
|
||||||
|
COPY ./app/pyproject.toml ./app/poetry.lock* /app/
|
||||||
|
RUN poetry install --no-dev --no-root
|
||||||
|
|
||||||
# For development, Jupyter remote kernel, Hydrogen
|
# For development, Jupyter remote kernel, Hydrogen
|
||||||
# Using inside the container:
|
# Using inside the container:
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
FROM python:3.7
|
FROM python:3.7
|
||||||
|
|
||||||
RUN pip install requests pytest tenacity passlib[bcrypt] "fastapi>=0.16.0" psycopg2-binary SQLAlchemy
|
WORKDIR /app/
|
||||||
|
|
||||||
|
# Install Poetry
|
||||||
|
RUN curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py | POETRY_HOME=/opt/poetry python && \
|
||||||
|
cd /usr/local/bin && \
|
||||||
|
ln -s /opt/poetry/bin/poetry && \
|
||||||
|
poetry config virtualenvs.create false
|
||||||
|
|
||||||
|
# Copy poetry.lock* in case it doesn't exist in the repo
|
||||||
|
COPY ./app/pyproject.toml ./app/poetry.lock* /app/
|
||||||
|
RUN poetry install --no-dev --no-root
|
||||||
|
|
||||||
# For development, Jupyter remote kernel, Hydrogen
|
# For development, Jupyter remote kernel, Hydrogen
|
||||||
# Using inside the container:
|
# Using inside the container:
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ default_context:
|
|||||||
pgadmin_default_user_password: '{{ cookiecutter.pgadmin_default_user_password }}'
|
pgadmin_default_user_password: '{{ cookiecutter.pgadmin_default_user_password }}'
|
||||||
traefik_constraint_tag: '{{ cookiecutter.traefik_constraint_tag }}'
|
traefik_constraint_tag: '{{ cookiecutter.traefik_constraint_tag }}'
|
||||||
traefik_constraint_tag_staging: '{{ cookiecutter.traefik_constraint_tag_staging }}'
|
traefik_constraint_tag_staging: '{{ cookiecutter.traefik_constraint_tag_staging }}'
|
||||||
traefik_public_network: '{{ cookiecutter.traefik_public_network }}'
|
|
||||||
traefik_public_constraint_tag: '{{ cookiecutter.traefik_public_constraint_tag }}'
|
traefik_public_constraint_tag: '{{ cookiecutter.traefik_public_constraint_tag }}'
|
||||||
flower_auth: '{{ cookiecutter.flower_auth }}'
|
flower_auth: '{{ cookiecutter.flower_auth }}'
|
||||||
sentry_dsn: '{{ cookiecutter.sentry_dsn }}'
|
sentry_dsn: '{{ cookiecutter.sentry_dsn }}'
|
||||||
|
|||||||
@@ -8,11 +8,8 @@ services:
|
|||||||
- traefik.port=5050
|
- traefik.port=5050
|
||||||
- traefik.tags=${TRAEFIK_PUBLIC_TAG}
|
- traefik.tags=${TRAEFIK_PUBLIC_TAG}
|
||||||
- traefik.docker.network=${TRAEFIK_PUBLIC_NETWORK}
|
- traefik.docker.network=${TRAEFIK_PUBLIC_NETWORK}
|
||||||
# Traefik service that listens to HTTP
|
- traefik.frontend.entryPoints=http,https
|
||||||
- traefik.redirectorservice.frontend.entryPoints=http
|
- traefik.frontend.redirect.entryPoint=https
|
||||||
- traefik.redirectorservice.frontend.redirect.entryPoint=https
|
|
||||||
# Traefik service that listens to HTTPS
|
|
||||||
- traefik.webservice.frontend.entryPoints=https
|
|
||||||
proxy:
|
proxy:
|
||||||
deploy:
|
deploy:
|
||||||
labels:
|
labels:
|
||||||
@@ -25,18 +22,15 @@ services:
|
|||||||
- traefik.port=80
|
- traefik.port=80
|
||||||
- traefik.tags=${TRAEFIK_PUBLIC_TAG}
|
- traefik.tags=${TRAEFIK_PUBLIC_TAG}
|
||||||
- traefik.docker.network=${TRAEFIK_PUBLIC_NETWORK}
|
- traefik.docker.network=${TRAEFIK_PUBLIC_NETWORK}
|
||||||
# Traefik service that listens to HTTP
|
- traefik.frontend.entryPoints=http,https
|
||||||
- traefik.servicehttp.frontend.entryPoints=http
|
- traefik.frontend.redirect.entryPoint=https
|
||||||
- traefik.servicehttp.frontend.redirect.entryPoint=https
|
|
||||||
# Traefik service that listens to HTTPS
|
|
||||||
- traefik.servicehttps.frontend.entryPoints=https
|
|
||||||
# Uncomment the config line below to detect and redirect www to non-www (or the contrary)
|
# Uncomment the config line below to detect and redirect www to non-www (or the contrary)
|
||||||
# The lines above for traefik.frontend.rule are needed too
|
# The lines above for traefik.frontend.rule are needed too
|
||||||
# - "traefik.servicehttps.frontend.redirect.regex=^https?://(www.)?(${DOMAIN})/(.*)"
|
# - "traefik.frontend.redirect.regex=^https?://(www.)?(${DOMAIN})/(.*)"
|
||||||
# To redirect from non-www to www un-comment the line below
|
# To redirect from non-www to www un-comment the line below
|
||||||
# - "traefik.servicehttps.frontend.redirect.replacement=https://www.${DOMAIN}/$$3"
|
# - "traefik.frontend.redirect.replacement=https://www.${DOMAIN}/$$3"
|
||||||
# To redirect from www to non-www un-comment the line below
|
# To redirect from www to non-www un-comment the line below
|
||||||
# - "traefik.servicehttps.frontend.redirect.replacement=https://${DOMAIN}/$$3"
|
# - "traefik.frontend.redirect.replacement=https://${DOMAIN}/$$3"
|
||||||
flower:
|
flower:
|
||||||
deploy:
|
deploy:
|
||||||
labels:
|
labels:
|
||||||
@@ -45,11 +39,8 @@ services:
|
|||||||
- traefik.port=5555
|
- traefik.port=5555
|
||||||
- traefik.tags=${TRAEFIK_PUBLIC_TAG}
|
- traefik.tags=${TRAEFIK_PUBLIC_TAG}
|
||||||
- traefik.docker.network=${TRAEFIK_PUBLIC_NETWORK}
|
- traefik.docker.network=${TRAEFIK_PUBLIC_NETWORK}
|
||||||
# Traefik service that listens to HTTP
|
- traefik.frontend.entryPoints=http,https
|
||||||
- traefik.redirectorservice.frontend.entryPoints=http
|
- traefik.frontend.redirect.entryPoint=https
|
||||||
- traefik.redirectorservice.frontend.redirect.entryPoint=https
|
|
||||||
# Traefik service that listens to HTTPS
|
|
||||||
- traefik.webservice.frontend.entryPoints=https
|
|
||||||
backend:
|
backend:
|
||||||
deploy:
|
deploy:
|
||||||
labels:
|
labels:
|
||||||
|
|||||||
@@ -14,5 +14,5 @@ services:
|
|||||||
- default
|
- default
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
{{cookiecutter.traefik_public_network}}:
|
traefik-public:
|
||||||
external: true
|
external: true
|
||||||
|
|||||||
@@ -12,3 +12,4 @@ services:
|
|||||||
backend-tests:
|
backend-tests:
|
||||||
environment:
|
environment:
|
||||||
- JUPYTER=jupyter lab --ip=0.0.0.0 --allow-root --NotebookApp.custom_display_url=http://127.0.0.1:8888
|
- JUPYTER=jupyter lab --ip=0.0.0.0 --allow-root --NotebookApp.custom_display_url=http://127.0.0.1:8888
|
||||||
|
- SERVER_HOST=http://${DOMAIN}
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
flower:
|
flower:
|
||||||
image: totem/celery-flower-docker
|
image: mher/flower
|
||||||
env_file:
|
env_file:
|
||||||
- env-flower.env
|
- env-flower.env
|
||||||
|
command:
|
||||||
|
- "--broker=amqp://guest@queue:5672//"
|
||||||
|
# For the "Broker" tab to work in the flower UI, uncomment the following command argument,
|
||||||
|
# and change the queue service's image as described in docker-compose.shared.base-images.yml
|
||||||
|
# - "--broker_api=http://guest:guest@queue:15672/api//"
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
version: '3.3'
|
version: '3.3'
|
||||||
services:
|
services:
|
||||||
db:
|
db:
|
||||||
image: postgres:11
|
image: postgres:12
|
||||||
queue:
|
queue:
|
||||||
image: rabbitmq:3
|
image: rabbitmq:3
|
||||||
|
# Using the below image instead is required to enable the "Broker" tab in the flower UI:
|
||||||
|
# image: rabbitmq:3-management
|
||||||
|
#
|
||||||
|
# You also have to change the flower command as documented in docker-compose.shared.admin.yml
|
||||||
|
|||||||
@@ -17,4 +17,5 @@ services:
|
|||||||
- env-postgres.env
|
- env-postgres.env
|
||||||
- env-backend.env
|
- env-backend.env
|
||||||
environment:
|
environment:
|
||||||
|
- SERVER_NAME=${DOMAIN}
|
||||||
- SERVER_HOST=https://${DOMAIN}
|
- SERVER_HOST=https://${DOMAIN}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ services:
|
|||||||
- env-postgres.env
|
- env-postgres.env
|
||||||
environment:
|
environment:
|
||||||
- SERVER_NAME=backend
|
- SERVER_NAME=backend
|
||||||
|
- SERVER_HOST=http://${DOMAIN}
|
||||||
backend:
|
backend:
|
||||||
environment:
|
environment:
|
||||||
# Don't send emails during testing
|
# Don't send emails during testing
|
||||||
|
|||||||
@@ -1,3 +1 @@
|
|||||||
FLOWER_BASIC_AUTH={{cookiecutter.flower_auth}}
|
FLOWER_BASIC_AUTH={{cookiecutter.flower_auth}}
|
||||||
AMQP_ADMIN_HOST=queue
|
|
||||||
AMQP_HOST=queue
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
"presets": [
|
"presets": [
|
||||||
[
|
[
|
||||||
"@vue/app",
|
"@vue/cli-plugin-babel/preset",
|
||||||
{
|
{
|
||||||
"useBuiltIns": "entry"
|
"useBuiltIns": "entry"
|
||||||
}
|
}
|
||||||
|
|||||||
13952
{{cookiecutter.project_slug}}/frontend/package-lock.json
generated
13952
{{cookiecutter.project_slug}}/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -5,12 +5,13 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
"build": "vue-cli-service build",
|
"build": "vue-cli-service build",
|
||||||
"lint": "vue-cli-service lint",
|
"test:unit": "vue-cli-service test:unit",
|
||||||
"test:unit": "vue-cli-service test:unit"
|
"lint": "vue-cli-service lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/polyfill": "^7.2.5",
|
"@babel/polyfill": "^7.2.5",
|
||||||
"axios": "^0.18.0",
|
"axios": "^0.18.0",
|
||||||
|
"core-js": "^3.4.3",
|
||||||
"register-service-worker": "^1.0.0",
|
"register-service-worker": "^1.0.0",
|
||||||
"typesafe-vuex": "^3.1.1",
|
"typesafe-vuex": "^3.1.1",
|
||||||
"vee-validate": "^2.1.7",
|
"vee-validate": "^2.1.7",
|
||||||
@@ -23,16 +24,16 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^23.3.13",
|
"@types/jest": "^23.3.13",
|
||||||
"@vue/cli-plugin-babel": "^3.3.0",
|
"@vue/cli-plugin-babel": "^4.1.1",
|
||||||
"@vue/cli-plugin-pwa": "^3.3.0",
|
"@vue/cli-plugin-pwa": "^4.1.1",
|
||||||
"@vue/cli-plugin-typescript": "^3.3.0",
|
"@vue/cli-plugin-typescript": "^4.1.1",
|
||||||
"@vue/cli-plugin-unit-jest": "^3.5.0",
|
"@vue/cli-plugin-unit-jest": "^4.1.1",
|
||||||
"@vue/cli-service": "^3.3.1",
|
"@vue/cli-service": "^4.1.1",
|
||||||
"@vue/test-utils": "^1.0.0-beta.28",
|
"@vue/test-utils": "^1.0.0-beta.28",
|
||||||
"babel-core": "7.0.0-bridge.0",
|
"babel-core": "7.0.0-bridge.0",
|
||||||
"ts-jest": "^23.10.5",
|
"ts-jest": "^23.10.5",
|
||||||
"typescript": "^3.2.4",
|
"typescript": "^3.2.4",
|
||||||
"vue-cli-plugin-vuetify": "^0.2.1",
|
"vue-cli-plugin-vuetify": "^2.0.2",
|
||||||
"vue-template-compiler": "^2.5.22"
|
"vue-template-compiler": "^2.5.22"
|
||||||
},
|
},
|
||||||
"postcss": {
|
"postcss": {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import { readUserProfile } from '@/store/main/getters';
|
|||||||
export default class Dashboard extends Vue {
|
export default class Dashboard extends Vue {
|
||||||
get greetedUser() {
|
get greetedUser() {
|
||||||
const userProfile = readUserProfile(this.$store);
|
const userProfile = readUserProfile(this.$store);
|
||||||
if (userProfile && userProfile.full_name) {
|
if (userProfile) {
|
||||||
if (userProfile.full_name) {
|
if (userProfile.full_name) {
|
||||||
return userProfile.full_name;
|
return userProfile.full_name;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -5,6 +5,6 @@ set -e
|
|||||||
|
|
||||||
TAG=${TAG} \
|
TAG=${TAG} \
|
||||||
FRONTEND_ENV=${FRONTEND_ENV-production} \
|
FRONTEND_ENV=${FRONTEND_ENV-production} \
|
||||||
source ./scripts/build.sh
|
. ./scripts/build.sh
|
||||||
|
|
||||||
docker-compose -f docker-stack.yml push
|
docker-compose -f docker-stack.yml push
|
||||||
|
|||||||
@@ -27,4 +27,4 @@ docker-compose \
|
|||||||
docker-compose -f docker-stack.yml build
|
docker-compose -f docker-stack.yml build
|
||||||
docker-compose -f docker-stack.yml down -v --remove-orphans # Remove possibly previous broken stacks left hanging after an error
|
docker-compose -f docker-stack.yml down -v --remove-orphans # Remove possibly previous broken stacks left hanging after an error
|
||||||
docker-compose -f docker-stack.yml up -d
|
docker-compose -f docker-stack.yml up -d
|
||||||
docker-compose -f docker-stack.yml exec -T backend-tests /tests-start.sh
|
docker-compose -f docker-stack.yml exec -T backend-tests /tests-start.sh "$@"
|
||||||
|
|||||||
@@ -15,5 +15,5 @@ config > docker-stack.yml
|
|||||||
docker-compose -f docker-stack.yml build
|
docker-compose -f docker-stack.yml build
|
||||||
docker-compose -f docker-stack.yml down -v --remove-orphans # Remove possibly previous broken stacks left hanging after an error
|
docker-compose -f docker-stack.yml down -v --remove-orphans # Remove possibly previous broken stacks left hanging after an error
|
||||||
docker-compose -f docker-stack.yml up -d
|
docker-compose -f docker-stack.yml up -d
|
||||||
docker-compose -f docker-stack.yml exec -T backend-tests /tests-start.sh
|
docker-compose -f docker-stack.yml exec -T backend-tests /tests-start.sh "$@"
|
||||||
docker-compose -f docker-stack.yml down -v --remove-orphans
|
docker-compose -f docker-stack.yml down -v --remove-orphans
|
||||||
|
|||||||
Reference in New Issue
Block a user