├── .docker ├── dev.env ├── graylog │ └── dev.env └── test.env ├── .dockerignore ├── .editorconfig ├── .env-example ├── .gitignore ├── Dockerfile ├── Dockerfile-prd ├── Jenkinsfile ├── Makefile ├── README.md ├── __version__.py ├── apps ├── __init__.py ├── app.py ├── auth │ ├── __init__.py │ ├── commands.py │ ├── exceptions.py │ ├── resources.py │ ├── schemas.py │ └── use_case.py ├── events │ ├── __init__.py │ └── user_created.py ├── extensions │ ├── __init__.py │ ├── api.py │ ├── config.py │ ├── db.py │ ├── jwt.py │ ├── logging.py │ ├── messages.py │ └── responses.py └── users │ ├── __init__.py │ ├── commands.py │ ├── exceptions.py │ ├── models.py │ ├── repositories.py │ ├── resources.py │ ├── resources_admin.py │ ├── schemas.py │ ├── use_case.py │ └── utils.py ├── boot.sh ├── confs ├── gunicorn_api_users.conf.py ├── nginx_api_users.conf ├── nginx_api_users_https.conf └── supervisor_api_users.conf ├── docker-compose.yaml ├── fabfile.py ├── gunicorn_settings.py ├── poetry.lock ├── pyproject.toml ├── pytest.ini ├── requirements.txt ├── requirements └── base.txt ├── run.py ├── setup.cfg ├── setup.py ├── static ├── Insomnia_2023-05-11.json ├── coverage.svg ├── create-superuser-with-docker.gif ├── create-superuser.gif ├── docker-create-user.gif ├── login-and-fetch-users.gif ├── make-test.gif └── openapi3.png └── tests ├── __init__.py ├── auth ├── __init__.py └── test_resources.py ├── conftest.py ├── factories ├── __init__.py └── users.py ├── home ├── __init__.py └── test_home.py ├── messages └── test_messages.py ├── responses └── test_responses.py └── users ├── __init__.py ├── test_admin_user_by_cpf.py ├── test_admin_user_page_list.py ├── test_check_password_in_signup.py ├── test_cpf.py ├── test_generate_password.py ├── test_models.py ├── test_repositories.py └── test_resources.py /.docker/dev.env: -------------------------------------------------------------------------------- 1 | FLASK_APP=run:app 2 | FLASK_ENV=default 3 | SECRET_KEY=strong-key 4 | PORT=8080 5 | DEBUG=False 6 | JWT_ACCESS_TOKEN_EXPIRES=20 7 | JWT_REFRESH_TOKEN_EXPIRES=30 8 | MONGODB_URI=mongodb://mongodb:27017/api-users 9 | -------------------------------------------------------------------------------- /.docker/graylog/dev.env: -------------------------------------------------------------------------------- 1 | DEBUG=True 2 | GRAYLOG_HTTP_ENDPOINT=http://graylog:12201/gelf 3 | GRAYLOG_UDP_ENDPOINT_IP=graylog 4 | GRAYLOG_UDP_ENDPOINT_PORT=12201 5 | GRAYLOG_HTTP_EXTERNAL_URI=http://localhost:12200/ 6 | GRAYLOG_ROOT_PASSWORD_SHA2=e8c6ba34f6fadc59d0a64e6229baddbe0cea2b5ea31e18b8c81dce47249fe5eb 7 | -------------------------------------------------------------------------------- /.docker/test.env: -------------------------------------------------------------------------------- 1 | MONGODB_URI_TEST=mongodb://mongodb:27017/api-users-test 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .ash_history 2 | .cache 3 | .coverage 4 | .editorconfig 5 | .env 6 | .env-example 7 | .git 8 | .gitignore 9 | .pytest_cache 10 | .vscode 11 | .idea 12 | .cache/ 13 | __pycache__/ 14 | *.pytest_cache/ 15 | .venv 16 | env/ 17 | venv/ 18 | .DS_Store 19 | *.eggs 20 | *.egg-info 21 | *.py[cod] 22 | *.swp 23 | *.log 24 | .config 25 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | # 4 space indentation 13 | [*.py] 14 | charset = utf-8 15 | indent_style = space 16 | indent_size = 4 17 | 18 | [*.{md,yml,yaml,feature}] 19 | indent_style = space 20 | indent_size = 2 21 | 22 | # Tab indentation (no size specified) 23 | [Makefile] 24 | indent_style = tab 25 | -------------------------------------------------------------------------------- /.env-example: -------------------------------------------------------------------------------- 1 | FLASK_APP=run:app 2 | FLASK_CONFIG=default 3 | SECRET_KEY=strong-key 4 | PORT=8080 5 | DEBUG=False 6 | JWT_ACCESS_TOKEN_EXPIRES=20 7 | JWT_REFRESH_TOKEN_EXPIRES=30 8 | MONGODB_URI=mongodb://0.0.0.0:27017/api-users 9 | MONGODB_URI_TEST=mongodb://0.0.0.0:27017/api-users-test 10 | ; MONGODB_URI=mongodb://mongo-latest:27017/api-users 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | __pycache__/ 3 | *.pytest_cache/ 4 | .local/ 5 | .ash_history 6 | local_settings.py 7 | *.py[cod] 8 | *.swp 9 | *.log 10 | .DS_Store 11 | .env 12 | .venv 13 | env/ 14 | *.eggs 15 | *.egg-info 16 | venv/ 17 | .vscode 18 | .idea 19 | .cache/ 20 | # Elastic Beanstalk Files 21 | .elasticbeanstalk/* 22 | !.elasticbeanstalk/*.cfg.yml 23 | !.elasticbeanstalk/*.global.yml 24 | notes.md 25 | celerybeat-schedule 26 | .coverage 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-alpine as base 2 | ENV HOME=/home/app \ 3 | POETRY_VIRTUALENVS_PATH=/home/app/venv \ 4 | POETRY_HOME=/home/app/poetry \ 5 | PATH="/home/app/venv/bin:$PATH" \ 6 | PYTHONDONTWRITEBYTECODE=1 \ 7 | PYTHONUNBUFFERED=1 8 | 9 | RUN rm -rf /var/cache/apk/* && \ 10 | apk --no-cache update && \ 11 | apk add make && \ 12 | apk add build-base && \ 13 | apk add gcc && \ 14 | apk add python3-dev && \ 15 | apk add libffi-dev && \ 16 | apk add musl-dev && \ 17 | apk add openssl-dev && \ 18 | apk add curl && \ 19 | apk del build-base && \ 20 | rm -rf /var/cache/apk/* && \ 21 | python -m venv /home/app/venv && \ 22 | /home/app/venv/bin/pip install --upgrade pip && \ 23 | rm -rf /home/app/.config/pypoetry/ && \ 24 | pip install poetry && \ 25 | poetry config virtualenvs.path /home/app/venv && \ 26 | poetry config virtualenvs.create false 27 | 28 | # DEVLOPMENT 29 | FROM base as dev 30 | # USER 1000:1000 31 | WORKDIR $HOME 32 | 33 | # CI 34 | FROM dev as ci 35 | WORKDIR $HOME 36 | COPY . . 37 | RUN rm -rf /home/app/.config/pypoetry/ && \ 38 | source /home/app/venv/bin/activate && \ 39 | poetry install --with dev,test,docs 40 | 41 | # PROD 42 | FROM ci as build 43 | RUN rm -rf /home/app/venv && \ 44 | python -m venv /home/app/venv && \ 45 | pip install poetry && \ 46 | poetry config virtualenvs.path /home/app/venv && \ 47 | poetry config virtualenvs.create false && \ 48 | poetry install --with main,prod --without dev,test,docs 49 | 50 | # SHIPMENT 51 | 52 | FROM base 53 | WORKDIR $HOME 54 | COPY --chown=nobody --from=build /home/app/venv /home/app/venv 55 | COPY --chown=nobody --from=build ${HOME} ${HOME} 56 | USER nobody 57 | # ENTRYPOINT ["tail", "-f", "/dev/null"] 58 | ENTRYPOINT [ "gunicorn", "--config", "gunicorn_settings.py", "-b", ":8080", "run:app" ] 59 | -------------------------------------------------------------------------------- /Dockerfile-prd: -------------------------------------------------------------------------------- 1 | FROM python:3.6-alpine 2 | RUN rm -rf /var/cache/apk/* && \ 3 | apk update && \ 4 | apk add make && \ 5 | apk add build-base && \ 6 | apk add gcc && \ 7 | apk add python3-dev && \ 8 | apk add libffi-dev && \ 9 | apk add musl-dev && \ 10 | apk add openssl-dev && \ 11 | apk del build-base && \ 12 | rm -rf /var/cache/apk/* 13 | 14 | ENV HOME=/home/api FLASK_APP=application.py FLASK_ENV=production WORKERS=4 PORT=5000 15 | RUN adduser -D api 16 | USER api 17 | WORKDIR $HOME 18 | COPY --chown=api:api . $HOME 19 | 20 | RUN python -m venv venv && \ 21 | venv/bin/pip install --upgrade pip && \ 22 | venv/bin/pip install -r requirements/prod.txt 23 | 24 | EXPOSE 5000 25 | ENTRYPOINT [ "./boot.sh" ] 26 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | environment { 3 | MONGODB_URI_TEST = credentials('API_USERS_MONGO_DB_TEST') 4 | FLASK_ENV = 'testing' 5 | FLASK_APP = 'application.py' 6 | DEBUG = true 7 | HOSTS = credentials('API_USERS_DEPLOY_HOSTS') 8 | } 9 | options 10 | { 11 | skipDefaultCheckout(true) 12 | buildDiscarder(logRotator(numToKeepStr: '3', daysToKeepStr: '7')) 13 | timestamps() 14 | } 15 | agent any 16 | stages { 17 | stage('Checkout') { 18 | steps { 19 | checkout scm 20 | } 21 | } 22 | stage ("Install Dependencies") { 23 | steps { 24 | sh """ 25 | python3 -m venv .venv 26 | source .venv/bin/activate 27 | pip install --upgrade pip 28 | pip install -r ${env.WORKSPACE}/requirements/test.txt 29 | """ 30 | } 31 | } 32 | stage('Run Tests') { 33 | steps { 34 | sh """ 35 | source .venv/bin/activate 36 | echo "Running the unit test..." 37 | make clean 38 | make coverage 39 | """ 40 | } 41 | } 42 | stage('Generate Release and deploy') { 43 | steps { 44 | script { 45 | def version = readFile encoding: 'utf-8', file: '__version__.py' 46 | def message = "Latest ${version}. New version:" 47 | def releaseInput = input( 48 | id: 'userInput', 49 | message: "${message}", 50 | parameters: [ 51 | [ 52 | $class: 'TextParameterDefinition', 53 | defaultValue: 'uat', 54 | description: 'Release candidate', 55 | name: 'rc' 56 | ] 57 | ] 58 | ) 59 | sh """ 60 | make release v=${releaseInput} 61 | source .venv/bin/activate 62 | fab -H ${env.HOSTS} deploy --tag ${releaseInput} 63 | """ 64 | } 65 | } 66 | } 67 | } 68 | post { 69 | always { 70 | sh """ 71 | rm -rf .venv 72 | """ 73 | } 74 | success { 75 | echo "Success" 76 | } 77 | failure { 78 | echo "Send e-mail, when failed" 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GIT_CURRENT_BRANCH := ${shell git symbolic-ref --short HEAD} 2 | 3 | .PHONY: help clean test clean-build isort run 4 | 5 | .DEFAULT: help 6 | 7 | help: 8 | @echo "make clean:" 9 | @echo " Removes all pyc, pyo and __pycache__" 10 | @echo "" 11 | @echo "make clean-build:" 12 | @echo " Clear all build directories" 13 | @echo "" 14 | @echo "make clone-dotenv" 15 | @echo " Creates .env file base on .env-example" 16 | @echo " Used by setup_dev command" 17 | @echo "" 18 | @echo "make isort:" 19 | @echo " Run isort command cli in development features" 20 | @echo "" 21 | @echo "make lint:" 22 | @echo " Run lint" 23 | @echo "" 24 | @echo "make coverage:" 25 | @echo " Run tests with coverage and generate a badge (svg)" 26 | @echo "" 27 | @echo "make test:" 28 | @echo " Run tests with coverage, lint, and clean commands" 29 | @echo "" 30 | @echo "make dev:" 31 | @echo " Run the dev web application, with tests and coverage" 32 | @echo "" 33 | @echo "make run:" 34 | @echo " Run the web application without tests" 35 | @echo "" 36 | @echo "make release:" 37 | @echo " Creates a new tag and set the version in this package" 38 | @echo " Ex: make release v=1.0.0" 39 | @echo "" 40 | 41 | clean: 42 | find . -name '*.pyc' -exec rm --force {} + 43 | find . -name '*.pyo' -exec rm --force {} + 44 | find . | grep -E "__pycache__|.pytest_cache|.pyc|.DS_Store$$" | xargs rm -rf 45 | 46 | clean-build: 47 | rm --force --recursive build/ 48 | rm --force --recursive dist/ 49 | rm --force --recursive *.egg-info 50 | 51 | clone-dotenv: 52 | @echo "---- Clone dotenv ----" 53 | @cp .env-example .env 54 | @echo "---- Finish clone ----" 55 | 56 | coverage: test 57 | @echo "---- Create coverage ----" 58 | @coverage-badge > static/coverage.svg 59 | 60 | isort: 61 | sh -c "isort --skip-glob=.tox --recursive . " 62 | 63 | lint: clean 64 | pylint 65 | 66 | test: 67 | @pytest -s --verbose --disable-warnings --cov=apps --color=yes tests/ 68 | 69 | dev: lint test 70 | python run.py 71 | 72 | release: 73 | @echo "creating a new release ${v}" 74 | @echo "version = '${v}'" > `pwd`/__version__.py 75 | @git add `pwd`/__version__.py 76 | @git add `pwd`/static/coverage.svg 77 | @git commit -m '${v}' 78 | @git tag ${v} 79 | @git push origin ${v} 80 | @git push --set-upstream origin "${GIT_CURRENT_BRANCH}" 81 | @git push origin 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flask-api-users 2 | 3 | Api para gerenciamento de usuários e administradores com Flask Framework, MongoDB e autenticação JWT. 4 | 5 | Os administradores podem gerenciar usuarios cadastrados no micro serviço. 6 | 7 | Os usuários pode ser qualquer ator, como por exemplo customers, que necessita se autenticar e ter acesso ao perfil, configurações e etc... 8 | 9 | Para autenticação utilizamos o JWT tanto para admins quanto para users. 10 | 11 | 12 | ![coverage](./static/coverage.svg) 13 | 14 | ## Crie MongoDB 15 | 16 | ```shell 17 | docker run -itd --name mongo-latest -p 27017:27017 --network local-containers mongo 18 | ``` 19 | 20 | ## Setup com virtualenv 21 | 22 | Inicializar o virtualenv `python3 -m venv venv` e em seguida ativar o virtualenv `source venv/bin/activate` 23 | 24 | Se houver o `poetry` instalado pode executar `poetry install` ou instalar via pip `pip install -r requirements/base.txt` 25 | 26 | #### Excute a aplicação de dev 27 | 28 | Necessário que o mongodb esteja executando 29 | 30 | ```shell 31 | $ python run.py 32 | ``` 33 | 34 | ou 35 | 36 | ```shell 37 | $ flask run --host=0.0.0.0 --port=8080 --debugger 38 | ``` 39 | 40 | ![Login and fetch users](./static/login-and-fetch-users.gif) 41 | 42 | 43 | ## Criar um superusuario para executar os endpoints 44 | 45 | Via local 46 | 47 | ```shell 48 | $ flask createsuperuser admin admin@admin.com teste123 49 | ``` 50 | 51 | ![](./static/create-superuser.gif) 52 | 53 | 54 | ## Excute a aplicação via docker 55 | 56 | Primeiro faço um build da minha imagem 57 | 58 | ```shell 59 | $ docker build -t users . 60 | ``` 61 | 62 | Em seguida executar o container a partir da imagem criada. Altere seu `.env` e descomente a linha `; MONGODB_URI=mongodb://mongo-latest:27017/api-users` colocando o hostname do mongo `mongo-latest`. Então crie o container com o comando abaixo. 63 | 64 | ```shell 65 | $ docker run -itd --name users_local --env-file ./.env -p 8080:8080 --network local-containers users 66 | ``` 67 | 68 | Criar superuser via docker. 69 | 70 | ![](./static/create-superuser-with-docker.gif) 71 | 72 | ## Criando um usuario ou um customers de exemplo, com validação de cpf 73 | 74 | ![Create user example](./static/docker-create-user.gif) 75 | 76 | ## Docker compose 77 | 78 | A ser testado. Não utilize essa opção por enquanto. 79 | 80 | ## Insomnia collection 81 | 82 | Pode baixar a coleção do insomnia na pasta static e importar no app 83 | 84 | [Clique aqui para baixar](./static/Insomnia_2023-05-11.json) 85 | 86 | ## OpenApi3 87 | 88 | Levante o servidor `python run.py` e acesse no browser o endereço `http://0.0.0.0:8080/swagger-ui/#/` 89 | 90 | ![OpenApi](./static/openapi3.png) 91 | 92 | ## Testes 93 | 94 | ```shell 95 | $ pytest 96 | ``` 97 | 98 | ou 99 | 100 | ```shell 101 | $ make test 102 | ``` 103 | 104 | ![](./static/make-test.gif) 105 | 106 | ## Roadmap 107 | 108 | * Capítulo 1: [Introdução, configuração e Hello World](https://www.lucassimon.com.br/2018/06/serie-api-em-flask---parte-1---introducao-configuracao-e-hello-world/) 109 | 110 | * Capítulo 2: [Organizando as dependências e requerimentos](https://lucassimon.com.br/2018/06/serie-api-em-flask---parte-2---organizando-as-dependencias-e-requerimentos/) 111 | 112 | * Capítulo 3: [Configurando o pytest e nosso primeiro teste](https://lucassimon.com.br/2018/06/serie-api-em-flask---parte-3---configurando-o-pytest-e-nosso-primeiro-teste/) 113 | 114 | * Capítulo 4: [Configurando o Makefile](https://lucassimon.com.br/2018/06/serie-api-em-flask---parte-4---configurando-o-makefile/) 115 | 116 | * Capítulo 5: [Adicionando o MongoDB a API](https://lucassimon.com.br/2018/07/serie-api-em-flask---parte-5---mongodb/) 117 | 118 | * Capítulo 6: [Criando e testando nosso modelo de usuários](https://lucassimon.com.br/2018/10/serie-api-em-flask---parte-6---criando-e-testando-nosso-modelo-de-usuarios/) 119 | 120 | * Capítulo 7: [Criando usuários](https://lucassimon.com.br/2018/10/serie-api-em-flask---parte-7---criando-usuarios/) 121 | 122 | * Capítulo 8: [Listando usuários](https://lucassimon.com.br/2018/10/serie-api-em-flask---parte-8---listando-usuarios/) 123 | 124 | * Capítulo 9: [Buscando usuários](https://lucassimon.com.br/2018/10/serie-api-em-flask---parte-9---buscando-usuarios/) 125 | 126 | * Capítulo 10: [Editando um usuário](https://lucassimon.com.br/2018/10/serie-api-em-flask---parte-10---editando-um-usuario/) 127 | 128 | * Capítulo 11: [Deletando um usuário](https://lucassimon.com.br/2018/10/serie-api-em-flask---parte-11---deletando-um-usuario/) 129 | 130 | * Capítulo 12: [Autênticação por JWT](https://lucassimon.com.br/2018/10/serie-api-em-flask---parte-12---autenticacao-por-jwt/) 131 | 132 | * Capítulo 13: [Criando um container docker](https://lucassimon.com.br/2018/10/serie-api-em-flask---parte-13---criando-um-container-docker/) 133 | 134 | * Capítulo 14: [Deploy Flask na Digital Ocean](https://lucassimon.com.br/2018/10/serie-api-em-flask---parte-14---arquivos-de-configuracao-para-deploy-na-digital-ocean/) 135 | 136 | * Capítulo 15: [Automatizando o processo de deploy com Fabric](https://lucassimon.com.br/2018/11/serie-api-em-flask---parte-15---automatizando-o-processo-de-deploy-com-fabric/) 137 | 138 | * Capítulo 16: [CI e CD com Jenkins, Python, Flask e Fabric](https://lucassimon.com.br/2018/11/serie-api-em-flask---parte-16---ci-e-cd-com-jenkins-python-flask-e-fabric/) 139 | -------------------------------------------------------------------------------- /__version__.py: -------------------------------------------------------------------------------- 1 | version = '1.0.29' 2 | -------------------------------------------------------------------------------- /apps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucassimon/flask-api-users/88da2933b260d698c8513299ba64957e08230762/apps/__init__.py -------------------------------------------------------------------------------- /apps/app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import mongomock 3 | 4 | from flask import Flask 5 | 6 | 7 | # from datetime import datetime 8 | # from datetime import timedelta 9 | # from datetime import timezone 10 | from apispec import APISpec 11 | from apispec.ext.marshmallow import MarshmallowPlugin 12 | from flask_apispec.extension import FlaskApiSpec 13 | 14 | from flask_jwt_extended import get_jwt 15 | from flask_jwt_extended import create_access_token, set_access_cookies 16 | # Realize a importação da função que configura a api 17 | from apps.extensions.api import configure_api 18 | from apps.extensions.config import config 19 | from apps.extensions.db import db 20 | from apps.extensions.jwt import configure_jwt 21 | from apps.users.commands import createsuperuser 22 | 23 | 24 | def create_app(testing=False): 25 | app = Flask('api-users') 26 | 27 | config_name = os.getenv("FLASK_CONFIG", "development") 28 | 29 | if testing: 30 | app.config.from_object(config['testing']) 31 | 32 | else: 33 | app.config.from_object(config[config_name]) 34 | 35 | app.config.update({ 36 | 'APISPEC_SPEC': APISpec( 37 | title='Flask Api Users', 38 | version='v1', 39 | plugins=[MarshmallowPlugin()], 40 | openapi_version='2.0.0' 41 | ), 42 | 'APISPEC_SWAGGER_URL': '/swagger/', # URI to access API Doc JSON 43 | 'APISPEC_SWAGGER_UI_URL': '/swagger-ui/' # URI to access UI of API Doc 44 | }) 45 | 46 | # Configure MongoEngine 47 | db.init_app(app) 48 | 49 | # Configure JWT 50 | configure_jwt(app) 51 | 52 | # executa a chamada da função de configuração 53 | configure_api(app) 54 | 55 | # add command function to cli commands 56 | app.cli.add_command(createsuperuser) 57 | 58 | # Implicit Refreshing With Cookies¶ 59 | # @app.after_request 60 | # def refresh_expiring_jwts(response): 61 | # try: 62 | # exp_timestamp = get_jwt()["exp"] 63 | # now = datetime.now(timezone.utc) 64 | # target_timestamp = datetime.timestamp(now + timedelta(minutes=30)) 65 | # if target_timestamp > exp_timestamp: 66 | # access_token = create_access_token(identity=get_jwt_identity()) 67 | # set_access_cookies(response, access_token) 68 | # return response 69 | # except (RuntimeError, KeyError): 70 | # # Case where there is not a valid JWT. Just return the original response 71 | # return response 72 | 73 | return app 74 | 75 | # app = create_app() 76 | -------------------------------------------------------------------------------- /apps/auth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucassimon/flask-api-users/88da2933b260d698c8513299ba64957e08230762/apps/auth/__init__.py -------------------------------------------------------------------------------- /apps/auth/commands.py: -------------------------------------------------------------------------------- 1 | 2 | from typing import Any, Mapping 3 | 4 | from flask_jwt_extended import create_access_token, set_access_cookies 5 | 6 | from apps.extensions.logging import make_logger 7 | from apps.extensions.jwt import create_tokens 8 | from apps.users.repositories import UserMongoRepository, AdminMongoRepository 9 | from apps.users.schemas import UserSchema 10 | from .schemas import LoginSchema 11 | from .use_case import AuthUserUseCase, AdminAuthUserUseCase 12 | 13 | logger = make_logger(debug=True) 14 | 15 | 16 | class AuthAdminUsersCommand: 17 | @staticmethod 18 | def get_user_and_check_password(payload: Mapping[str, Any], *_, **kwargs: dict[str, Any]): 19 | try: 20 | if logger: 21 | logger.info("auth.admin.user.command", message="Get the user and check the password") 22 | 23 | repo: AdminMongoRepository = AdminMongoRepository() 24 | schema = LoginSchema() 25 | use_case: AdminAuthUserUseCase = AdminAuthUserUseCase(repo=repo, logger=logger) 26 | if kwargs: 27 | payload.update({'roles': kwargs}) 28 | 29 | if logger: 30 | logger.info("auth.admin.user.command", message="Execute use case") 31 | 32 | output: UserSchema = use_case.execute(schema_input=schema, input_params=payload) 33 | return output 34 | 35 | except Exception as exc: 36 | raise exc 37 | 38 | @staticmethod 39 | def create_access_and_refresh_token(output): 40 | return create_tokens(output=output, additional_claims={'group': 'admin'}, logger=logger) 41 | 42 | @staticmethod 43 | def run(payload: Mapping[str, Any], *args, **kwargs: dict[str, Any]): 44 | output = AuthAdminUsersCommand.get_user_and_check_password(payload, *args, **kwargs) 45 | # gen tokens 46 | tokens = AuthAdminUsersCommand.create_access_and_refresh_token(output) 47 | output.update(tokens) 48 | 49 | return output 50 | 51 | 52 | 53 | class AuthUsersCommand: 54 | @staticmethod 55 | def get_user_and_check_password(payload: Mapping[str, Any], *_, **kwargs: dict[str, Any]): 56 | try: 57 | if logger: 58 | logger.info("auth.user.command", message="Get the user and check the password") 59 | 60 | repo: UserMongoRepository = UserMongoRepository() 61 | schema = LoginSchema() 62 | use_case: AuthUserUseCase = AuthUserUseCase(repo=repo, logger=logger) 63 | if kwargs: 64 | payload.update({'roles': kwargs}) 65 | 66 | if logger: 67 | logger.info("auth.user.command", message="Execute use case") 68 | 69 | output: AuthUserUseCase = use_case.execute(schema_input=schema, input_params=payload) 70 | return output 71 | 72 | except Exception as exc: 73 | raise exc 74 | 75 | @staticmethod 76 | def create_access_and_refresh_token(output): 77 | return create_tokens(output=output, additional_claims={ 78 | 'group': 'users', 79 | 'user_id': output['id'], 80 | }, logger=logger) 81 | 82 | 83 | @staticmethod 84 | def run(payload: Mapping[str, Any], *args, **kwargs: dict[str, Any]): 85 | output = AuthUsersCommand.get_user_and_check_password(payload, *args, **kwargs) 86 | # gen tokens 87 | tokens = AuthUsersCommand.create_access_and_refresh_token(output) 88 | output.update(tokens) 89 | 90 | return output 91 | -------------------------------------------------------------------------------- /apps/auth/exceptions.py: -------------------------------------------------------------------------------- 1 | class LoginSchemaValidationErrorException(Exception): 2 | 3 | def __init__(self, errors=None): 4 | self.msg = f"The input data is wrong" 5 | self.errors = errors 6 | 7 | def __str__(self): 8 | return self.msg 9 | -------------------------------------------------------------------------------- /apps/auth/resources.py: -------------------------------------------------------------------------------- 1 | # Python 2 | 3 | # Flask 4 | from flask import request 5 | 6 | # Third 7 | from flask_restful import Resource 8 | from flask_jwt_extended import create_access_token 9 | from flask_jwt_extended import jwt_required, get_jwt_identity, set_access_cookies 10 | from flask_apispec import marshal_with, doc, use_kwargs 11 | from flask_apispec.views import MethodResource 12 | # Apps 13 | from apps.extensions.messages import MSG_TOKEN_CREATED 14 | from apps.extensions.responses import resp_ok, resp_data_invalid, resp_does_not_exist, resp_exception 15 | from apps.users.exceptions import UserMongoDoesNotExistException 16 | from apps.users.schemas import UserSchema 17 | 18 | # Local 19 | from .commands import AuthUsersCommand, AuthAdminUsersCommand 20 | from .exceptions import LoginSchemaValidationErrorException 21 | from .schemas import LoginSchema 22 | 23 | 24 | class AuthAdminResource(MethodResource, Resource): 25 | 26 | @doc(description='Autenticar um usuário admin', tags=['Auth']) 27 | @use_kwargs(LoginSchema, location=('json'), apply=False) 28 | @marshal_with(UserSchema) 29 | def post(self, *args, **kwargs): 30 | ''' 31 | Route to do login in API 32 | ''' 33 | # Inicializo todas as variaveis utilizadas 34 | payload = request.get_json() or None 35 | 36 | try: 37 | output = AuthAdminUsersCommand.run(payload, *args, **kwargs) 38 | # Retorno 200 o meu endpoint 39 | response = resp_ok( 40 | 'Users', MSG_TOKEN_CREATED.format('usuário'), data=output, 41 | ) 42 | # set_access_cookies(response=response) 43 | return response 44 | 45 | except LoginSchemaValidationErrorException as exc: 46 | return resp_data_invalid( 47 | resource='users', 48 | errors=exc.errors, 49 | msg=exc.__str__() 50 | ) 51 | 52 | except UserMongoDoesNotExistException as exc: 53 | return resp_does_not_exist(resource='users', description=f"usuário") 54 | 55 | except Exception as exc: 56 | return resp_exception( 57 | resource='users', 58 | description='An error occurred', 59 | msg=exc.__str__() 60 | ) 61 | 62 | 63 | class AuthResource(MethodResource, Resource): 64 | @doc(description='Autenticar um usuário/customer', tags=['Auth']) 65 | @use_kwargs(LoginSchema, location=('json'), apply=False) 66 | @marshal_with(UserSchema) 67 | def post(self, *args, **kwargs): 68 | ''' 69 | Route to do login in API 70 | ''' 71 | # Inicializo todas as variaveis utilizadas 72 | payload = request.get_json() or None 73 | try: 74 | output = AuthUsersCommand.run(payload, *args, **kwargs) 75 | # Retorno 200 o meu endpoint 76 | return resp_ok( 77 | 'Users', MSG_TOKEN_CREATED, data=output, 78 | ) 79 | 80 | except LoginSchemaValidationErrorException as exc: 81 | return resp_data_invalid( 82 | resource='users', 83 | errors=exc.errors, 84 | msg=exc.__str__() 85 | ) 86 | 87 | except UserMongoDoesNotExistException as exc: 88 | return resp_does_not_exist(resource='users', description=f"usuário") 89 | 90 | except Exception as exc: 91 | return resp_exception( 92 | resource='users', 93 | description='An error occurred', 94 | msg=exc.__str__() 95 | ) 96 | 97 | 98 | class RefreshTokenResource(Resource): 99 | @jwt_required(refresh=True) 100 | def post(self, *args, **kwargs): 101 | ''' 102 | Refresh a token that expired. 103 | 104 | http://flask-jwt-extended.readthedocs.io/en/latest/refresh_tokens.html 105 | ''' 106 | extras = { 107 | 'token': create_access_token(identity=get_jwt_identity(), additional_claims={'group': 'users'}), 108 | } 109 | 110 | return resp_ok( 111 | 'Auth', MSG_TOKEN_CREATED, **extras 112 | ) 113 | -------------------------------------------------------------------------------- /apps/auth/schemas.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | from marshmallow import Schema 5 | from marshmallow.fields import Email, Str 6 | 7 | from apps.extensions.messages import MSG_FIELD_REQUIRED 8 | 9 | 10 | class LoginSchema(Schema): 11 | email = Email( 12 | required=True, error_messages={'required': MSG_FIELD_REQUIRED} 13 | ) 14 | password = Str( 15 | required=True, error_messages={'required': MSG_FIELD_REQUIRED} 16 | ) 17 | -------------------------------------------------------------------------------- /apps/auth/use_case.py: -------------------------------------------------------------------------------- 1 | from logging import Logger 2 | 3 | from marshmallow import ValidationError 4 | from bcrypt import checkpw 5 | 6 | 7 | from apps.users.repositories import UserMongoRepository, AdminMongoRepository 8 | from apps.users.schemas import UserSchema 9 | from .exceptions import LoginSchemaValidationErrorException 10 | 11 | 12 | class AuthUserUseCase: 13 | """ 14 | Classe para autenticar um usuario 15 | """ 16 | 17 | def __init__(self, repo: UserMongoRepository, logger: Logger | None = None) -> None: 18 | self.repo = repo 19 | self.logger = logger 20 | 21 | def __validate(self, schema_input, input_params): 22 | try: 23 | data = schema_input.load(input_params) 24 | return data 25 | 26 | except ValidationError as err: 27 | raise LoginSchemaValidationErrorException(err.messages) from err 28 | 29 | def __to_output(self, user): 30 | # Realizo um dump dos dados de acordo com o modelo salvo 31 | schema = UserSchema() 32 | result = schema.dump(user) 33 | if self.logger: 34 | self.logger.info("auth.user.usecase", message="Render user output") 35 | return result 36 | 37 | def validate(self, schema_input, input_params): 38 | # Desserialização os dados postados ou melhor meu payload 39 | try: 40 | data = self.__validate(schema_input=schema_input, input_params=input_params) 41 | 42 | # supress password and confirm password before loggin payload 43 | # By according LGPD the cpf is a sensitive data too. So is necessary to be supressed 44 | if self.logger: 45 | self.logger.info("auth.user.usecase", message="Validate initial Payload") 46 | 47 | return data 48 | 49 | except ValidationError as err: 50 | if self.logger: 51 | self.logger.info("auth.user.usecase", message="Schema validation failed", errors=err.messages) 52 | 53 | raise err 54 | 55 | def get_user(self, data): 56 | # Buscamos nosso usuário pelo email 57 | return self.repo.get_user_by_email(data.get('email')) 58 | 59 | def check_user_is_active(self, user): 60 | if not user.is_active(): 61 | raise Exception('Not allowed User') 62 | 63 | def check_password(self, user, data): 64 | if not checkpw(data.get('password').encode('utf-8'), user.password.encode('utf-8')): 65 | raise Exception('Password did not match') 66 | 67 | def execute(self, schema_input, input_params): 68 | data = self.validate(schema_input=schema_input, input_params=input_params) 69 | user = self.get_user(data=data) 70 | self.check_user_is_active(user=user) 71 | self.check_password(user=user, data=data) 72 | return self.__to_output(user=user) 73 | 74 | 75 | 76 | class AdminAuthUserUseCase: 77 | """ 78 | Classe para autenticar um admin 79 | """ 80 | 81 | def __init__(self, repo: AdminMongoRepository, logger: Logger | None = None) -> None: 82 | self.repo = repo 83 | self.logger = logger 84 | 85 | def __validate(self, schema_input, input_params): 86 | try: 87 | data = schema_input.load(input_params) 88 | return data 89 | 90 | except ValidationError as err: 91 | raise LoginSchemaValidationErrorException(err.messages) from err 92 | 93 | def __to_output(self, user): 94 | # Realizo um dump dos dados de acordo com o modelo salvo 95 | schema = UserSchema() 96 | result = schema.dump(user) 97 | if self.logger: 98 | self.logger.info("auth.admin.user.usecase", message="Render user output") 99 | return result 100 | 101 | def validate(self, schema_input, input_params): 102 | # Desserialização os dados postados ou melhor meu payload 103 | try: 104 | data = self.__validate(schema_input=schema_input, input_params=input_params) 105 | 106 | # supress password and confirm password before loggin payload 107 | # By according LGPD the cpf is a sensitive data too. So is necessary to be supressed 108 | if self.logger: 109 | self.logger.info("auth.admin.user.usecase", message="Validate initial Payload") 110 | 111 | return data 112 | 113 | except ValidationError as err: 114 | if self.logger: 115 | self.logger.info("auth.admin.user.usecase", message="Schema validation failed", errors=err.messages) 116 | 117 | raise err 118 | 119 | def get_admin(self, data): 120 | # Buscamos nosso usuário pelo email 121 | return self.repo.get_admin_by_email(data.get('email')) 122 | 123 | def check_admin_is_active(self, admin): 124 | if not admin.is_active(): 125 | raise Exception('Not allowed Admin') 126 | 127 | def check_password(self, admin, data): 128 | if not checkpw(data.get('password').encode('utf-8'), admin.password.encode('utf-8')): 129 | raise Exception('Password did not match') 130 | 131 | def execute(self, schema_input, input_params): 132 | data = self.validate(schema_input=schema_input, input_params=input_params) 133 | admin = self.get_admin(data=data) 134 | self.check_admin_is_active(admin=admin) 135 | self.check_password(admin=admin, data=data) 136 | return self.__to_output(user=admin) 137 | -------------------------------------------------------------------------------- /apps/events/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucassimon/flask-api-users/88da2933b260d698c8513299ba64957e08230762/apps/events/__init__.py -------------------------------------------------------------------------------- /apps/events/user_created.py: -------------------------------------------------------------------------------- 1 | from pyee.base import EventEmitter 2 | import time 3 | 4 | ee = EventEmitter() 5 | 6 | @ee.on('event') 7 | def event_handler(): 8 | print('BANG BANG') 9 | time.sleep(012.100) 10 | -------------------------------------------------------------------------------- /apps/extensions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucassimon/flask-api-users/88da2933b260d698c8513299ba64957e08230762/apps/extensions/__init__.py -------------------------------------------------------------------------------- /apps/extensions/api.py: -------------------------------------------------------------------------------- 1 | # Third 2 | from flask_apispec.extension import FlaskApiSpec 3 | 4 | # Importamos as classes API e Resource 5 | from flask_restful import Api, Resource 6 | 7 | from apps.users.resources import SignUp 8 | from apps.users.resources_admin import AdminUserPageList, AdminUserResource, AdminUserResourceByCpf 9 | from apps.auth.resources import AuthResource, AuthAdminResource, RefreshTokenResource 10 | 11 | 12 | # Criamos uma classe que extende de Resource 13 | class Index(Resource): 14 | 15 | # Definimos a operação get do protocolo http 16 | def get(self): 17 | 18 | # retornamos um simples dicionário que será automáticamente 19 | # retornado em json pelo flask 20 | return {'hello': 'world by apps'} 21 | 22 | 23 | # Instânciamos a API do FlaskRestful 24 | api = Api() 25 | 26 | 27 | def configure_api(app): 28 | 29 | api.add_resource(Index, '/') 30 | 31 | # rotas para o endpoint de usuarios 32 | api.add_resource(SignUp, '/users') 33 | 34 | # rotas para os admins 35 | api.add_resource(AdminUserPageList, '/admin/users/page/') 36 | api.add_resource(AdminUserResource, '/admin/users/') 37 | api.add_resource(AdminUserResourceByCpf, '/admin/users/cpf/') 38 | 39 | # rotas para autenticacao 40 | api.add_resource(AuthResource, '/auth') 41 | api.add_resource(AuthAdminResource, '/auth/admin') 42 | api.add_resource(RefreshTokenResource, '/auth/refresh') 43 | 44 | # inicializamos a api com as configurações do flask vinda por parâmetro 45 | api.init_app(app) 46 | 47 | docs = FlaskApiSpec(app) 48 | docs.register(SignUp) 49 | docs.register(AdminUserPageList) 50 | docs.register(AdminUserResourceByCpf) 51 | docs.register(AuthResource) 52 | docs.register(AuthAdminResource) 53 | -------------------------------------------------------------------------------- /apps/extensions/config.py: -------------------------------------------------------------------------------- 1 | # Python 2 | from os import getenv 3 | from datetime import timedelta 4 | 5 | import mongomock 6 | 7 | class Config: 8 | SECRET_KEY = getenv('SECRET_KEY') 9 | PORT = int(getenv('PORT', 8080)) 10 | DEBUG = getenv('DEBUG') or False 11 | MONGODB_SETTINGS = { 12 | 'host': getenv('MONGODB_URI', 'mongodb://0.0.0.0:27017/api-users'), 13 | } 14 | JWT_ACCESS_TOKEN_EXPIRES = timedelta( 15 | days=int(getenv('JWT_ACCESS_TOKEN_EXPIRES', 20)) 16 | ) 17 | JWT_REFRESH_TOKEN_EXPIRES = timedelta( 18 | days=int(getenv('JWT_REFRESH_TOKEN_EXPIRES', 30)) 19 | ) 20 | 21 | 22 | class DevelopmentConfig(Config): 23 | FLASK_ENV = 'development' 24 | DEBUG = True 25 | 26 | 27 | class TestingConfig(Config): 28 | FLASK_ENV = 'testing' 29 | TESTING = True 30 | MONGODB_SETTINGS = { 31 | 'db': 'api-users-test', 32 | 'host': 'mongodb://localhost', 33 | 'mongo_client_class': mongomock.MongoClient 34 | } 35 | 36 | 37 | class ProductionConfig(Config): 38 | FLASK_ENV = 'production' 39 | TESTING = False 40 | DEBUG = False 41 | 42 | 43 | config = { 44 | 'production': ProductionConfig, 45 | 'development': DevelopmentConfig, 46 | 'testing': TestingConfig, 47 | 'default': DevelopmentConfig 48 | } 49 | -------------------------------------------------------------------------------- /apps/extensions/db.py: -------------------------------------------------------------------------------- 1 | # Third 2 | from flask_mongoengine import MongoEngine 3 | 4 | 5 | db = MongoEngine() 6 | -------------------------------------------------------------------------------- /apps/extensions/jwt.py: -------------------------------------------------------------------------------- 1 | # Flask 2 | 3 | from flask import jsonify 4 | 5 | # Third 6 | from flask_jwt_extended import JWTManager 7 | from flask_jwt_extended import create_access_token, create_refresh_token 8 | # Apps 9 | from apps.users.models import User 10 | 11 | # Local 12 | from .messages import MSG_INVALID_CREDENTIALS, MSG_TOKEN_EXPIRED 13 | 14 | def create_tokens(output, additional_claims={'group': 'users'}, logger=None): 15 | token = create_access_token(identity=output["email"], additional_claims=additional_claims) 16 | refresh_token = create_refresh_token(identity=output["email"]) 17 | 18 | if logger: 19 | logger.info("auth.user.command", message="Creating jwt tokens") 20 | 21 | return { 22 | 'token': token, 23 | 'refresh': refresh_token 24 | } 25 | 26 | def configure_jwt(app): 27 | 28 | # Add jwt handler 29 | jwt = JWTManager(app) 30 | 31 | @jwt.additional_claims_loader 32 | def add_claims_to_access_token(identity): 33 | return { 34 | "aud": "some_audience", 35 | "foo": "bar", 36 | } 37 | 38 | @jwt.expired_token_loader 39 | def my_expired_token_callback(): 40 | resp = jsonify({ 41 | 'status': 401, 42 | 'sub_status': 42, 43 | 'message': MSG_TOKEN_EXPIRED 44 | }) 45 | 46 | resp.status_code = 401 47 | 48 | return resp 49 | 50 | @jwt.unauthorized_loader 51 | def my_unauthorized_callback(e): 52 | resp = jsonify({ 53 | 'status': 401, 54 | 'sub_status': 1, 55 | 'description': e, 56 | 'message': MSG_INVALID_CREDENTIALS 57 | }) 58 | 59 | resp.status_code = 401 60 | 61 | return resp 62 | 63 | # @jwt.claims_verification_loader 64 | # def my_claims_verification_callback(e): 65 | # resp = jsonify({ 66 | # 'status': 401, 67 | # 'sub_status': 2, 68 | # 'description': e, 69 | # 'message': MSG_INVALID_CREDENTIALS 70 | # }) 71 | 72 | # resp.status_code = 401 73 | 74 | # return resp 75 | 76 | @jwt.invalid_token_loader 77 | def my_invalid_token_loader_callback(e): 78 | resp = jsonify({ 79 | 'status': 401, 80 | 'sub_status': 3, 81 | 'description': e, 82 | 'message': MSG_INVALID_CREDENTIALS 83 | }) 84 | 85 | resp.status_code = 401 86 | 87 | return resp 88 | 89 | @jwt.needs_fresh_token_loader 90 | def my_needs_fresh_token_callback(e): 91 | resp = jsonify({ 92 | 'status': 401, 93 | 'sub_status': 4, 94 | 'description': e, 95 | 'message': MSG_INVALID_CREDENTIALS 96 | }) 97 | 98 | resp.status_code = 401 99 | 100 | return resp 101 | 102 | @jwt.revoked_token_loader 103 | def my_revoked_token_callback(e): 104 | resp = jsonify({ 105 | 'status': 401, 106 | 'sub_status': 5, 107 | 'description': e, 108 | 'message': MSG_INVALID_CREDENTIALS 109 | }) 110 | 111 | resp.status_code = 401 112 | 113 | return resp 114 | 115 | # @jwt.user_loader_callback_loader 116 | # def my_user_loader_callback(e): 117 | # resp = jsonify({ 118 | # 'status': 401, 119 | # 'sub_status': 6, 120 | # 'description': e, 121 | # 'message': MSG_INVALID_CREDENTIALS 122 | # }) 123 | 124 | # resp.status_code = 401 125 | 126 | # return resp 127 | 128 | # @jwt.user_loader_error_loader 129 | # def my_user_loader_error_callback(e): 130 | # resp = jsonify({ 131 | # 'status': 401, 132 | # 'sub_status': 7, 133 | # 'description': e, 134 | # 'message': MSG_INVALID_CREDENTIALS 135 | # }) 136 | 137 | # resp.status_code = 401 138 | 139 | # return resp 140 | 141 | # @jwt.token_in_blacklist_loader 142 | # def my_token_in_blacklist_callback(e): 143 | # resp = jsonify({ 144 | # 'status': 401, 145 | # 'sub_status': 8, 146 | # 'description': e, 147 | # 'message': MSG_INVALID_CREDENTIALS 148 | # }) 149 | 150 | # resp.status_code = 401 151 | 152 | # return resp 153 | 154 | # @jwt.claims_verification_failed_loader 155 | # def my_claims_verification_failed_callback(e): 156 | # resp = jsonify({ 157 | # 'status': 401, 158 | # 'sub_status': 9, 159 | # 'description': e, 160 | # 'message': MSG_INVALID_CREDENTIALS 161 | # }) 162 | 163 | # resp.status_code = 401 164 | 165 | # return resp 166 | -------------------------------------------------------------------------------- /apps/extensions/logging.py: -------------------------------------------------------------------------------- 1 | # Python 2 | import logging 3 | 4 | # Third 5 | import structlog 6 | 7 | 8 | def configure_logging(debug=False): 9 | 10 | default = [ 11 | structlog.contextvars.merge_contextvars, 12 | structlog.processors.add_log_level, 13 | structlog.processors.TimeStamper(), 14 | structlog.processors.StackInfoRenderer(), 15 | structlog.processors.CallsiteParameterAdder( 16 | { 17 | structlog.processors.CallsiteParameter.FILENAME, 18 | structlog.processors.CallsiteParameter.FUNC_NAME, 19 | structlog.processors.CallsiteParameter.LINENO, 20 | structlog.processors.CallsiteParameter.PATHNAME, 21 | } 22 | ), 23 | ] 24 | production = default + [structlog.processors.dict_tracebacks, structlog.processors.JSONRenderer()] 25 | 26 | development = default + [ 27 | structlog.dev.set_exc_info, 28 | structlog.dev.ConsoleRenderer(exception_formatter=structlog.dev.rich_traceback), 29 | ] 30 | 31 | processors = development if debug else production 32 | 33 | structlog.configure( 34 | processors=processors, 35 | wrapper_class=structlog.make_filtering_bound_logger(logging.NOTSET), 36 | context_class=dict, 37 | logger_factory=structlog.PrintLoggerFactory(), 38 | cache_logger_on_first_use=False, 39 | ) 40 | 41 | 42 | def make_logger(debug: bool): 43 | configure_logging(debug) 44 | log = structlog.get_logger() 45 | return log 46 | -------------------------------------------------------------------------------- /apps/extensions/messages.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | MSG_FIELD_REQUIRED = 'Campo obrigatório.' 4 | MSG_INVALID_DATA = 'Ocorreu um erro nos campos informados.' 5 | MSG_DOES_NOT_EXIST = 'Este(a) {} não existe.' 6 | MSG_EXCEPTION = 'Ocorreu um erro no servidor. Contate o administrador.' 7 | MSG_ALREADY_EXISTS = 'Já existe um(a) {} com estes dados.' 8 | MSG_NO_DATA = 'Nenhum dado foi postado.' 9 | MSG_PASSWORD_DIDNT_MATCH = 'Password did not matches' 10 | 11 | MSG_RESOURCE_CREATED = '{} criado(a).' 12 | MSG_RESOURCE_FETCHED_PAGINATED = 'Lista os/as {} paginados(as).' 13 | MSG_RESOURCE_FETCHED = '{} retornado(a).' 14 | MSG_RESOURCE_UPDATED = '{} atualizado(a).' 15 | MSG_RESOURCE_DELETED = '{} deletado(a).' 16 | 17 | MSG_TOKEN_CREATED = 'Token criado.' 18 | MSG_INVALID_CREDENTIALS = 'As credenciais estão inválidas para log in.' 19 | MSG_TOKEN_EXPIRED = 'Token expirou.' 20 | MSG_PERMISSION_DENIED = 'Permissão negada.' 21 | -------------------------------------------------------------------------------- /apps/extensions/responses.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from flask import jsonify 4 | 5 | from .messages import MSG_INVALID_DATA, MSG_DOES_NOT_EXIST, MSG_EXCEPTION 6 | from .messages import MSG_ALREADY_EXISTS, MSG_PERMISSION_DENIED 7 | 8 | 9 | def resp_data_invalid( 10 | resource: str, errors: dict, msg: str = MSG_INVALID_DATA 11 | ): 12 | ''' 13 | Responses 422 Unprocessable Entity 14 | ''' 15 | 16 | if not isinstance(resource, str): 17 | raise ValueError('O recurso precisa ser uma string.') 18 | 19 | resp = jsonify({ 20 | 'resource': resource, 21 | 'message': msg, 22 | 'errors': errors, 23 | }) 24 | 25 | resp.status_code = 422 26 | 27 | return resp 28 | 29 | 30 | def resp_exception( 31 | resource: str, description: str = '', msg: str = MSG_EXCEPTION 32 | ): 33 | ''' 34 | Responses 500 35 | ''' 36 | 37 | if not isinstance(resource, str): 38 | raise ValueError('O recurso precisa ser uma string.') 39 | 40 | resp = jsonify({ 41 | 'resource': resource, 42 | 'message': msg, 43 | 'description': description 44 | }) 45 | 46 | resp.status_code = 500 47 | 48 | return resp 49 | 50 | 51 | def resp_does_not_exist(resource: str, description: str): 52 | ''' 53 | Responses 404 Not Found 54 | ''' 55 | 56 | if not isinstance(resource, str): 57 | raise ValueError('O recurso precisa ser uma string.') 58 | 59 | resp = jsonify({ 60 | 'resource': resource, 61 | 'message': MSG_DOES_NOT_EXIST.format(description), 62 | }) 63 | 64 | resp.status_code = 404 65 | 66 | return resp 67 | 68 | 69 | def resp_already_exists(resource: str, description: str): 70 | ''' 71 | Responses 409 72 | ''' 73 | 74 | if not isinstance(resource, str): 75 | raise ValueError('O recurso precisa ser uma string.') 76 | 77 | resp = jsonify({ 78 | 'resource': resource, 79 | 'message': MSG_ALREADY_EXISTS.format(description), 80 | }) 81 | 82 | resp.status_code = 409 83 | 84 | return resp 85 | 86 | 87 | def resp_ok(resource: str, message: str, data=None, **extras): 88 | ''' 89 | Responses 200 90 | ''' 91 | response = {'status': 200, 'message': message, 'resource': resource} 92 | 93 | if data: 94 | response['data'] = data 95 | 96 | response.update(extras) 97 | 98 | resp = jsonify(response) 99 | 100 | resp.status_code = 200 101 | 102 | return resp 103 | 104 | 105 | def resp_notallowed_user(resource: str, msg: str = MSG_PERMISSION_DENIED): 106 | if not isinstance(resource, str): 107 | raise ValueError('Recurso precisa ser uma string') 108 | 109 | resp = jsonify({ 110 | 'status': 401, 111 | 'resource': resource, 112 | 'message': msg 113 | }) 114 | 115 | resp.status_code = 401 116 | 117 | return resp 118 | -------------------------------------------------------------------------------- /apps/users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucassimon/flask-api-users/88da2933b260d698c8513299ba64957e08230762/apps/users/__init__.py -------------------------------------------------------------------------------- /apps/users/commands.py: -------------------------------------------------------------------------------- 1 | 2 | from typing import Any, Mapping 3 | import asyncio 4 | import click 5 | from flask.cli import with_appcontext 6 | 7 | from apps.extensions.logging import make_logger 8 | from apps.extensions.jwt import create_tokens 9 | 10 | from .schemas import CreateAdminInput, CreateUserInput, CreateUserOutput, UserSchema 11 | from .use_case import CreateUserUseCase, GetUserByCpfCnpjUseCase 12 | from .repositories import UserMongoRepository, AdminMongoRepository 13 | from .exceptions import UserSchemaValidationErrorException, UserMongoNotUniqueException, UserMongoValidationErrorException 14 | 15 | logger = make_logger(debug=True) 16 | 17 | 18 | class CreateUserCommand: 19 | @staticmethod 20 | def save_in_database(payload: Mapping[str, Any], *_, **kwargs: dict[str, Any]): 21 | try: 22 | if logger: 23 | logger.info("create.user.command", message="Save in database operation") 24 | 25 | repo: UserMongoRepository = UserMongoRepository() 26 | schema = CreateUserInput() 27 | use_case: CreateUserUseCase = CreateUserUseCase(repo=repo, logger=logger) 28 | 29 | if logger: 30 | logger.info("create.user.command", message="Execute use case") 31 | 32 | output: CreateUserOutput = use_case.execute(schema_input=schema, input_params=payload) 33 | return output 34 | 35 | except Exception as exc: 36 | raise exc 37 | 38 | @staticmethod 39 | def send_user_to_queue(output): 40 | try: 41 | # your code is here 42 | print(output) 43 | except Exception as exc: 44 | raise exc 45 | 46 | if logger: 47 | logger.info("create.user.command", message="User was sent to queue") 48 | 49 | @staticmethod 50 | def another_operation(output): 51 | try: 52 | # your code is here 53 | print(output) 54 | if logger: 55 | logger.info("create.user.command", message="Another operation") 56 | except Exception as exc: 57 | raise exc 58 | 59 | @staticmethod 60 | def create_access_and_refresh_token(output): 61 | return create_tokens(output=output, additional_claims={'group': 'users'}) 62 | 63 | @staticmethod 64 | def run(payload: Mapping[str, Any], *args, **kwargs: dict[str, Any]): 65 | output = CreateUserCommand.save_in_database(payload, *args, **kwargs) 66 | 67 | # use it with async function 68 | CreateUserCommand.send_user_to_queue(output) 69 | CreateUserCommand.another_operation(output) 70 | 71 | # gen tokens 72 | tokens = CreateUserCommand.create_access_and_refresh_token(output) 73 | output.update(tokens) 74 | 75 | return output 76 | 77 | 78 | class CreateSuperUserCommand: 79 | @staticmethod 80 | def save_in_database(payload: Mapping[str, Any], *_, **kwargs: dict[str, Any]): 81 | try: 82 | if logger: 83 | logger.info("create.admin.user.command", message="Save in database operation") 84 | 85 | repo: AdminMongoRepository = AdminMongoRepository() 86 | schema = CreateAdminInput() 87 | use_case: CreateUserUseCase = CreateUserUseCase(repo=repo, logger=logger) 88 | if kwargs: 89 | payload.update({'roles': kwargs}) 90 | 91 | if logger: 92 | logger.info("create.admin.user.command", message="Execute use case") 93 | 94 | output: CreateUserOutput = use_case.execute(schema_input=schema, input_params=payload) 95 | return output 96 | 97 | except Exception as exc: 98 | raise exc 99 | 100 | @staticmethod 101 | def run(payload: Mapping[str, Any], *args, **kwargs: dict[str, Any]): 102 | output = CreateSuperUserCommand.save_in_database(payload, *args, **kwargs) 103 | 104 | return output 105 | 106 | 107 | # create command function 108 | @click.command(name='createsuperuser') 109 | @click.argument("name") 110 | @click.argument("email") 111 | @click.argument("password") 112 | @with_appcontext 113 | def createsuperuser(name: str, email: str, password: str): 114 | """ 115 | Create a super user. 116 | Use: flask create [some-name] [some-email] [some-password] 117 | """ 118 | 119 | payload = {'email': email, 'password': password, 'full_name': name} 120 | roles = {'superuser': True} 121 | 122 | try: 123 | output = CreateSuperUserCommand().run(payload=payload, **roles) 124 | click.echo(f"Superuser {output['email']} created") 125 | 126 | except UserSchemaValidationErrorException: 127 | click.echo(f"Superuser payload invalid") 128 | 129 | except UserMongoNotUniqueException: 130 | click.echo(f"Superuser {payload['email']} already exists") 131 | 132 | except UserMongoValidationErrorException: 133 | click.echo(f"Superuser {payload['email']} some fields are wrong") 134 | 135 | except Exception: 136 | click.echo(f"An error occurred. Superuser not created") 137 | 138 | 139 | class GetUserByCpfCnpjCommand: 140 | @staticmethod 141 | def get_user_by_cpf_cnpj(current_user, cpf_cnpj, *_, **kwargs: dict[str, Any]): 142 | try: 143 | if logger: 144 | logger.info("users.GetUserByCpfCnpj.command", message="Get the user and check the password") 145 | 146 | repo: AdminMongoRepository = AdminMongoRepository() 147 | use_case: GetUserByCpfCnpjUseCase = GetUserByCpfCnpjUseCase(repo=repo, logger=logger) 148 | 149 | if logger: 150 | logger.info("users.GetUserByCpfCnpj.command", message="Execute use case") 151 | 152 | output: UserSchema = use_case.execute(current_user=current_user, cpf_cnpj=cpf_cnpj) 153 | return output 154 | 155 | except Exception as exc: 156 | raise exc 157 | 158 | @staticmethod 159 | def run(current_user, cpf_cnpj, *args, **kwargs: dict[str, Any]): 160 | output = GetUserByCpfCnpjCommand.get_user_by_cpf_cnpj(current_user=current_user, cpf_cnpj=cpf_cnpj, *args, **kwargs) 161 | 162 | return output 163 | -------------------------------------------------------------------------------- /apps/users/exceptions.py: -------------------------------------------------------------------------------- 1 | class UserMongoNotUniqueException(Exception): 2 | pass 3 | 4 | 5 | class UserMongoValidationErrorException(Exception): 6 | pass 7 | 8 | 9 | class UserMongoFieldsDoesNotExistException(Exception): 10 | pass 11 | 12 | 13 | class UserMongoDoesNotExistException(Exception): 14 | pass 15 | 16 | 17 | class UserMongoMultipleObjectsReturnedException(Exception): 18 | pass 19 | 20 | 21 | class UserSchemaValidationErrorException(Exception): 22 | 23 | def __init__(self, errors=None): 24 | self.msg = f"The input data is wrong" 25 | self.errors = errors 26 | 27 | def __str__(self): 28 | return self.msg 29 | -------------------------------------------------------------------------------- /apps/users/models.py: -------------------------------------------------------------------------------- 1 | # Python 2 | from datetime import datetime 3 | 4 | # Third 5 | from mongoengine import ( 6 | BooleanField, 7 | DateTimeField, 8 | EmailField, 9 | EmbeddedDocument, 10 | EmbeddedDocumentField, 11 | StringField, 12 | ) 13 | import mongoengine as me 14 | # Apps 15 | from apps.extensions.db import db 16 | 17 | 18 | class Address(EmbeddedDocument): 19 | """ 20 | Default implementation for address fields 21 | """ 22 | meta = { 23 | 'ordering': ['zip_code'] 24 | } 25 | zip_code = StringField(default='') 26 | address = StringField(default='') 27 | number = StringField(default='') 28 | complement = StringField(default='') 29 | neighborhood = StringField(default='') 30 | city = StringField(default='') 31 | city_id = StringField(default='') 32 | state = StringField(default='') 33 | country = StringField(default='BRA') 34 | 35 | 36 | class Roles(EmbeddedDocument): 37 | """ 38 | Roles permissions 39 | """ 40 | superuser = BooleanField(default=True) 41 | manager = BooleanField(default=False) 42 | coordinators = BooleanField(default=False) 43 | vendors = BooleanField(default=False) 44 | 45 | 46 | class UserMixin(me.Document): 47 | """ 48 | Default implementation for User fields 49 | """ 50 | meta = { 51 | 'abstract': True, 52 | 'ordering': ['email'] 53 | } 54 | 55 | email = EmailField(required=True, unique=True) 56 | password = StringField(required=True) 57 | created = DateTimeField(default=datetime.now) 58 | active = BooleanField(default=True) 59 | 60 | def is_active(self): 61 | return self.active 62 | 63 | 64 | class User(UserMixin): 65 | ''' 66 | Users 67 | ''' 68 | meta = {'collection': 'users'} 69 | 70 | full_name = StringField(required=True) 71 | cpf_cnpj = StringField(required=True, unique=True) 72 | date_of_birth = DateTimeField(required=True) 73 | address = EmbeddedDocumentField(Address, default=Address) 74 | 75 | 76 | class Admin(UserMixin): 77 | ''' 78 | Admin users 79 | ''' 80 | meta = {'collection': 'admins'} 81 | 82 | full_name = StringField(required=True) 83 | roles = EmbeddedDocumentField(Roles, default=Roles) 84 | -------------------------------------------------------------------------------- /apps/users/repositories.py: -------------------------------------------------------------------------------- 1 | from mongoengine.errors import NotUniqueError, ValidationError, FieldDoesNotExist, DoesNotExist, MultipleObjectsReturned 2 | from flask_mongoengine import Pagination 3 | from .models import User, Admin 4 | from .exceptions import ( 5 | UserMongoNotUniqueException, UserMongoValidationErrorException, UserMongoDoesNotExistException, 6 | UserMongoFieldsDoesNotExistException, UserMongoMultipleObjectsReturnedException 7 | ) 8 | 9 | class UserMongoMixin: 10 | def get_user_by_email(self, email: str): 11 | try: 12 | # buscamos todos os usuários da base utilizando o paginate 13 | return User.objects.get(email=email) 14 | 15 | except DoesNotExist as err: 16 | raise UserMongoDoesNotExistException from err 17 | 18 | except FieldDoesNotExist as err: 19 | raise UserMongoFieldsDoesNotExistException from err 20 | 21 | except Exception as err: 22 | raise err 23 | 24 | 25 | class UserMongoRepository(UserMongoMixin): 26 | def insert(self, data) -> None: 27 | try: 28 | model = User(**data) 29 | model.save() 30 | return model 31 | except NotUniqueError as err: 32 | raise UserMongoNotUniqueException from err 33 | 34 | except ValidationError as err: 35 | raise UserMongoValidationErrorException from err 36 | 37 | except Exception as err: 38 | raise err 39 | 40 | 41 | class AdminMongoRepository(UserMongoMixin): 42 | def insert(self, data) -> None: 43 | try: 44 | model = Admin(**data) 45 | model.save() 46 | return model 47 | except NotUniqueError as err: 48 | raise UserMongoNotUniqueException from err 49 | 50 | except ValidationError as err: 51 | raise UserMongoValidationErrorException from err 52 | 53 | except Exception as err: 54 | raise err 55 | 56 | def get_admin_by_email(self, email: str): 57 | try: 58 | # buscamos todos os usuários da base utilizando o paginate 59 | return Admin.objects.get(email=email) 60 | 61 | except DoesNotExist as err: 62 | raise UserMongoDoesNotExistException from err 63 | 64 | except FieldDoesNotExist as err: 65 | raise UserMongoFieldsDoesNotExistException from err 66 | 67 | except Exception as err: 68 | raise err 69 | 70 | def get_users_with_pagination(self, page_id=1, page_size=10): 71 | try: 72 | # buscamos todos os usuarios da base utilizando o paginate 73 | users = User.objects() 74 | return Pagination(iterable=users, page=page_id, per_page=page_size) 75 | 76 | except FieldDoesNotExist as err: 77 | raise UserMongoFieldsDoesNotExistException from err 78 | 79 | except Exception as err: 80 | raise err 81 | 82 | def get_user_by_id(self, user_id: str): 83 | try: 84 | # buscamos todos os usuários da base utilizando o paginate 85 | return User.objects.get(id=user_id) 86 | 87 | except DoesNotExist as err: 88 | raise err 89 | 90 | except FieldDoesNotExist as err: 91 | raise UserMongoFieldsDoesNotExistException from err 92 | 93 | except Exception as err: 94 | raise err 95 | 96 | def get_user_by_cpf_cnpj(self, cpf_cnpj: str): 97 | try: 98 | # buscamos todos os usuários da base utilizando o paginate 99 | return User.objects.get(cpf_cnpj=cpf_cnpj) 100 | 101 | except DoesNotExist as err: 102 | raise UserMongoDoesNotExistException from err 103 | 104 | except FieldDoesNotExist as err: 105 | raise UserMongoFieldsDoesNotExistException from err 106 | 107 | except Exception as err: 108 | raise err 109 | 110 | def exists_email_in_users(self, email: str, instance=None): 111 | """ 112 | Verifico se existe um usuário com aquele email 113 | """ 114 | user = None 115 | 116 | try: 117 | user = User.objects.get(email=email) 118 | 119 | except DoesNotExist as err: 120 | raise UserMongoDoesNotExistException from err 121 | 122 | except MultipleObjectsReturned as err: 123 | raise UserMongoMultipleObjectsReturnedException from err 124 | 125 | # verifico se o id retornado na pesquisa é mesmo da minha instancia 126 | # informado no parâmetro 127 | if instance and instance.id == user.id: 128 | return False 129 | 130 | return True 131 | 132 | def update_user(self, user, data): 133 | try: 134 | # para cada chave dentro do dados do update schema 135 | # atribuimos seu valor 136 | for i in data.keys(): 137 | user[i] = data[i] 138 | 139 | user.save() 140 | 141 | except NotUniqueError as err: 142 | raise UserMongoNotUniqueException from err 143 | 144 | except ValidationError as err: 145 | raise UserMongoValidationErrorException from err 146 | 147 | except Exception as err: 148 | raise err 149 | 150 | def delete_user(self, user): 151 | try: 152 | user.active = False 153 | user.save() 154 | 155 | except NotUniqueError as err: 156 | raise UserMongoNotUniqueException from err 157 | 158 | except ValidationError as err: 159 | raise UserMongoValidationErrorException from err 160 | 161 | except Exception as err: 162 | raise err 163 | -------------------------------------------------------------------------------- /apps/users/resources.py: -------------------------------------------------------------------------------- 1 | # Python 2 | from dataclasses import asdict 3 | # Flask 4 | from flask import request 5 | 6 | # Third 7 | from flask_restful import Resource 8 | from flask_apispec import marshal_with 9 | from flask_apispec.views import MethodResource 10 | from flask_apispec import marshal_with, doc, use_kwargs 11 | 12 | # Apps 13 | from .commands import CreateUserCommand 14 | from apps.extensions.responses import ( 15 | resp_already_exists, 16 | resp_exception, 17 | resp_data_invalid, 18 | resp_ok 19 | ) 20 | from apps.extensions.messages import MSG_NO_DATA, MSG_PASSWORD_DIDNT_MATCH, MSG_INVALID_DATA 21 | from apps.extensions.messages import MSG_RESOURCE_CREATED 22 | from .schemas import CreateUserInput, CreateUserOutput 23 | from .exceptions import UserSchemaValidationErrorException, UserMongoNotUniqueException, UserMongoValidationErrorException 24 | 25 | class SignUp(MethodResource, Resource): 26 | 27 | @doc(description='Registrar um usuário/customers', tags=['Customer']) 28 | @use_kwargs(CreateUserInput, location=('json'), apply=False) 29 | @marshal_with(CreateUserOutput) 30 | def post(self, *args, **kwargs): 31 | # Inicializo todas as variaveis utilizadas 32 | payload = request.get_json() or None 33 | try: 34 | output = CreateUserCommand.run(payload, *args, **kwargs) 35 | # Retorno 200 o meu endpoint 36 | return resp_ok( 37 | 'Users', MSG_RESOURCE_CREATED.format('Usuário'), data=output, 38 | ) 39 | except UserSchemaValidationErrorException as exc: 40 | return resp_data_invalid( 41 | resource='users', 42 | errors=exc.errors, 43 | msg=exc.__str__() 44 | ) 45 | 46 | except UserMongoNotUniqueException as exc: 47 | return resp_already_exists(resource='users', description=f"usuário") 48 | 49 | except UserMongoValidationErrorException as exc: 50 | return resp_data_invalid( 51 | resource='users', 52 | errors=exc.errors, 53 | msg=exc.__str__() 54 | ) 55 | 56 | except Exception as exc: 57 | return resp_exception( 58 | resource='users', 59 | description='An error occurred', 60 | msg=exc.__str__() 61 | ) 62 | -------------------------------------------------------------------------------- /apps/users/resources_admin.py: -------------------------------------------------------------------------------- 1 | # Flask 2 | from flask import request 3 | 4 | # Third 5 | from mongoengine.errors import FieldDoesNotExist 6 | from mongoengine.errors import NotUniqueError, ValidationError 7 | from flask_restful import Resource 8 | from flask_jwt_extended import get_jwt_identity, jwt_required 9 | from flask_apispec import marshal_with, doc, use_kwargs 10 | from flask_apispec.views import MethodResource 11 | 12 | # Apps 13 | from apps.extensions.responses import resp_ok, resp_exception, resp_data_invalid, resp_already_exists 14 | from apps.extensions.responses import resp_notallowed_user, resp_does_not_exist 15 | 16 | from apps.extensions.messages import MSG_RESOURCE_FETCHED_PAGINATED, MSG_RESOURCE_FETCHED 17 | from apps.extensions.messages import MSG_NO_DATA, MSG_RESOURCE_UPDATED, MSG_INVALID_DATA 18 | from apps.extensions.messages import MSG_ALREADY_EXISTS, MSG_RESOURCE_DELETED 19 | 20 | # Local 21 | from .models import User, Admin 22 | from .schemas import UserSchema, UserUpdateSchema 23 | from .repositories import AdminMongoRepository 24 | from .exceptions import ( 25 | UserMongoNotUniqueException, UserMongoValidationErrorException, 26 | UserMongoDoesNotExistException 27 | ) 28 | from .commands import GetUserByCpfCnpjCommand 29 | 30 | 31 | class AdminUserPageList(MethodResource, Resource): 32 | 33 | @doc(description='Listar usuários/customers paginado', tags=['Users']) 34 | @marshal_with(UserSchema) 35 | @jwt_required() 36 | def get(self, page_id=1): 37 | # inicializa o schema podendo conter varios objetos 38 | schema = UserSchema(many=True) 39 | # incializa o page_size sempre com 10 40 | page_size = 10 41 | 42 | current_user = AdminMongoRepository().get_admin_by_email(get_jwt_identity()) 43 | 44 | if not isinstance(current_user, Admin): 45 | return current_user 46 | 47 | if not (current_user.is_active()): 48 | return resp_notallowed_user('Users') 49 | 50 | # se enviarmos o page_size como parametro 51 | if 'page_size' in request.args: 52 | # verificamos se ele é menor que 1 53 | if int(request.args.get('page_size')) < 1: 54 | page_size = 10 55 | else: 56 | # fazemos um type cast convertendo para inteiro 57 | page_size = int(request.args.get('page_size')) 58 | 59 | try: 60 | pagination = AdminMongoRepository().get_users_with_pagination(page_id=page_id, page_size=page_size) 61 | 62 | # criamos dados extras a serem respondidos 63 | extra = { 64 | 'page': pagination.page, 'pages': pagination.pages, 'total': pagination.total, 65 | 'params': {'page_size': page_size} 66 | } 67 | 68 | # fazemos um dump dos objetos pesquisados 69 | result = schema.dump(pagination.items) 70 | 71 | return resp_ok( 72 | 'Users', MSG_RESOURCE_FETCHED_PAGINATED.format('usuários'), data=result, 73 | **extra 74 | ) 75 | except UserMongoNotUniqueException as exc: 76 | return resp_already_exists(resource='users', description=f"usuário") 77 | 78 | except UserMongoValidationErrorException as exc: 79 | return resp_data_invalid( 80 | resource='users', 81 | errors=exc.errors, 82 | msg=exc.__str__() 83 | ) 84 | 85 | except Exception as exc: 86 | return resp_exception( 87 | resource='users', 88 | description='An error occurred', 89 | msg=exc.__str__() 90 | ) 91 | 92 | 93 | class AdminUserResourceByCpf(MethodResource, Resource): 94 | 95 | @doc(description='Buscar usuário por cpf ou cnpj', tags=['Users']) 96 | @marshal_with(UserSchema) 97 | @jwt_required() 98 | def get(self, cpf_cnpj): 99 | try: 100 | current_user = get_jwt_identity() 101 | output = GetUserByCpfCnpjCommand.run(current_user=current_user, cpf_cnpj=cpf_cnpj) 102 | # Retorno 200 o meu endpoint 103 | response = resp_ok( 104 | 'Users', MSG_RESOURCE_FETCHED.format("Usuário"), data=output, 105 | ) 106 | return response 107 | 108 | except UserMongoDoesNotExistException as exc: 109 | return resp_does_not_exist(resource='users', description=f"usuário") 110 | 111 | except Exception as exc: 112 | return resp_exception( 113 | resource='users', 114 | description='An error occurred', 115 | msg=exc.__str__() 116 | ) 117 | 118 | 119 | class AdminUserResource(Resource): 120 | @jwt_required() 121 | def get(self, user_id): 122 | result = None 123 | schema = UserSchema() 124 | current_user = AdminMongoRepository().get_user_by_email(get_jwt_identity()) 125 | 126 | if not isinstance(current_user, Admin): 127 | return current_user 128 | 129 | if not (current_user.is_active()) and current_user.is_admin(): 130 | return resp_notallowed_user('Users') 131 | 132 | user = AdminMongoRepository().get_user_by_id(user_id) 133 | 134 | if not isinstance(user, User): 135 | return user 136 | 137 | result = schema.dump(user) 138 | 139 | return resp_ok( 140 | 'Users', MSG_RESOURCE_FETCHED.format('Usuários'), data=result 141 | ) 142 | 143 | @jwt_required() 144 | def put(self, user_id): 145 | result = None 146 | schema = UserSchema() 147 | update_schema = UserUpdateSchema() 148 | req_data = request.get_json() or None 149 | email = None 150 | current_user = AdminMongoRepository().get_user_by_email(get_jwt_identity()) 151 | 152 | if not isinstance(current_user, Admin): 153 | return current_user 154 | 155 | if not (current_user.is_active()) and current_user.is_admin(): 156 | return resp_notallowed_user('Users') 157 | 158 | # Valido se o payload está vazio 159 | if req_data is None: 160 | return resp_data_invalid('Users', [], msg=MSG_NO_DATA) 161 | 162 | # Busco o usuário na coleção users pelo seu id 163 | user = AdminMongoRepository().get_user_by_id(user_id) 164 | 165 | # se não for uma instancia do modelo User retorno uma resposta 166 | # da requisição http do flask 167 | if not isinstance(user, User): 168 | return user 169 | 170 | # carrego meus dados de acordo com o schema de atualização 171 | try: 172 | data = update_schema.load(req_data) 173 | 174 | except ValidationError as err: 175 | return resp_data_invalid('Users', err.messages) 176 | 177 | email = data.get('email', None) 178 | 179 | # Valido se existe um email na coleção de usuários 180 | if email and AdminMongoRepository().exists_email_in_users(email, user): 181 | return resp_data_invalid( 182 | 'Users', [{'email': [MSG_ALREADY_EXISTS.format('usuário')]}] 183 | ) 184 | 185 | try: 186 | user = AdminMongoRepository().update_user(user, data) 187 | result = schema.dump(user) 188 | 189 | return resp_ok( 190 | 'Users', MSG_RESOURCE_UPDATED.format('Usuário'), data=result 191 | ) 192 | 193 | except UserMongoNotUniqueException as exc: 194 | return resp_already_exists(resource='users', description=f"usuário") 195 | 196 | except UserMongoValidationErrorException as exc: 197 | return resp_data_invalid( 198 | resource='users', 199 | errors=exc.errors, 200 | msg=exc.__str__() 201 | ) 202 | 203 | except Exception as exc: 204 | return resp_exception( 205 | resource='users', 206 | description='An error occurred', 207 | msg=exc.__str__() 208 | ) 209 | 210 | @jwt_required() 211 | def delete(self, user_id): 212 | current_user = AdminMongoRepository().get_user_by_email(get_jwt_identity()) 213 | 214 | if not isinstance(current_user, Admin): 215 | return current_user 216 | 217 | if not (current_user.is_active()) and current_user.is_admin(): 218 | return resp_notallowed_user('Users') 219 | 220 | # Busco o usuário na coleção users pelo seu id 221 | user = AdminMongoRepository().get_user_by_id(user_id) 222 | 223 | # se não for uma instancia do modelo User retorno uma resposta 224 | # da requisição http do flask 225 | if not isinstance(user, User): 226 | return user 227 | 228 | try: 229 | AdminMongoRepository().delete_user(user) 230 | return resp_ok('Users', MSG_RESOURCE_DELETED.format('Usuário')) 231 | 232 | except UserMongoNotUniqueException as exc: 233 | return resp_already_exists(resource='users', description=f"usuário") 234 | 235 | except UserMongoValidationErrorException as exc: 236 | return resp_data_invalid( 237 | resource='users', 238 | errors=exc.errors, 239 | msg=exc.__str__() 240 | ) 241 | 242 | except Exception as exc: 243 | return resp_exception( 244 | resource='users', 245 | description='An error occurred', 246 | msg=exc.__str__() 247 | ) 248 | 249 | -------------------------------------------------------------------------------- /apps/users/schemas.py: -------------------------------------------------------------------------------- 1 | from marshmallow import Schema, pre_load, post_load, validates, validates_schema, ValidationError 2 | from marshmallow.fields import Email, Str, Boolean, Nested, Date 3 | 4 | from apps.extensions.messages import MSG_FIELD_REQUIRED 5 | 6 | from .utils import Cpf, check_password_in_signup 7 | 8 | 9 | 10 | def check_cpf(data): 11 | return Cpf(data).validate() 12 | 13 | 14 | class RolesSchema(Schema): 15 | superuser = Boolean() 16 | manager = Boolean() 17 | coordinators = Boolean() 18 | vendors = Boolean() 19 | 20 | 21 | class CreateAdminInput(Schema): 22 | full_name = Str( 23 | required=True, error_messages={'required': MSG_FIELD_REQUIRED} 24 | ) 25 | email = Email( 26 | required=True, error_messages={'required': MSG_FIELD_REQUIRED} 27 | ) 28 | password = Str( 29 | required=True, load_only=True,error_messages={'required': MSG_FIELD_REQUIRED} 30 | ) 31 | 32 | roles = Nested(RolesSchema) 33 | 34 | @post_load 35 | def lower_email(self, payload, **kwargs): 36 | # transformo o email para lowercase e retiro espaços 37 | payload["email"] = payload["email"].lower().strip() 38 | return payload 39 | 40 | 41 | class CreateUserInput(Schema): 42 | full_name = Str( 43 | required=True, error_messages={'required': MSG_FIELD_REQUIRED} 44 | ) 45 | email = Email( 46 | required=True, error_messages={'required': MSG_FIELD_REQUIRED} 47 | ) 48 | cpf_cnpj = Str(required=True, error_messages={'required': MSG_FIELD_REQUIRED}) 49 | date_of_birth = Date(required=True, error_messages={'required': MSG_FIELD_REQUIRED}) 50 | password = Str( 51 | required=True, load_only=True,error_messages={'required': MSG_FIELD_REQUIRED} 52 | ) 53 | confirm_password = Str( 54 | required=True, load_only=True, error_messages={'required': MSG_FIELD_REQUIRED} 55 | ) 56 | 57 | def normalize_cpf_cnpj(self, value, **kwargs): 58 | # normalizo a string retirando caracteres especiais 59 | cpf_instance = Cpf(value) 60 | return cpf_instance.cpf 61 | 62 | @post_load 63 | def lower_email(self, payload, **kwargs): 64 | # transformo o email para lowercase e retiro espaços 65 | payload["email"] = payload["email"].lower().strip() 66 | return payload 67 | 68 | @post_load 69 | def render_cpf_cnpj(self, payload, **kwargs): 70 | payload["cpf_cnpj"] = self.normalize_cpf_cnpj(payload["cpf_cnpj"]) 71 | return payload 72 | 73 | @post_load 74 | def remove_confirm_password(self, payload, **kwargs): 75 | del payload["confirm_password"] 76 | return payload 77 | 78 | @validates_schema 79 | def validate_password(self, data, **kwargs): 80 | # verifico através de uma função a senha e a confirmação da senha 81 | # Se as senhas não são iguais retorno uma respota inválida 82 | if not check_password_in_signup(data['password'], data['confirm_password']): 83 | raise ValidationError("Password did not matches", "password") 84 | 85 | @validates("cpf_cnpj") 86 | def validate_cpf_cnpj(self, value): 87 | data = self.normalize_cpf_cnpj(value) 88 | if len(data) == 11: 89 | if not check_cpf(data): 90 | raise ValidationError("CPF is wrong.", field_name="cpf_cnpj") 91 | 92 | elif len(data) == 14: 93 | pass 94 | 95 | else: 96 | raise ValidationError("Field cpf_cnpj is wrong", field_name="cpf_cnpj") 97 | 98 | 99 | class UserSchema(Schema): 100 | id = Str() 101 | full_name = Str( 102 | required=True, error_messages={'required': MSG_FIELD_REQUIRED} 103 | ) 104 | email = Email( 105 | required=True, error_messages={'required': MSG_FIELD_REQUIRED} 106 | ) 107 | cpf_cnpj = Str() 108 | date_of_birth = Date() 109 | active = Boolean() 110 | 111 | 112 | class CreateUserOutput(UserSchema): 113 | pass 114 | 115 | 116 | class AddressSchema(Schema): 117 | zip_code = Str() 118 | address = Str() 119 | number = Str() 120 | complement = Str() 121 | neighborhood = Str() 122 | city = Str() 123 | city_id = Str() 124 | state = Str() 125 | country = Str() 126 | 127 | 128 | class UserUpdateSchema(Schema): 129 | full_name = Str() 130 | address = Nested(AddressSchema) 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /apps/users/use_case.py: -------------------------------------------------------------------------------- 1 | from logging import Logger 2 | 3 | from marshmallow import ValidationError 4 | 5 | from .exceptions import ( 6 | UserMongoNotUniqueException, UserMongoValidationErrorException, 7 | UserSchemaValidationErrorException 8 | ) 9 | from .repositories import UserMongoRepository, AdminMongoRepository 10 | from .schemas import UserSchema 11 | from .utils import generate_password 12 | 13 | 14 | class CreateUserUseCase: 15 | """ 16 | Classe para criar um usuario 17 | """ 18 | 19 | def __init__(self, repo: UserMongoRepository, logger: Logger | None = None) -> None: 20 | self.repo = repo 21 | self.logger = logger 22 | 23 | def __validate(self, schema_input, input_params): 24 | try: 25 | data = schema_input.load(input_params) 26 | return data 27 | 28 | except ValidationError as err: 29 | raise UserSchemaValidationErrorException(err.messages) from err 30 | 31 | def __to_output(self, user): 32 | # Realizo um dump dos dados de acordo com o modelo salvo 33 | schema = UserSchema() 34 | result = schema.dump(user) 35 | if self.logger: 36 | self.logger.info("create.user.usecase", message="Render user output") 37 | return result 38 | 39 | def __hashed_password(self, data): 40 | # Crio um hash da minha senha 41 | return generate_password(data['password']) 42 | 43 | def validate(self, schema_input, input_params): 44 | # Desserialização os dados postados ou melhor meu payload 45 | try: 46 | data = self.__validate(schema_input=schema_input, input_params=input_params) 47 | 48 | # supress password and confirm password before loggin payload 49 | # By according LGPD the cpf is a sensitive data too. So is necessary to be supressed 50 | if self.logger: 51 | self.logger.info("create.user.usecase", message="Validate initial Payload") 52 | 53 | return data 54 | 55 | except ValidationError as err: 56 | if self.logger: 57 | self.logger.info("create.user.usecase", message="Schema validation failed", errors=err.messages) 58 | 59 | raise err 60 | 61 | def encrypted_password(self, data): 62 | # encriptar a senha 63 | try: 64 | data['password'] = self.__hashed_password(data) 65 | if self.logger: 66 | self.logger.info("create.user.usecase", message="Encrypted password") 67 | return data 68 | 69 | except Exception as err: 70 | if self.logger: 71 | self.logger.info("create.user.usecase", message="Hashed password failed") 72 | raise err 73 | 74 | def save(self, data): 75 | try: 76 | model = self.repo.insert(data) 77 | if self.logger: 78 | self.logger.info("create.user.usecase", message="User saved") 79 | 80 | return model 81 | 82 | except UserMongoNotUniqueException as err: 83 | if self.logger: 84 | self.logger.info("create.user.usecase", message="User already saved") 85 | raise err 86 | 87 | except UserMongoValidationErrorException as err: 88 | if self.logger: 89 | self.logger.info("create.user.usecase", message="User fields and model are not equal",) 90 | raise err 91 | 92 | except Exception as err: 93 | if self.logger: 94 | self.logger.info("create.user.usecase", message="User not saved. Strange error") 95 | raise err 96 | 97 | def execute(self, schema_input, input_params): 98 | data = self.validate(schema_input=schema_input, input_params=input_params) 99 | data = self.encrypted_password(data=data) 100 | model = self.save(data=data) 101 | 102 | return self.__to_output(user=model) 103 | 104 | 105 | class GetUserByCpfCnpjUseCase: 106 | def __init__(self, repo: AdminMongoRepository, logger: Logger | None = None) -> None: 107 | self.repo = repo 108 | self.logger = logger 109 | 110 | def get_admin(self, email): 111 | # Buscamos nosso usuário pelo email 112 | if self.logger: 113 | self.logger.info("users.GetUserByCpfCnpjUseCase.usecase", message="Get admin current user") 114 | 115 | return self.repo.get_admin_by_email(email) 116 | 117 | def check_admin_is_active(self, admin): 118 | if self.logger: 119 | self.logger.info("users.GetUserByCpfCnpjUseCase.usecase", message="Check admin is active") 120 | 121 | if not admin.is_active(): 122 | if self.logger: 123 | self.logger.info("users.GetUserByCpfCnpjUseCase.usecase", message="The admin is not activate") 124 | 125 | raise Exception('Not allowed Admin') 126 | 127 | def get_user_by_cpf_cnpj(self, cpf_cnpj): 128 | # Buscamos nosso usuário pelo email 129 | if self.logger: 130 | self.logger.info("users.GetUserByCpfCnpjUseCase.usecase", message="Get the user by cpf and cnpj") 131 | 132 | return self.repo.get_user_by_cpf_cnpj(cpf_cnpj=cpf_cnpj) 133 | 134 | def __to_output(self, user): 135 | # Realizo um dump dos dados de acordo com o modelo salvo 136 | schema = UserSchema() 137 | result = schema.dump(user) 138 | 139 | if self.logger: 140 | self.logger.info("users.GetUserByCpfCnpjUseCase.usecase", message="Render user output") 141 | 142 | return result 143 | 144 | def execute(self, current_user, cpf_cnpj, *args, **kargs): 145 | admin = self.get_admin(email=current_user) 146 | self.check_admin_is_active(admin=admin) 147 | user = self.get_user_by_cpf_cnpj(cpf_cnpj=cpf_cnpj) 148 | return self.__to_output(user=user) 149 | -------------------------------------------------------------------------------- /apps/users/utils.py: -------------------------------------------------------------------------------- 1 | # Third 2 | from bcrypt import gensalt, hashpw 3 | 4 | # Apps 5 | # Local 6 | 7 | def generate_password(password: str): 8 | return hashpw(password.encode(), gensalt(12)) 9 | 10 | 11 | def check_password_in_signup(password: str, confirm_password: str): 12 | 13 | if not password: 14 | return False 15 | 16 | if not confirm_password: 17 | return False 18 | 19 | if not password == confirm_password: 20 | return False 21 | 22 | return True 23 | 24 | class Cpf: 25 | def __init__(self, cpf): 26 | # forçando inteiros para serem transformados para string 27 | self.cpf = str(cpf) 28 | self.normalize_cpf_cnpj() 29 | 30 | def normalize_cpf_cnpj(self): 31 | # normalizo a string retirando caracteres especiais 32 | self.cpf = self.cpf.strip().replace(".", "").replace("-", "").replace("/", "") 33 | 34 | def validate(self): 35 | if self.check_len(): 36 | return False 37 | 38 | first_digit = self.calculate_first_digit() 39 | if self.cpf[9] != str(first_digit): 40 | return False 41 | 42 | second_digit = self.calculate_second_digit() 43 | if self.cpf[10] != str(second_digit): 44 | return False 45 | 46 | return True 47 | 48 | def check_len(self): 49 | return len(self.cpf) != 11 50 | 51 | def calculate_first_digit(self): 52 | first_digit = 0 53 | for i in range(10, 1, -1): 54 | first_digit += int(self.cpf[10 - i]) * i 55 | 56 | rest = first_digit % 11 57 | 58 | return self.cpf_rule(rest) 59 | 60 | def calculate_second_digit(self): 61 | second_digit = 0 62 | for i in range(11, 1, -1): 63 | second_digit += int(self.cpf[11 - i]) * i 64 | 65 | rest = second_digit % 11 66 | 67 | return self.cpf_rule(rest) 68 | 69 | def cpf_rule(self, rest): 70 | if rest < 2: 71 | return 0 72 | else: 73 | return 11 - rest 74 | -------------------------------------------------------------------------------- /boot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | source venv/bin/activate 3 | 4 | case "$FLASK_ENV" in 5 | production) 6 | # exec gunicorn -b :$PORT --access-logfile - --error-logfile - "apps:create_app('production')" 7 | exec gunicorn -w $WORKERS -b :$PORT --access-logfile - --error-logfile - application:app 8 | ;; 9 | *) exec make run ;; 10 | esac 11 | -------------------------------------------------------------------------------- /confs/gunicorn_api_users.conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | bind = "127.0.0.1:9000" 4 | workers = (os.sysconf("SC_NPROCESSORS_ONLN") * 2) + 1 5 | loglevel = "error" 6 | pidfile = "/home/apiflask/run/app.pid" 7 | accesslog = "/home/apiflask/logs/gunicorn/access-app.log" 8 | errorlog = "/home/apiflask/logs/gunicorn/error-app.log" 9 | secure_scheme_headers = { 10 | 'X-FORWARDED-PROTOCOL': 'http', 11 | 'X-FORWARDED-PROTO': 'http', 12 | } 13 | -------------------------------------------------------------------------------- /confs/nginx_api_users.conf: -------------------------------------------------------------------------------- 1 | upstream gunicorn_api_users { 2 | server 127.0.0.1:9000; 3 | } 4 | 5 | server { 6 | listen 80; 7 | listen [::]:80; 8 | server_name default_server; 9 | 10 | client_max_body_size 10M; 11 | keepalive_timeout 15; 12 | 13 | access_log /home/apiflask/logs/nginx/access-site-api-users.log; 14 | error_log /home/apiflask/logs/nginx/error-site-api-users.log error; 15 | 16 | location / { 17 | 18 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 19 | proxy_set_header Host $http_host; 20 | proxy_redirect off; 21 | 22 | if (!-f $request_filename) { 23 | proxy_pass http://gunicorn_api_users; 24 | break; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /confs/nginx_api_users_https.conf: -------------------------------------------------------------------------------- 1 | upstream gunicorn_api_users { 2 | server 127.0.0.1:9000; 3 | } 4 | 5 | server { 6 | listen 80; 7 | listen [::]:80; 8 | server_name default_server; 9 | return 301 https://$server_name$request_uri; 10 | } 11 | 12 | server { 13 | listen 443 ssl ; 14 | 15 | client_max_body_size 10M; 16 | keepalive_timeout 15; 17 | 18 | server_name default_server; 19 | 20 | ssl on; 21 | 22 | ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; 23 | ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; 24 | 25 | include snippets/ssl-params.conf; 26 | 27 | access_log /home/apiflask/logs/nginx/access-site-api-users.log; 28 | error_log /home/apiflask/logs/nginx/error-site-api-users.log error; 29 | 30 | location / { 31 | 32 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 33 | proxy_set_header Host $http_host; 34 | proxy_redirect off; 35 | 36 | if (!-f $request_filename) { 37 | proxy_pass http://gunicorn_api_users; 38 | break; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /confs/supervisor_api_users.conf: -------------------------------------------------------------------------------- 1 | [group:api_users] 2 | programs=gunicorn_api_users 3 | 4 | 5 | [program:gunicorn_api_users] 6 | command=/home/apiflask/venvs/bin/gunicorn -c /home/apiflask/scripts/gunicorn_api_users.conf.py application:app 7 | 8 | directory=/home/apiflask/sites/flask-api-users/ 9 | 10 | user=apiflask 11 | group=apiflask 12 | 13 | stdout_logfile=/home/apiflask/logs/supervisor/access-site-api-users.log 14 | stderr_logfile=/home/apiflask/logs/supervisor/error-site-api-users.log 15 | 16 | autostart=true 17 | autorestart=true 18 | redirect_stderr=True 19 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.5" 2 | services: 3 | test: &defaults 4 | build: 5 | context: . 6 | target: ci 7 | volumes: 8 | - .:/home/app/ 9 | command: pytest -s -vvv 10 | env_file: 11 | - ./.docker/dev.env 12 | - ./.docker/test.env 13 | 14 | app: 15 | <<: *defaults 16 | build: 17 | context: . 18 | target: dev 19 | # volumes: 20 | # - .:/home/app 21 | # - ./venv:/home/app/venv 22 | command: flask run --host=0.0.0.0 --port=8080 23 | # command: tail -f /dev/null 24 | working_dir: /home/app 25 | ports: 26 | - "5000:8080" 27 | env_file: 28 | - ./.docker/dev.env 29 | depends_on: 30 | - mongodb 31 | 32 | ci: 33 | <<: *defaults 34 | build: 35 | context: . 36 | target: ci 37 | command: 38 | - /bin/sh 39 | - c 40 | - | 41 | pytest 42 | volumes: [] 43 | 44 | mongodb: 45 | image: mongo 46 | ports: 47 | - "27017:27017" 48 | 49 | redis: 50 | image: redis 51 | ports: 52 | - "6379:6379" 53 | 54 | elasticsearch: 55 | image: docker.elastic.co/elasticsearch/elasticsearch-oss:7.10.0 56 | volumes: 57 | - es_data:/usr/share/elasticsearch/data 58 | environment: 59 | - http.host=0.0.0.0 60 | - "discovery.type=single-node" 61 | - "ES_JAVA_OPTS=-Xms512m -Xmx512m" 62 | 63 | graylog: 64 | image: graylog/graylog 65 | volumes: 66 | - graylog_data:/usr/share/graylog/data 67 | env_file: 68 | - ./.docker/graylog/dev.env 69 | restart: always 70 | depends_on: 71 | - mongodb 72 | - elasticsearch 73 | ports: 74 | # Graylog web interface and REST API 75 | - 12200:9000 76 | # GELF TCP 77 | - 12201:12201 78 | # GELF UDP 79 | - 12201:12201/udp 80 | 81 | 82 | volumes: 83 | dependencies: {} 84 | mongo_data: 85 | es_data: 86 | -------------------------------------------------------------------------------- /fabfile.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from fabric import Connection 4 | from invoke import task 5 | 6 | 7 | SITE_DIR = '~/sites/flask-api-users' 8 | VENV = '~/venvs/' 9 | PYTHON_BIN = VENV + 'bin/python' 10 | PIP_BIN = VENV + 'bin/pip' 11 | 12 | 13 | def whoami(c): 14 | """ 15 | Print the envirioment and user 16 | """ 17 | c.run('whoami') 18 | 19 | 20 | def git(conn, cmd): 21 | """ 22 | Create a method to execute git on the server 23 | """ 24 | with conn.cd('{}'.format(SITE_DIR)): 25 | conn.run('git {}'.format(cmd)) 26 | 27 | 28 | def checkout(conn, branch='master', tag=None): 29 | """ 30 | Run git checkout to branch or tag 31 | """ 32 | if tag: 33 | git(conn, 'checkout {}'.format(tag)) 34 | 35 | else: 36 | git(conn, 'checkout {}'.format(branch)) 37 | 38 | 39 | def pull(conn): 40 | """ 41 | Run git pull command on repository 42 | """ 43 | git(conn, "pull") 44 | 45 | 46 | def fetch(conn): 47 | """ 48 | Run git pull command on repository 49 | """ 50 | git(conn, "fetch") 51 | 52 | 53 | def sudo(conn, cmd): 54 | conn.sudo(cmd) 55 | 56 | 57 | def install_requirements(conn, env='prod'): 58 | """ 59 | Install all requirements by the server 60 | """ 61 | cmd = "{} install -r requirements/{}.txt".format(PIP_BIN, env) 62 | with conn.cd('{}'.format(SITE_DIR)): 63 | conn.run(cmd) 64 | 65 | 66 | @task 67 | def uname(c): 68 | """ Prints information about the host. """ 69 | c.run('uname -a') 70 | whoami(c) 71 | 72 | 73 | def restart(conn): 74 | """ 75 | Restart supervisor and webserver 76 | """ 77 | conn.sudo("supervisorctl restart api_users:gunicorn_api_users") 78 | conn.sudo("service nginx restart") 79 | 80 | 81 | @task 82 | def status(c): 83 | """ 84 | Status supervisor and webserver 85 | """ 86 | c.sudo("supervisorctl status api_users:gunicorn_api_users") 87 | c.sudo("service nginx status") 88 | 89 | 90 | @task 91 | def stop(c): 92 | """ 93 | Stopping supervisor 94 | """ 95 | c.sudo("supervisorctl stop api_users:gunicorn_api_users") 96 | 97 | 98 | @task 99 | def deploy(c, tag=None): 100 | """ 101 | Deploy the code to context 102 | """ 103 | 104 | fetch(c) 105 | 106 | if tag: 107 | checkout(c, tag) 108 | else: 109 | checkout(c) 110 | 111 | install_requirements(c) 112 | 113 | restart(c) 114 | 115 | -------------------------------------------------------------------------------- /gunicorn_settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | bind = "127.0.0.1:9000" 4 | workers = (os.sysconf("SC_NPROCESSORS_ONLN") * 2) + 1 5 | threads = 4 6 | worker_class = "gthread" 7 | worker_connections = 1000 8 | timeout = 60 9 | keepalive = 30 10 | daemon = False 11 | pidfile = None 12 | loglevel = "debug" 13 | accesslog = "-" 14 | errorlog = "-" 15 | secure_scheme_headers = { 16 | 'X-FORWARDED-PROTOCOL': 'http', 17 | 'X-FORWARDED-PROTO': 'http', 18 | } 19 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "flask-api-users" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Lucas "] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.11" 10 | flask = "2.2.2" 11 | flask-restful = "^0.3.9" 12 | python-dotenv = "^1.0.0" 13 | pytz = "^2023.3" 14 | flask-mongoengine = "^1.0.0" 15 | marshmallow = "^3.19.0" 16 | bcrypt = "^4.0.1" 17 | flask-jwt-extended = "^4.4.4" 18 | pyee = "^9.0.4" 19 | structlog = "^23.1.0" 20 | mongomock = "^4.1.2" 21 | flask-apispec = "^0.11.4" 22 | 23 | 24 | [tool.poetry.group.test.dependencies] 25 | pytest = "^7.3.1" 26 | pytest-cov = "^4.0.0" 27 | pytest-runner = "^6.0.0" 28 | factory-boy = "^3.2.1" 29 | snapshottest = "^0.6.0" 30 | coverage-badge = "^1.1.0" 31 | 32 | 33 | [tool.poetry.group.docs.dependencies] 34 | mkdocs = "^1.4.2" 35 | 36 | 37 | [tool.poetry.group.dev.dependencies] 38 | ipdb = "^0.13.13" 39 | isort = "^5.12.0" 40 | flask-shell-ipython = "^0.5.1" 41 | pylint = "^2.17.4" 42 | 43 | 44 | [tool.poetry.group.prod.dependencies] 45 | gunicorn = "^20.1.0" 46 | 47 | [build-system] 48 | requires = ["poetry-core"] 49 | build-backend = "poetry.core.masonry.api" 50 | 51 | [virtualenvs] 52 | create = false 53 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | norecursedirs = venv build* .git _build tmp* __pypackages__ .tox .pytest_cache .local .vscode .devcontainer .cache 3 | python_files = tests/**/*.py 4 | addopts = -s -vv -x --ff --cov-fail-under 60 --cov=apps --cov-report html --color=yes --disable-warnings 5 | markers = 6 | unit: mark a test as a unit. 7 | integration: mark a test as a integration. 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aniso8601==9.0.1 ; python_version >= "3.11" and python_version < "4.0" 2 | apispec==6.3.0 ; python_version >= "3.11" and python_version < "4.0" 3 | appnope==0.1.3 ; python_version >= "3.11" and python_version < "4.0" and sys_platform == "darwin" 4 | astroid==2.15.4 ; python_version >= "3.11" and python_version < "4.0" 5 | asttokens==2.2.1 ; python_version >= "3.11" and python_version < "4.0" 6 | backcall==0.2.0 ; python_version >= "3.11" and python_version < "4.0" 7 | bcrypt==4.0.1 ; python_version >= "3.11" and python_version < "4.0" 8 | click==8.1.3 ; python_version >= "3.11" and python_version < "4.0" 9 | colorama==0.4.6 ; python_version >= "3.11" and python_version < "4.0" and sys_platform == "win32" or python_version >= "3.11" and python_version < "4.0" and platform_system == "Windows" 10 | coverage-badge==1.1.0 ; python_version >= "3.11" and python_version < "4.0" 11 | coverage==7.2.5 ; python_version >= "3.11" and python_version < "4.0" 12 | coverage[toml]==7.2.5 ; python_version >= "3.11" and python_version < "4.0" 13 | decorator==5.1.1 ; python_version >= "3.11" and python_version < "4.0" 14 | dill==0.3.6 ; python_version >= "3.11" and python_version < "4.0" 15 | dnspython==2.3.0 ; python_version >= "3.11" and python_version < "4.0" 16 | email-validator==2.0.0.post2 ; python_version >= "3.11" and python_version < "4.0" 17 | executing==1.2.0 ; python_version >= "3.11" and python_version < "4.0" 18 | factory-boy==3.2.1 ; python_version >= "3.11" and python_version < "4.0" 19 | faker==18.7.0 ; python_version >= "3.11" and python_version < "4.0" 20 | fastdiff==0.3.0 ; python_version >= "3.11" and python_version < "4.0" 21 | flask-apispec==0.11.4 ; python_version >= "3.11" and python_version < "4.0" 22 | flask-jwt-extended==4.4.4 ; python_version >= "3.11" and python_version < "4" 23 | flask-mongoengine==1.0.0 ; python_version >= "3.11" and python_version < "4.0" 24 | flask-restful==0.3.9 ; python_version >= "3.11" and python_version < "4.0" 25 | flask-shell-ipython==0.5.1 ; python_version >= "3.11" and python_version < "4" 26 | flask-wtf==1.1.1 ; python_version >= "3.11" and python_version < "4.0" 27 | flask==2.2.2 ; python_version >= "3.11" and python_version < "4.0" 28 | ghp-import==2.1.0 ; python_version >= "3.11" and python_version < "4.0" 29 | idna==3.4 ; python_version >= "3.11" and python_version < "4.0" 30 | iniconfig==2.0.0 ; python_version >= "3.11" and python_version < "4.0" 31 | ipdb==0.13.13 ; python_version >= "3.11" and python_version < "4.0" 32 | ipython==8.13.2 ; python_version >= "3.11" and python_version < "4.0" 33 | isort==5.12.0 ; python_version >= "3.11" and python_version < "4.0" 34 | itsdangerous==2.1.2 ; python_version >= "3.11" and python_version < "4.0" 35 | jedi==0.18.2 ; python_version >= "3.11" and python_version < "4.0" 36 | jinja2==3.1.2 ; python_version >= "3.11" and python_version < "4.0" 37 | lazy-object-proxy==1.9.0 ; python_version >= "3.11" and python_version < "4.0" 38 | markdown==3.3.7 ; python_version >= "3.11" and python_version < "4.0" 39 | markupsafe==2.1.2 ; python_version >= "3.11" and python_version < "4.0" 40 | marshmallow==3.19.0 ; python_version >= "3.11" and python_version < "4.0" 41 | matplotlib-inline==0.1.6 ; python_version >= "3.11" and python_version < "4.0" 42 | mccabe==0.7.0 ; python_version >= "3.11" and python_version < "4.0" 43 | mergedeep==1.3.4 ; python_version >= "3.11" and python_version < "4.0" 44 | mkdocs==1.4.3 ; python_version >= "3.11" and python_version < "4.0" 45 | mongoengine==0.27.0 ; python_version >= "3.11" and python_version < "4.0" 46 | mongomock==4.1.2 ; python_version >= "3.11" and python_version < "4.0" 47 | packaging==23.1 ; python_version >= "3.11" and python_version < "4.0" 48 | parso==0.8.3 ; python_version >= "3.11" and python_version < "4.0" 49 | pexpect==4.8.0 ; python_version >= "3.11" and python_version < "4.0" and sys_platform != "win32" 50 | pickleshare==0.7.5 ; python_version >= "3.11" and python_version < "4.0" 51 | platformdirs==3.5.0 ; python_version >= "3.11" and python_version < "4.0" 52 | pluggy==1.0.0 ; python_version >= "3.11" and python_version < "4.0" 53 | prompt-toolkit==3.0.38 ; python_version >= "3.11" and python_version < "4.0" 54 | ptyprocess==0.7.0 ; python_version >= "3.11" and python_version < "4.0" and sys_platform != "win32" 55 | pure-eval==0.2.2 ; python_version >= "3.11" and python_version < "4.0" 56 | pyee==9.1.0 ; python_version >= "3.11" and python_version < "4.0" 57 | pygments==2.15.1 ; python_version >= "3.11" and python_version < "4.0" 58 | pyjwt==2.7.0 ; python_version >= "3.11" and python_version < "4" 59 | pylint==2.17.4 ; python_version >= "3.11" and python_version < "4.0" 60 | pymongo==4.3.3 ; python_version >= "3.11" and python_version < "4.0" 61 | pytest-cov==4.0.0 ; python_version >= "3.11" and python_version < "4.0" 62 | pytest-runner==6.0.0 ; python_version >= "3.11" and python_version < "4.0" 63 | pytest==7.3.1 ; python_version >= "3.11" and python_version < "4.0" 64 | python-dateutil==2.8.2 ; python_version >= "3.11" and python_version < "4.0" 65 | python-dotenv==1.0.0 ; python_version >= "3.11" and python_version < "4.0" 66 | pytz==2023.3 ; python_version >= "3.11" and python_version < "4.0" 67 | pyyaml-env-tag==0.1 ; python_version >= "3.11" and python_version < "4.0" 68 | pyyaml==6.0 ; python_version >= "3.11" and python_version < "4.0" 69 | sentinels==1.0.0 ; python_version >= "3.11" and python_version < "4.0" 70 | six==1.16.0 ; python_version >= "3.11" and python_version < "4.0" 71 | snapshottest==0.6.0 ; python_version >= "3.11" and python_version < "4.0" 72 | stack-data==0.6.2 ; python_version >= "3.11" and python_version < "4.0" 73 | structlog==23.1.0 ; python_version >= "3.11" and python_version < "4.0" 74 | termcolor==2.3.0 ; python_version >= "3.11" and python_version < "4.0" 75 | tomlkit==0.11.8 ; python_version >= "3.11" and python_version < "4.0" 76 | traitlets==5.9.0 ; python_version >= "3.11" and python_version < "4.0" 77 | typing-extensions==4.5.0 ; python_version >= "3.11" and python_version < "4.0" 78 | wasmer-compiler-cranelift==1.1.0 ; python_version >= "3.11" and python_version < "4.0" 79 | wasmer==1.1.0 ; python_version >= "3.11" and python_version < "4.0" 80 | watchdog==3.0.0 ; python_version >= "3.11" and python_version < "4.0" 81 | wcwidth==0.2.6 ; python_version >= "3.11" and python_version < "4.0" 82 | webargs==8.2.0 ; python_version >= "3.11" and python_version < "4.0" 83 | werkzeug==2.3.4 ; python_version >= "3.11" and python_version < "4.0" 84 | wrapt==1.15.0 ; python_version >= "3.11" and python_version < "4.0" 85 | wtforms==3.0.1 ; python_version >= "3.11" and python_version < "4.0" 86 | wtforms[email]==3.0.1 ; python_version >= "3.11" and python_version < "4.0" 87 | -------------------------------------------------------------------------------- /requirements/base.txt: -------------------------------------------------------------------------------- 1 | aniso8601==9.0.1 ; python_version >= "3.11" and python_version < "4.0" 2 | apispec==6.3.0 ; python_version >= "3.11" and python_version < "4.0" 3 | appnope==0.1.3 ; python_version >= "3.11" and python_version < "4.0" and sys_platform == "darwin" 4 | astroid==2.15.4 ; python_version >= "3.11" and python_version < "4.0" 5 | asttokens==2.2.1 ; python_version >= "3.11" and python_version < "4.0" 6 | backcall==0.2.0 ; python_version >= "3.11" and python_version < "4.0" 7 | bcrypt==4.0.1 ; python_version >= "3.11" and python_version < "4.0" 8 | click==8.1.3 ; python_version >= "3.11" and python_version < "4.0" 9 | colorama==0.4.6 ; python_version >= "3.11" and python_version < "4.0" and sys_platform == "win32" or python_version >= "3.11" and python_version < "4.0" and platform_system == "Windows" 10 | coverage-badge==1.1.0 ; python_version >= "3.11" and python_version < "4.0" 11 | coverage==7.2.5 ; python_version >= "3.11" and python_version < "4.0" 12 | coverage[toml]==7.2.5 ; python_version >= "3.11" and python_version < "4.0" 13 | decorator==5.1.1 ; python_version >= "3.11" and python_version < "4.0" 14 | dill==0.3.6 ; python_version >= "3.11" and python_version < "4.0" 15 | dnspython==2.3.0 ; python_version >= "3.11" and python_version < "4.0" 16 | email-validator==2.0.0.post2 ; python_version >= "3.11" and python_version < "4.0" 17 | executing==1.2.0 ; python_version >= "3.11" and python_version < "4.0" 18 | factory-boy==3.2.1 ; python_version >= "3.11" and python_version < "4.0" 19 | faker==18.7.0 ; python_version >= "3.11" and python_version < "4.0" 20 | fastdiff==0.3.0 ; python_version >= "3.11" and python_version < "4.0" 21 | flask-apispec==0.11.4 ; python_version >= "3.11" and python_version < "4.0" 22 | flask-jwt-extended==4.4.4 ; python_version >= "3.11" and python_version < "4" 23 | flask-mongoengine==1.0.0 ; python_version >= "3.11" and python_version < "4.0" 24 | flask-restful==0.3.9 ; python_version >= "3.11" and python_version < "4.0" 25 | flask-shell-ipython==0.5.1 ; python_version >= "3.11" and python_version < "4" 26 | flask-wtf==1.1.1 ; python_version >= "3.11" and python_version < "4.0" 27 | flask==2.2.2 ; python_version >= "3.11" and python_version < "4.0" 28 | ghp-import==2.1.0 ; python_version >= "3.11" and python_version < "4.0" 29 | idna==3.4 ; python_version >= "3.11" and python_version < "4.0" 30 | iniconfig==2.0.0 ; python_version >= "3.11" and python_version < "4.0" 31 | ipdb==0.13.13 ; python_version >= "3.11" and python_version < "4.0" 32 | ipython==8.13.2 ; python_version >= "3.11" and python_version < "4.0" 33 | isort==5.12.0 ; python_version >= "3.11" and python_version < "4.0" 34 | itsdangerous==2.1.2 ; python_version >= "3.11" and python_version < "4.0" 35 | jedi==0.18.2 ; python_version >= "3.11" and python_version < "4.0" 36 | jinja2==3.1.2 ; python_version >= "3.11" and python_version < "4.0" 37 | lazy-object-proxy==1.9.0 ; python_version >= "3.11" and python_version < "4.0" 38 | markdown==3.3.7 ; python_version >= "3.11" and python_version < "4.0" 39 | markupsafe==2.1.2 ; python_version >= "3.11" and python_version < "4.0" 40 | marshmallow==3.19.0 ; python_version >= "3.11" and python_version < "4.0" 41 | matplotlib-inline==0.1.6 ; python_version >= "3.11" and python_version < "4.0" 42 | mccabe==0.7.0 ; python_version >= "3.11" and python_version < "4.0" 43 | mergedeep==1.3.4 ; python_version >= "3.11" and python_version < "4.0" 44 | mkdocs==1.4.3 ; python_version >= "3.11" and python_version < "4.0" 45 | mongoengine==0.27.0 ; python_version >= "3.11" and python_version < "4.0" 46 | mongomock==4.1.2 ; python_version >= "3.11" and python_version < "4.0" 47 | packaging==23.1 ; python_version >= "3.11" and python_version < "4.0" 48 | parso==0.8.3 ; python_version >= "3.11" and python_version < "4.0" 49 | pexpect==4.8.0 ; python_version >= "3.11" and python_version < "4.0" and sys_platform != "win32" 50 | pickleshare==0.7.5 ; python_version >= "3.11" and python_version < "4.0" 51 | platformdirs==3.5.0 ; python_version >= "3.11" and python_version < "4.0" 52 | pluggy==1.0.0 ; python_version >= "3.11" and python_version < "4.0" 53 | prompt-toolkit==3.0.38 ; python_version >= "3.11" and python_version < "4.0" 54 | ptyprocess==0.7.0 ; python_version >= "3.11" and python_version < "4.0" and sys_platform != "win32" 55 | pure-eval==0.2.2 ; python_version >= "3.11" and python_version < "4.0" 56 | pyee==9.1.0 ; python_version >= "3.11" and python_version < "4.0" 57 | pygments==2.15.1 ; python_version >= "3.11" and python_version < "4.0" 58 | pyjwt==2.7.0 ; python_version >= "3.11" and python_version < "4" 59 | pylint==2.17.4 ; python_version >= "3.11" and python_version < "4.0" 60 | pymongo==4.3.3 ; python_version >= "3.11" and python_version < "4.0" 61 | pytest-cov==4.0.0 ; python_version >= "3.11" and python_version < "4.0" 62 | pytest-runner==6.0.0 ; python_version >= "3.11" and python_version < "4.0" 63 | pytest==7.3.1 ; python_version >= "3.11" and python_version < "4.0" 64 | python-dateutil==2.8.2 ; python_version >= "3.11" and python_version < "4.0" 65 | python-dotenv==1.0.0 ; python_version >= "3.11" and python_version < "4.0" 66 | pytz==2023.3 ; python_version >= "3.11" and python_version < "4.0" 67 | pyyaml-env-tag==0.1 ; python_version >= "3.11" and python_version < "4.0" 68 | pyyaml==6.0 ; python_version >= "3.11" and python_version < "4.0" 69 | sentinels==1.0.0 ; python_version >= "3.11" and python_version < "4.0" 70 | six==1.16.0 ; python_version >= "3.11" and python_version < "4.0" 71 | snapshottest==0.6.0 ; python_version >= "3.11" and python_version < "4.0" 72 | stack-data==0.6.2 ; python_version >= "3.11" and python_version < "4.0" 73 | structlog==23.1.0 ; python_version >= "3.11" and python_version < "4.0" 74 | termcolor==2.3.0 ; python_version >= "3.11" and python_version < "4.0" 75 | tomlkit==0.11.8 ; python_version >= "3.11" and python_version < "4.0" 76 | traitlets==5.9.0 ; python_version >= "3.11" and python_version < "4.0" 77 | typing-extensions==4.5.0 ; python_version >= "3.11" and python_version < "4.0" 78 | wasmer-compiler-cranelift==1.1.0 ; python_version >= "3.11" and python_version < "4.0" 79 | wasmer==1.1.0 ; python_version >= "3.11" and python_version < "4.0" 80 | watchdog==3.0.0 ; python_version >= "3.11" and python_version < "4.0" 81 | wcwidth==0.2.6 ; python_version >= "3.11" and python_version < "4.0" 82 | webargs==8.2.0 ; python_version >= "3.11" and python_version < "4.0" 83 | werkzeug==2.3.4 ; python_version >= "3.11" and python_version < "4.0" 84 | wrapt==1.15.0 ; python_version >= "3.11" and python_version < "4.0" 85 | wtforms==3.0.1 ; python_version >= "3.11" and python_version < "4.0" 86 | wtforms[email]==3.0.1 ; python_version >= "3.11" and python_version < "4.0" 87 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Python 4 | from os import getenv 5 | from os.path import dirname, isfile, join 6 | 7 | # Third 8 | from dotenv import load_dotenv 9 | 10 | 11 | # a partir do arquivo atual adicione ao path o arquivo .env 12 | _ENV_FILE = join(dirname(__file__), '.env') 13 | 14 | # existindo o arquivo faça a leitura do arquivo através da função load_dotenv 15 | if isfile(_ENV_FILE): 16 | load_dotenv(dotenv_path=_ENV_FILE) 17 | 18 | 19 | # Apps 20 | from apps.app import create_app 21 | 22 | # instancia nossa função factory criada anteriormente 23 | app = create_app() 24 | 25 | if __name__ == '__main__': 26 | ip = '0.0.0.0' 27 | port = app.config['PORT'] 28 | debug = app.config['DEBUG'] 29 | 30 | # executa o servidor web do flask 31 | app.run( 32 | host=ip, debug=debug, port=port, use_reloader=debug 33 | ) 34 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test=pytest 3 | 4 | [flake8] 5 | exclude = application.py, conftest.py, tests/**/* 6 | max-line-length = 120 7 | 8 | [isort] 9 | skip=application.py,conftest.py 10 | default_section = THIRDPARTY 11 | known_first_party = apps 12 | known_flask = flask 13 | sections = FUTURE,STDLIB,FLASK,THIRDPARTY,FIRSTPARTY,LOCALFOLDER 14 | import_heading_future = Future 15 | import_heading_stdlib = Python 16 | import_heading_thirdparty = Third 17 | import_heading_firstparty = Apps 18 | import_heading_localfolder = Local 19 | indent=' ' 20 | multi_line_output=3 21 | 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Third 4 | from setuptools import find_packages, setup 5 | from __version__ import version 6 | 7 | __description__ = 'Api Python Flask' 8 | __long_description__ = 'This is an API to Flask Api Users' 9 | 10 | __author__ = 'Lucas Simon' 11 | __author_email__ = 'lucassrod@gmail.com' 12 | 13 | testing_extras = [ 14 | 'pytest', 15 | 'pytest-cov', 16 | ] 17 | 18 | setup( 19 | name='api', 20 | version=version, 21 | author=__author__, 22 | author_email=__author_email__, 23 | packages=find_packages(), 24 | license='MIT', 25 | description=__description__, 26 | long_description=__long_description__, 27 | url='https://github.com/lucassimon/flask-api-users.git', 28 | keywords='API, MongoDB', 29 | include_package_data=True, 30 | zip_safe=False, 31 | classifiers=[ 32 | 'Intended Audience :: Developers', 33 | 'Intended Audience :: System Administrators', 34 | 'Operating System :: OS Independent', 35 | 'Topic :: Software Development', 36 | 'Environment :: Web Environment', 37 | 'Programming Language :: Python :: 3.2', 38 | 'Programming Language :: Python :: 3.3', 39 | 'Programming Language :: Python :: 3.4', 40 | 'Programming Language :: Python :: 3.5', 41 | 'Programming Language :: Python :: 3.6', 42 | 'License :: OSI Approved :: MIT License', 43 | ], 44 | setup_requires=['pytest-runner'], 45 | tests_require=['pytest'], 46 | extras_require={ 47 | 'testing': testing_extras, 48 | }, 49 | ) 50 | -------------------------------------------------------------------------------- /static/Insomnia_2023-05-11.json: -------------------------------------------------------------------------------- 1 | {"_type":"export","__export_format":4,"__export_date":"2023-05-11T22:20:54.337Z","__export_source":"insomnia.desktop.app:v2023.2.0","resources":[{"_id":"req_cc14fe796d2147968bf10282d494406d","parentId":"wrk_eb8a016621754748848e8569a6d5408b","modified":1682713530364,"created":1682713420284,"url":"{{API_URL}}/","name":"Index","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1682713420284,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"wrk_eb8a016621754748848e8569a6d5408b","parentId":null,"modified":1682713420278,"created":1682713420278,"name":"New Document","description":"","scope":"design","_type":"workspace"},{"_id":"req_6149d8de6d3d4b8998034b5771c0db56","parentId":"wrk_eb8a016621754748848e8569a6d5408b","modified":1683831260775,"created":1683829737789,"url":"{{API_URL}}/admin/users/page/1","name":"List All Users Paginated with Admin","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{"type":"bearer","token":"{% response 'body', 'req_12efdf405a0949e09fac07e5624fb4e8', 'b64::JC5kYXRhLnRva2Vu::46b', 'never', 60 %}","prefix":"Bearer"},"metaSortKey":-1682713420259,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_7350fd4532c54219b0299ba34dc80da8","parentId":"wrk_eb8a016621754748848e8569a6d5408b","modified":1683843024935,"created":1683831232836,"url":"{{API_URL}}/admin/users/cpf/21226594913","name":"Get User By CpfCnpj with Admin","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{"type":"bearer","token":"{% response 'body', 'req_12efdf405a0949e09fac07e5624fb4e8', 'b64::JC5kYXRhLnRva2Vu::46b', 'never', 60 %}","prefix":"Bearer"},"metaSortKey":-1682713420246.5,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_c9af93fabd95428880cd8fd30f766915","parentId":"wrk_eb8a016621754748848e8569a6d5408b","modified":1683842650405,"created":1682713539087,"url":"{{API_URL}}/users","name":"Create User","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"full_name\": \"Some Full Name\",\n\t\"email\": \"some-email@gmail.com\",\n\t\"cpf_cnpj\": \"21226594913\",\n\t\"date_of_birth\": \"2010-11-12\",\n\t\"password\": \"teste123\",\n\t\"confirm_password\": \"teste123\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json"}],"authentication":{},"metaSortKey":-1682713420234,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_041d4bd0f9c64420be5bf5a9d5f7c0ed","parentId":"wrk_eb8a016621754748848e8569a6d5408b","modified":1683826998828,"created":1682714145117,"url":"{{API_URL}}/auth","name":"Login","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\t\n\t\"email\": \"lucassrod@gmail.com\",\n\t\"password\": \"teste123\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json"}],"authentication":{},"metaSortKey":-1682713420184,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_12efdf405a0949e09fac07e5624fb4e8","parentId":"wrk_eb8a016621754748848e8569a6d5408b","modified":1683829707578,"created":1683829550049,"url":"{{API_URL}}/auth/admin","name":"Login Admin","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\t\n\t\"email\": \"t@t.com\",\n\t\"password\": \"teste123\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json"}],"authentication":{},"metaSortKey":-1682713420134,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"env_d882da9fec9abe78a0b12491a28b071e71efb65b","parentId":"wrk_eb8a016621754748848e8569a6d5408b","modified":1682713507915,"created":1682713420282,"name":"Base Environment","data":{},"dataPropertyOrder":{},"color":null,"isPrivate":false,"metaSortKey":1682713420282,"_type":"environment"},{"_id":"jar_d882da9fec9abe78a0b12491a28b071e71efb65b","parentId":"wrk_eb8a016621754748848e8569a6d5408b","modified":1682713420282,"created":1682713420282,"name":"Default Jar","cookies":[],"_type":"cookie_jar"},{"_id":"spc_565638bfd880412e82c69973a556cc32","parentId":"wrk_eb8a016621754748848e8569a6d5408b","modified":1683826149728,"created":1682713420279,"fileName":"New Document","contents":"","contentType":"yaml","_type":"api_spec"},{"_id":"uts_ec361ce226fc4f9387d08996b3ce15c8","parentId":"wrk_eb8a016621754748848e8569a6d5408b","modified":1682713420285,"created":1682713420285,"name":"Example Test Suite","_type":"unit_test_suite"},{"_id":"env_07e80f6969384673b252ed2a1507c2e0","parentId":"env_d882da9fec9abe78a0b12491a28b071e71efb65b","modified":1682736545285,"created":1682713484846,"name":"Dev","data":{"API_URL":"http://0.0.0.0:8080"},"dataPropertyOrder":{"&":["API_URL"]},"color":null,"isPrivate":false,"metaSortKey":1682713484846,"_type":"environment"}]} -------------------------------------------------------------------------------- /static/coverage.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | coverage 17 | coverage 18 | 62% 19 | 62% 20 | 21 | 22 | -------------------------------------------------------------------------------- /static/create-superuser-with-docker.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucassimon/flask-api-users/88da2933b260d698c8513299ba64957e08230762/static/create-superuser-with-docker.gif -------------------------------------------------------------------------------- /static/create-superuser.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucassimon/flask-api-users/88da2933b260d698c8513299ba64957e08230762/static/create-superuser.gif -------------------------------------------------------------------------------- /static/docker-create-user.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucassimon/flask-api-users/88da2933b260d698c8513299ba64957e08230762/static/docker-create-user.gif -------------------------------------------------------------------------------- /static/login-and-fetch-users.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucassimon/flask-api-users/88da2933b260d698c8513299ba64957e08230762/static/login-and-fetch-users.gif -------------------------------------------------------------------------------- /static/make-test.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucassimon/flask-api-users/88da2933b260d698c8513299ba64957e08230762/static/make-test.gif -------------------------------------------------------------------------------- /static/openapi3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucassimon/flask-api-users/88da2933b260d698c8513299ba64957e08230762/static/openapi3.png -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucassimon/flask-api-users/88da2933b260d698c8513299ba64957e08230762/tests/__init__.py -------------------------------------------------------------------------------- /tests/auth/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucassimon/flask-api-users/88da2933b260d698c8513299ba64957e08230762/tests/auth/__init__.py -------------------------------------------------------------------------------- /tests/auth/test_resources.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from apps.auth.exceptions import LoginSchemaValidationErrorException 4 | from apps.users.exceptions import UserMongoDoesNotExistException 5 | 6 | 7 | class TestAuthUser: 8 | 9 | @mock.patch("apps.auth.resources.AuthUsersCommand.run") 10 | def test_responses_data_invalid(self, AuthUsersCommandMock, client, mongo): 11 | AuthUsersCommandMock.side_effect = LoginSchemaValidationErrorException( 12 | "Some error occurred" 13 | ) 14 | 15 | resp = client.post( 16 | '/auth', 17 | json=dict( 18 | email='teste@teste.com', 19 | password='123456', 20 | ), 21 | content_type='application/json' 22 | ) 23 | 24 | assert resp.status_code == 422 25 | assert resp.json.get('message') == 'The input data is wrong' 26 | assert resp.json.get('errors') == 'Some error occurred' 27 | 28 | @mock.patch("apps.auth.resources.AuthUsersCommand.run") 29 | def test_responses_user_does_not_exist(self, AuthUsersCommandMock, client, mongo): 30 | AuthUsersCommandMock.side_effect = UserMongoDoesNotExistException( 31 | "Some error occurred" 32 | ) 33 | 34 | resp = client.post( 35 | '/auth', 36 | json=dict( 37 | email='teste@teste.com', 38 | password='123456', 39 | ), 40 | content_type='application/json' 41 | ) 42 | 43 | assert resp.status_code == 404 44 | assert resp.json.get('message') == 'Este(a) usuário não existe.' 45 | 46 | @mock.patch("apps.auth.resources.AuthUsersCommand.run") 47 | def test_responses_user_exception(self, AuthUsersCommandMock, client, mongo): 48 | AuthUsersCommandMock.side_effect = Exception( 49 | "Some error occurred" 50 | ) 51 | 52 | resp = client.post( 53 | '/auth', 54 | json=dict( 55 | email='teste@teste.com', 56 | password='123456', 57 | ), 58 | content_type='application/json' 59 | ) 60 | 61 | assert resp.status_code == 500 62 | assert resp.json.get('message') == 'Some error occurred' 63 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Python 4 | from os.path import dirname, isfile, join 5 | 6 | import pytest 7 | from dotenv import load_dotenv 8 | 9 | # a partir do arquivo atual adicione ao path o arquivo .env 10 | _ENV_FILE = join(dirname(__file__), '../.env') 11 | 12 | # existindo o arquivo faça a leitura do arquivo através da função load_dotenv 13 | if isfile(_ENV_FILE): 14 | load_dotenv(dotenv_path=_ENV_FILE) 15 | 16 | # Cria uma fixture que será utilizada no escopo sessão 17 | # ou seja a cada execução do comando pytest 18 | 19 | 20 | @pytest.fixture(scope='session') 21 | def client(): 22 | from apps.app import create_app 23 | # instancia nossa função factory criada anteriormente 24 | flask_app = create_app(testing=True) 25 | 26 | # O Flask fornece um caminho para testar a aplicação 27 | # utilizando o Werkzeug test Client 28 | # e manipulando o contexto (configurações) 29 | testing_client = flask_app.test_client() 30 | 31 | # Antes de executar os testes, é criado um contexto com as configurações 32 | # da aplicação 33 | ctx = flask_app.app_context() 34 | ctx.push() 35 | 36 | # retorna o client criado 37 | yield testing_client # this is where the testing happens! 38 | 39 | # remove o contexto ao terminar os testes 40 | ctx.pop() 41 | 42 | 43 | @pytest.fixture(scope='function') 44 | def mongo(request, client): 45 | 46 | from apps.extensions.db import db 47 | 48 | yield db 49 | 50 | def fin(db): 51 | print("\n[teardown] disconnect from db") 52 | 53 | db.connection.drop_database("api-users-test") 54 | db.connection.close() 55 | fin(db) 56 | 57 | 58 | @pytest.fixture(scope="session") 59 | def auth(client): 60 | from tests.factories.users import AdminFactory 61 | from flask_jwt_extended import create_access_token, create_refresh_token 62 | from apps.users.utils import generate_password 63 | 64 | admin = AdminFactory.create( 65 | id="5ce089d4fb5d1b3bd3ad96a2", 66 | full_name="Admin", 67 | email="supertest@mail.com", 68 | password=generate_password("123456"), 69 | ) 70 | access_token = create_access_token( 71 | identity=admin.email, expires_delta=False, fresh=True 72 | ) 73 | refresh_token = create_refresh_token(identity=admin.email, expires_delta=False) 74 | client.access_token = access_token 75 | client.refresh_token = refresh_token 76 | client.user_id = f"{admin.id}" 77 | return client 78 | -------------------------------------------------------------------------------- /tests/factories/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucassimon/flask-api-users/88da2933b260d698c8513299ba64957e08230762/tests/factories/__init__.py -------------------------------------------------------------------------------- /tests/factories/users.py: -------------------------------------------------------------------------------- 1 | import factory 2 | 3 | from apps.users.models import Admin, User 4 | 5 | 6 | class AdminFactory(factory.mongoengine.MongoEngineFactory): 7 | class Meta: 8 | model = Admin 9 | 10 | full_name = factory.Sequence(lambda n: "Admin %d" % n) 11 | email = factory.Sequence(lambda n: "admin-%d@gmail.com" % n) 12 | password = "123456" 13 | 14 | 15 | class UserFactory(factory.mongoengine.MongoEngineFactory): 16 | class Meta: 17 | model = User 18 | 19 | full_name = factory.Sequence(lambda n: "User %d" % n) 20 | email = factory.Sequence(lambda n: "admin-%d@gmail.com" % n) 21 | cpf_cnpj = factory.Sequence(lambda n: "cpf-%d" % n) 22 | date_of_birth = "2019-07-23" 23 | password = "123456" 24 | -------------------------------------------------------------------------------- /tests/home/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucassimon/flask-api-users/88da2933b260d698c8513299ba64957e08230762/tests/home/__init__.py -------------------------------------------------------------------------------- /tests/home/test_home.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Python 4 | import json 5 | 6 | # O `client` é a fixture que criamos dentro do arquivo conftest.py 7 | # ela é passada por parâmetro na função e pode ser usada dentro do escopo dela 8 | 9 | 10 | def test_index_response_200(client): 11 | # Realiza uma requisição HTTP do tipo get para o endpoint / 12 | response = client.get('/') 13 | 14 | # Verificamos a assertividade do código de resposta da requisição 15 | # http. Ela deve ser exatamente igual 200 retornando um True para 16 | # o teste 17 | assert response.status_code == 200 18 | 19 | 20 | def test_home_response_hello(client): 21 | """ 22 | **Given** Luiza está acessando a API, 23 | **When** ela informa a rota/endpoint `/`, 24 | **Then** a api deve responder um objeto com a chave `['hello']`, 25 | **And** seu conteúdo deve ser `world by apps` 26 | """ 27 | # Realiza uma requisição HTTP do tipo get para o endpoint / 28 | response = client.get('/') 29 | 30 | # Utilizamos a função loads do modulo json para retornar um dict 31 | # para a váriavel data. 32 | # Precisamos passar por parâmetro para essa função a resposta 33 | # retornada pelo servidor, através da váriavel response.data 34 | # e decodificar para utf-8 35 | data = json.loads(response.data.decode('utf-8')) 36 | 37 | # Fazemos o teste de asserção pela chave 'hello' 38 | assert data['hello'] == 'world by apps' 39 | -------------------------------------------------------------------------------- /tests/messages/test_messages.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from apps.extensions.messages import MSG_FIELD_REQUIRED, MSG_INVALID_DATA 4 | from apps.extensions.messages import MSG_DOES_NOT_EXIST, MSG_EXCEPTION 5 | from apps.extensions.messages import MSG_ALREADY_EXISTS, MSG_NO_DATA 6 | from apps.extensions.messages import MSG_PASSWORD_DIDNT_MATCH, MSG_RESOURCE_CREATED 7 | 8 | 9 | def test_msg_field_required(): 10 | assert MSG_FIELD_REQUIRED == 'Campo obrigatório.' 11 | 12 | 13 | def test_msg_invalid_data(): 14 | assert MSG_INVALID_DATA == 'Ocorreu um erro nos campos informados.' 15 | 16 | 17 | def test_msg_does_not_exist(): 18 | assert MSG_DOES_NOT_EXIST == 'Este(a) {} não existe.' 19 | 20 | 21 | def test_msg_exception(): 22 | msg = 'Ocorreu um erro no servidor. Contate o administrador.' 23 | assert MSG_EXCEPTION == msg 24 | 25 | 26 | def test_msg_already_exists(): 27 | assert MSG_ALREADY_EXISTS == 'Já existe um(a) {} com estes dados.' 28 | 29 | 30 | def test_msg_no_data(): 31 | assert MSG_NO_DATA == 'Nenhum dado foi postado.' 32 | 33 | 34 | def test_msg_password_wrong(): 35 | assert MSG_PASSWORD_DIDNT_MATCH == 'Password did not matches' 36 | 37 | 38 | def test_msg_resource_created(): 39 | assert MSG_RESOURCE_CREATED == '{} criado(a).' 40 | -------------------------------------------------------------------------------- /tests/responses/test_responses.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | 4 | from apps.extensions.responses import resp_data_invalid, resp_exception 5 | from apps.extensions.responses import resp_does_not_exist, resp_already_exists, resp_ok 6 | 7 | from apps.extensions.messages import MSG_INVALID_DATA, MSG_EXCEPTION, MSG_DOES_NOT_EXIST 8 | from apps.extensions.messages import MSG_ALREADY_EXISTS 9 | 10 | 11 | def test_resp_data_invalid_raises_error(client): 12 | with pytest.raises(ValueError): 13 | resp_data_invalid(None, {}) 14 | 15 | 16 | def test_resp_data_invalid_response_status_code_422(client): 17 | resp = resp_data_invalid('pytest', {}) 18 | assert resp.status_code == 422 19 | 20 | 21 | def test_resp_data_invalid_response(client): 22 | resp = resp_data_invalid('pytest', {}) 23 | message = resp.json.get('message') 24 | assert message == MSG_INVALID_DATA 25 | 26 | 27 | def test_resp_exception_raises_error(client): 28 | with pytest.raises(ValueError): 29 | resp_exception(None, {}) 30 | 31 | 32 | def test_resp_exception_response_status_code_500(client): 33 | resp = resp_exception('pytest', 'Exception raises') 34 | assert resp.status_code == 500 35 | 36 | 37 | def test_resp_exception_response(client): 38 | resp = resp_exception('pytest', {}) 39 | message = resp.json.get('message') 40 | assert message == MSG_EXCEPTION 41 | 42 | 43 | def test_resp_does_not_exist_raises_error(client): 44 | with pytest.raises(ValueError): 45 | resp_does_not_exist(None, None) 46 | 47 | 48 | def test_resp_does_not_exist_response_status_code_404(client): 49 | resp = resp_does_not_exist('pytest', 'Some description') 50 | assert resp.status_code == 404 51 | 52 | 53 | def test_resp_does_not_exist_response(client): 54 | description = 'Some description' 55 | resp = resp_does_not_exist('pytest', description) 56 | message = resp.json.get('message') 57 | assert message == MSG_DOES_NOT_EXIST.format(description) 58 | 59 | 60 | def test_resp_already_exists_raises_error(client): 61 | with pytest.raises(ValueError): 62 | resp_already_exists(None, None) 63 | 64 | 65 | def test_resp_already_exists_response_status_code_400(client): 66 | resp = resp_already_exists('pytest', 'Some description') 67 | assert resp.status_code == 409 68 | 69 | 70 | def test_resp_already_exists_response(client): 71 | description = 'Some description' 72 | resp = resp_already_exists('pytest', description) 73 | message = resp.json.get('message') 74 | assert message == MSG_ALREADY_EXISTS.format(description) 75 | 76 | 77 | def test_resp_ok_response_status_code_200(client): 78 | resource = 'pytest' 79 | message = 'pytest retornado com sucesso' 80 | data = {'foo': 'bar'} 81 | extras = {'ping': 'pong'} 82 | resp = resp_ok(resource, message, data, **extras) 83 | assert resp.status_code == 200 84 | 85 | 86 | def test_resp_ok_response_message(client): 87 | resource = 'pytest' 88 | message = 'pytest retornado com sucesso' 89 | data = {'foo': 'bar'} 90 | extras = {'ping': 'pong'} 91 | resp = resp_ok(resource, message, data, **extras) 92 | assert resp.json.get('message') == message 93 | 94 | 95 | def test_resp_ok_response_data(client): 96 | resource = 'pytest' 97 | message = 'pytest retornado com sucesso' 98 | data = {'foo': 'bar'} 99 | extras = {'ping': 'pong'} 100 | resp = resp_ok(resource, message, data, **extras) 101 | assert resp.json.get('data').get('foo') == 'bar' 102 | 103 | 104 | def test_resp_ok_response_extra(client): 105 | resource = 'pytest' 106 | message = 'pytest retornado com sucesso' 107 | data = {'foo': 'bar'} 108 | extras = {'ping': 'pong'} 109 | resp = resp_ok(resource, message, data, **extras) 110 | assert resp.json.get('ping') == 'pong' 111 | -------------------------------------------------------------------------------- /tests/users/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucassimon/flask-api-users/88da2933b260d698c8513299ba64957e08230762/tests/users/__init__.py -------------------------------------------------------------------------------- /tests/users/test_admin_user_by_cpf.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from apps.auth.exceptions import LoginSchemaValidationErrorException 4 | from apps.users.exceptions import UserMongoDoesNotExistException 5 | 6 | 7 | class TestAdminUserResourceByCpf: 8 | @mock.patch("apps.users.resources_admin.GetUserByCpfCnpjCommand.run") 9 | def test_responses_user_does_not_exist(self, GetUserByCpfCnpjCommandMock, client, auth, mongo): 10 | GetUserByCpfCnpjCommandMock.side_effect = UserMongoDoesNotExistException( 11 | "Some error occurred" 12 | ) 13 | 14 | headers = {"Authorization": f"Bearer {client.access_token}"} 15 | url = '/admin/users/cpf/{}'.format('some-cpf') 16 | resp = client.get(url, content_type='application/json', headers=headers) 17 | 18 | assert resp.status_code == 404 19 | assert resp.json.get('message') == 'Este(a) usuário não existe.' 20 | 21 | @mock.patch("apps.users.resources_admin.GetUserByCpfCnpjCommand.run") 22 | def test_responses_user_exception(self, GetUserByCpfCnpjCommandMock, client, auth, mongo): 23 | GetUserByCpfCnpjCommandMock.side_effect = Exception( 24 | "Some error occurred" 25 | ) 26 | 27 | headers = {"Authorization": f"Bearer {client.access_token}"} 28 | url = '/admin/users/cpf/{}'.format('some-cpf') 29 | resp = client.get(url, content_type='application/json', headers=headers) 30 | 31 | assert resp.status_code == 500 32 | assert resp.json.get('message') == 'Some error occurred' 33 | -------------------------------------------------------------------------------- /tests/users/test_admin_user_page_list.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from os import getenv 3 | import pytest 4 | from json import dumps, loads 5 | from unittest import mock 6 | 7 | from pymongo import MongoClient 8 | from mongoengine.errors import FieldDoesNotExist 9 | 10 | from apps.extensions.messages import MSG_RESOURCE_FETCHED_PAGINATED, MSG_EXCEPTION 11 | from apps.extensions.responses import resp_data_invalid 12 | from apps.users.models import User 13 | from tests.factories.users import UserFactory 14 | 15 | # https://stackabuse.com/guide-to-flask-mongoengine-in-python/ 16 | 17 | class TestAdminUserPageList: 18 | 19 | def setup_method(self): 20 | self.data = {} 21 | self.CREATE_ENDPOINT = '/users' 22 | self.AUTH_ENDPOINT = '/auth' 23 | self.ENDPOINT = '/admin/users/page/{}' 24 | 25 | def teardown_method(self): 26 | User.objects.delete() 27 | 28 | def test_response_params_should_be_ten(self, client, auth, mongo): 29 | 30 | url = '{}'.format(self.ENDPOINT.format(1)) 31 | headers = {"Authorization": f"Bearer {client.access_token}"} 32 | 33 | resp = client.get(url, content_type='application/json', headers=headers) 34 | 35 | assert resp.status_code == 200 36 | assert resp.json.get('params').get('page_size') == 10 37 | 38 | 39 | def test_page_size_is_zero_should_returns_ten(self, client, auth, mongo): 40 | url = '{}?page_size={}'.format(self.ENDPOINT.format(1), -1) 41 | headers = {"Authorization": f"Bearer {client.access_token}"} 42 | 43 | resp = client.get(url, content_type='application/json', headers=headers) 44 | 45 | assert resp.status_code == 200 46 | assert resp.json.get('params').get('page_size') == 10 47 | 48 | def test_set_page_size_tweenty_should_returns_tweenty(self, client, auth, mongo): 49 | url = '{}?page_size={}'.format(self.ENDPOINT.format(1), 20) 50 | headers = {"Authorization": f"Bearer {client.access_token}"} 51 | 52 | resp = client.get(url, content_type='application/json', headers=headers) 53 | 54 | assert resp.status_code == 200 55 | assert resp.json.get('params').get('page_size') == 20 56 | 57 | @mock.patch("apps.users.repositories.User.objects") 58 | def test_responses_exception_field_not_exist(self, UserMock, client, auth, mongo): 59 | UserMock.side_effect = Exception(MSG_EXCEPTION) 60 | 61 | headers = {"Authorization": f"Bearer {client.access_token}"} 62 | resp = client.get(self.ENDPOINT.format(1), content_type='application/json', headers=headers) 63 | 64 | assert resp.status_code == 500 65 | assert resp.json.get('message') == MSG_EXCEPTION 66 | 67 | 68 | def test_responses_ok(self, client, auth, mongo): 69 | headers = {"Authorization": f"Bearer {client.access_token}"} 70 | resp = client.get(self.ENDPOINT.format(1), content_type='application/json', headers=headers) 71 | 72 | assert resp.status_code == 200 73 | assert resp.json.get('message') == MSG_RESOURCE_FETCHED_PAGINATED.format('usuários') 74 | 75 | def test_response_has_items_in_data(self, client, auth, mongo): 76 | user = UserFactory.create(full_name='teste', email='teste@teste.com', cpf_cnpj='some-cpf', date_of_birth='2010-11-12') 77 | headers = {"Authorization": f"Bearer {client.access_token}"} 78 | resp = client.get(self.ENDPOINT.format(1), content_type='application/json', headers=headers) 79 | data = resp.json.get('data') 80 | 81 | expected = [ 82 | { 83 | "active": user.active, 84 | "cpf_cnpj": user.cpf_cnpj, 85 | "email": user.email, 86 | "full_name": user.full_name, 87 | "date_of_birth": user.date_of_birth, 88 | "id": data[0].get("id") 89 | } 90 | ] 91 | 92 | assert data == expected 93 | 94 | def test_response_items_paginated(self, client, auth, mongo): 95 | users = UserFactory.create_batch(4) 96 | headers = {"Authorization": f"Bearer {client.access_token}"} 97 | url = '{}?page_size={}'.format(self.ENDPOINT.format(1), 1) 98 | resp = client.get(url, content_type='application/json', headers=headers) 99 | data = resp.json.get('data') 100 | 101 | expected = [ 102 | { 103 | "active": users[0].active, 104 | "cpf_cnpj": users[0].cpf_cnpj, 105 | "email": users[0].email, 106 | "full_name": users[0].full_name, 107 | "date_of_birth": users[0].date_of_birth, 108 | "id": data[0].get("id") 109 | } 110 | ] 111 | 112 | assert data == expected 113 | 114 | 115 | url = '{}?page_size={}'.format(self.ENDPOINT.format(2), 1) 116 | resp = client.get(url, content_type='application/json', headers=headers) 117 | data = resp.json.get('data') 118 | 119 | expected = [ 120 | { 121 | "active": users[1].active, 122 | "cpf_cnpj": users[1].cpf_cnpj, 123 | "email": users[1].email, 124 | "full_name": users[1].full_name, 125 | "date_of_birth": users[1].date_of_birth, 126 | "id": data[0].get("id") 127 | } 128 | ] 129 | 130 | assert data == expected 131 | -------------------------------------------------------------------------------- /tests/users/test_check_password_in_signup.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from apps.users.utils import check_password_in_signup 4 | 5 | 6 | @pytest.mark.parametrize("password,confirm_password,expected", [(None, 1, False), (1, None, False), (1,2,False), (2,2,True)]) 7 | def test_password(password, confirm_password, expected): 8 | assert check_password_in_signup(password, confirm_password) is expected 9 | -------------------------------------------------------------------------------- /tests/users/test_cpf.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from apps.users.utils import Cpf 4 | 5 | 6 | def test_cpf_when_length_is_less_than_11(): 7 | is_valid = Cpf('123456').validate() 8 | assert is_valid is False 9 | 10 | @pytest.mark.parametrize("rest,expected", [(1, 0), (8, 3)]) 11 | def test_cpf_rule(rest,expected): 12 | value = Cpf('123456').cpf_rule(rest) 13 | assert value == expected 14 | 15 | @pytest.mark.parametrize("cpf,expected", [(92144721068, 6), ('92144721068', 6), ('921.447.210-68', 6)]) 16 | def test_cpf_first_digit(cpf, expected): 17 | first_digit = Cpf(cpf).calculate_first_digit() 18 | assert first_digit == expected 19 | 20 | @pytest.mark.parametrize("cpf,expected", [(92144721068, 8), ('92144721068', 8), ('921.447.210-68', 8)]) 21 | def test_cpf_second_digit(cpf, expected): 22 | second_digit = Cpf(cpf).calculate_second_digit() 23 | assert second_digit == expected 24 | 25 | @pytest.mark.parametrize("cpf,expected", [(92144721068, True), ('92144721068', True), ('921.447.210-68', True)]) 26 | def test_cpf_is_valid(cpf, expected): 27 | is_valid = Cpf(cpf).validate() 28 | assert is_valid is expected 29 | 30 | def test_cpf_normalize(): 31 | cpf = Cpf('921.447.210-68') 32 | assert cpf.cpf == '92144721068' 33 | 34 | def test_cpf_as_numbers_only(): 35 | cpf = Cpf('92144721068') 36 | assert cpf.cpf == '92144721068' 37 | -------------------------------------------------------------------------------- /tests/users/test_generate_password.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from unittest import mock 3 | 4 | from apps.users.utils import generate_password 5 | 6 | 7 | def test_should_return_password_encrypted(): 8 | with mock.patch("apps.users.utils.hashpw") as haspw_mock: 9 | haspw_mock.return_value = b"$2b$12$encyptPassword" 10 | received = generate_password("132456") 11 | 12 | assert received == b"$2b$12$encyptPassword" 13 | -------------------------------------------------------------------------------- /tests/users/test_models.py: -------------------------------------------------------------------------------- 1 | # Third 2 | 3 | from mongoengine import ( 4 | BooleanField, 5 | StringField, 6 | ) 7 | 8 | # Apps 9 | 10 | from apps.users.models import User, Address 11 | 12 | class TestAddress: 13 | 14 | def setup_method(self): 15 | self.address = { 16 | 'zip_code': '31000-000', 'address': 'teste', 17 | 'number': '12a', 'complement': 'teste', 'neighborhood': 'teste', 18 | 'city': 'teste', 'state': 'teste', 'country': 'teste', 19 | } 20 | # Crio uma instancia do modelo User 21 | self.model = Address(**self.address) 22 | 23 | def test_zip_code_field_exists(self): 24 | """ 25 | Verifico se o campo zip_code existe 26 | """ 27 | assert 'zip_code' in self.model._fields 28 | 29 | def test_zip_code_field_is_required(self): 30 | """ 31 | Verifico se o campo zip_code é requirido 32 | """ 33 | assert self.model._fields['zip_code'].required is False 34 | 35 | def test_zip_code_field_is_unique(self): 36 | """ 37 | Verifico se o campo zip_code é unico 38 | """ 39 | assert self.model._fields['zip_code'].unique is False 40 | 41 | def test_zip_code_field_is_str(self): 42 | """ 43 | Verifico se o campo zip_code é do tipo string 44 | """ 45 | assert isinstance(self.model._fields['zip_code'], StringField) 46 | 47 | def test_zip_code_field_is_default_true(self): 48 | assert self.model._fields['zip_code'].default == '' 49 | 50 | def test_address_field_exists(self): 51 | """ 52 | Verifico se o campo address existe 53 | """ 54 | assert 'address' in self.model._fields 55 | 56 | def test_address_field_is_required(self): 57 | """ 58 | Verifico se o campo address é requirido 59 | """ 60 | assert self.model._fields['address'].required is False 61 | 62 | def test_address_field_is_unique(self): 63 | """ 64 | Verifico se o campo address é unico 65 | """ 66 | assert self.model._fields['address'].unique is False 67 | 68 | def test_address_field_is_str(self): 69 | """ 70 | Verifico se o campo address é do tipo string 71 | """ 72 | assert isinstance(self.model._fields['address'], StringField) 73 | 74 | def test_address_field_is_default_true(self): 75 | assert self.model._fields['address'].default == '' 76 | 77 | def test_number_field_exists(self): 78 | """ 79 | Verifico se o campo number existe 80 | """ 81 | assert 'number' in self.model._fields 82 | 83 | def test_number_field_is_required(self): 84 | """ 85 | Verifico se o campo number é requirido 86 | """ 87 | assert self.model._fields['number'].required is False 88 | 89 | def test_number_field_is_unique(self): 90 | """ 91 | Verifico se o campo number é unico 92 | """ 93 | assert self.model._fields['number'].unique is False 94 | 95 | def test_number_field_is_str(self): 96 | """ 97 | Verifico se o campo number é do tipo string 98 | """ 99 | assert isinstance(self.model._fields['number'], StringField) 100 | 101 | def test_number_field_is_default_true(self): 102 | assert self.model._fields['number'].default == '' 103 | 104 | def test_complement_field_exists(self): 105 | """ 106 | Verifico se o campo complement existe 107 | """ 108 | assert 'complement' in self.model._fields 109 | 110 | def test_complement_field_is_required(self): 111 | """ 112 | Verifico se o campo complement é requirido 113 | """ 114 | assert self.model._fields['complement'].required is False 115 | 116 | def test_complement_field_is_unique(self): 117 | """ 118 | Verifico se o campo complement é unico 119 | """ 120 | assert self.model._fields['complement'].unique is False 121 | 122 | def test_complement_field_is_str(self): 123 | """ 124 | Verifico se o campo complement é do tipo string 125 | """ 126 | assert isinstance(self.model._fields['complement'], StringField) 127 | 128 | def test_complement_field_is_default_true(self): 129 | assert self.model._fields['complement'].default == '' 130 | 131 | def test_neighborhood_field_exists(self): 132 | """ 133 | Verifico se o campo neighborhood existe 134 | """ 135 | assert 'neighborhood' in self.model._fields 136 | 137 | def test_neighborhood_field_is_required(self): 138 | """ 139 | Verifico se o campo neighborhood é requirido 140 | """ 141 | assert self.model._fields['neighborhood'].required is False 142 | 143 | def test_neighborhood_field_is_unique(self): 144 | """ 145 | Verifico se o campo neighborhood é unico 146 | """ 147 | assert self.model._fields['neighborhood'].unique is False 148 | 149 | def test_neighborhood_field_is_str(self): 150 | """ 151 | Verifico se o campo neighborhood é do tipo string 152 | """ 153 | assert isinstance(self.model._fields['neighborhood'], StringField) 154 | 155 | def test_neighborhood_field_is_default_true(self): 156 | assert self.model._fields['neighborhood'].default == '' 157 | 158 | def test_city_field_exists(self): 159 | """ 160 | Verifico se o campo city existe 161 | """ 162 | assert 'city' in self.model._fields 163 | 164 | def test_city_field_is_required(self): 165 | """ 166 | Verifico se o campo city é requirido 167 | """ 168 | assert self.model._fields['city'].required is False 169 | 170 | def test_city_field_is_unique(self): 171 | """ 172 | Verifico se o campo city é unico 173 | """ 174 | assert self.model._fields['city'].unique is False 175 | 176 | def test_city_field_is_str(self): 177 | """ 178 | Verifico se o campo city é do tipo string 179 | """ 180 | assert isinstance(self.model._fields['city'], StringField) 181 | 182 | def test_city_field_is_default_true(self): 183 | assert self.model._fields['city'].default == '' 184 | 185 | def test_city_id_field_exists(self): 186 | """ 187 | Verifico se o campo city_id existe 188 | """ 189 | assert 'city_id' in self.model._fields 190 | 191 | def test_city_id_field_is_required(self): 192 | """ 193 | Verifico se o campo city_id é requirido 194 | """ 195 | assert self.model._fields['city_id'].required is False 196 | 197 | def test_city_id_field_is_unique(self): 198 | """ 199 | Verifico se o campo city_id é unico 200 | """ 201 | assert self.model._fields['city_id'].unique is False 202 | 203 | def test_city_id_field_is_str(self): 204 | """ 205 | Verifico se o campo city_id é do tipo string 206 | """ 207 | assert isinstance(self.model._fields['city_id'], StringField) 208 | 209 | def test_city_id_field_is_default_true(self): 210 | assert self.model._fields['city_id'].default == '' 211 | 212 | def test_state_field_exists(self): 213 | """ 214 | Verifico se o campo state existe 215 | """ 216 | assert 'state' in self.model._fields 217 | 218 | def test_state_field_is_required(self): 219 | """ 220 | Verifico se o campo state é requirido 221 | """ 222 | assert self.model._fields['state'].required is False 223 | 224 | def test_state_field_is_unique(self): 225 | """ 226 | Verifico se o campo state é unico 227 | """ 228 | assert self.model._fields['state'].unique is False 229 | 230 | def test_state_field_is_str(self): 231 | """ 232 | Verifico se o campo state é do tipo string 233 | """ 234 | assert isinstance(self.model._fields['state'], StringField) 235 | 236 | def test_state_field_is_default_true(self): 237 | assert self.model._fields['state'].default == '' 238 | 239 | def test_country_field_exists(self): 240 | """ 241 | Verifico se o campo country existe 242 | """ 243 | assert 'country' in self.model._fields 244 | 245 | def test_country_field_is_required(self): 246 | """ 247 | Verifico se o campo country é requirido 248 | """ 249 | assert self.model._fields['country'].required is False 250 | 251 | def test_country_field_is_unique(self): 252 | """ 253 | Verifico se o campo country é unico 254 | """ 255 | assert self.model._fields['country'].unique is False 256 | 257 | def test_country_field_is_str(self): 258 | """ 259 | Verifico se o campo country é do tipo string 260 | """ 261 | assert isinstance(self.model._fields['country'], StringField) 262 | 263 | def test_country_field_is_default_true(self): 264 | assert self.model._fields['country'].default == 'BRA' 265 | 266 | 267 | class TestUser: 268 | 269 | def setup_method(self): 270 | 271 | self.data = { 272 | 'email': 'teste1@teste.com', 'password': 'teste123', 273 | 'active': True, 'full_name': 'Teste', 274 | 'cpf_cnpj': '11111111111' 275 | } 276 | 277 | # Crio uma instancia do modelo User 278 | self.model = User(**self.data) 279 | 280 | def test_email_field_exists(self): 281 | """ 282 | Verifico se o campo email existe 283 | """ 284 | assert 'email' in self.model._fields 285 | 286 | def test_email_field_is_required(self): 287 | """ 288 | Verifico se o campo email é requirido 289 | """ 290 | assert self.model._fields['email'].required is True 291 | 292 | def test_email_field_is_unique(self): 293 | """ 294 | Verifico se o campo email é unico 295 | """ 296 | assert self.model._fields['email'].unique is True 297 | 298 | def test_email_field_is_str(self): 299 | """ 300 | Verifico se o campo email é do tipo string 301 | """ 302 | assert isinstance(self.model._fields['email'], StringField) 303 | 304 | def test_active_field_exists(self): 305 | assert 'active' in self.model._fields 306 | 307 | def test_active_field_is_default_true(self): 308 | assert self.model._fields['active'].default is True 309 | 310 | def test_is_active_is_false(self): 311 | assert self.model.is_active() is True 312 | 313 | def test_active_field_is_bool(self): 314 | """ 315 | Verifico se o campo active é booleano 316 | """ 317 | assert isinstance(self.model._fields['active'], BooleanField) 318 | 319 | def test_full_name_field_exists(self): 320 | """ 321 | Verifico se o campo full_name existe 322 | """ 323 | assert 'full_name' in self.model._fields 324 | 325 | def test_full_name_field_is_str(self): 326 | assert isinstance(self.model._fields['full_name'], StringField) 327 | 328 | def test_full_name_field_is_required(self): 329 | """ 330 | Verifico se o campo full_name é requirido 331 | """ 332 | assert self.model._fields['full_name'].required is True 333 | 334 | def test_cpf_cnpj_field_exists(self): 335 | """ 336 | Verifico se o campo cpf_cnpj existe 337 | """ 338 | assert 'cpf_cnpj' in self.model._fields 339 | 340 | def test_cpf_cnpj_field_is_required(self): 341 | """ 342 | Verifico se o campo cpf_cnpj é requirido 343 | """ 344 | assert self.model._fields['cpf_cnpj'].required is True 345 | 346 | def test_cpf_cnpj_field_is_unique(self): 347 | """ 348 | Verifico se o campo cpf_cnpj é unico 349 | """ 350 | assert self.model._fields['cpf_cnpj'].unique is True 351 | 352 | def test_cpf_cnpj_field_is_str(self): 353 | """ 354 | Verifico se o campo cpf_cnpj é do tipo string 355 | """ 356 | assert isinstance(self.model._fields['cpf_cnpj'], StringField) 357 | 358 | def test_all_fields_in_model(self): 359 | """ 360 | Verifico se todos os campos estão de fato no meu modelo 361 | """ 362 | fields = [ 363 | 'active', 'address', 'cpf_cnpj', 'created', 'email', 364 | 'full_name', 'id', 'password', 'date_of_birth' 365 | ] 366 | 367 | model_keys = [i for i in self.model._fields.keys()] 368 | 369 | fields.sort() 370 | model_keys.sort() 371 | 372 | assert fields == model_keys 373 | -------------------------------------------------------------------------------- /tests/users/test_repositories.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from unittest import mock 3 | 4 | from mongoengine.errors import NotUniqueError, ValidationError, FieldDoesNotExist, DoesNotExist, MultipleObjectsReturned 5 | 6 | from apps.users.repositories import AdminMongoRepository, UserMongoRepository 7 | from apps.users.models import User, Admin 8 | from apps.users.exceptions import UserMongoNotUniqueException, UserMongoValidationErrorException, UserMongoFieldsDoesNotExistException, UserMongoDoesNotExistException 9 | from tests.factories.users import AdminFactory, UserFactory 10 | 11 | 12 | class TestUserRepoInsert: 13 | def setup_method(self): 14 | UserFactory.reset_sequence() 15 | 16 | def teardown_method(self): 17 | User.objects.delete() 18 | 19 | @mock.patch("apps.users.repositories.User.save", side_effect=NotUniqueError) 20 | def test_should_raises_not_unique_exception(self, client, mongo): 21 | with pytest.raises(UserMongoNotUniqueException) as exc: 22 | UserMongoRepository().insert(data={ 23 | 'full_name': 'some-full-name', 24 | 'email': 'some-email', 25 | 'cpf_cnpj': 'some-cpf', 26 | 'date_of_birth': 'some-date', 27 | 'password': 'some-password' 28 | }) 29 | 30 | @mock.patch("apps.users.repositories.User.save", side_effect=ValidationError) 31 | def test_should_raises_validation_exception(self, client, mongo): 32 | with pytest.raises(UserMongoValidationErrorException) as exc: 33 | UserMongoRepository().insert(data={ 34 | 'full_name': 'some-full-name', 35 | 'email': 'some-email', 36 | 'cpf_cnpj': 'some-cpf', 37 | 'date_of_birth': 'some-date', 38 | 'password': 'some-password' 39 | }) 40 | 41 | @mock.patch("apps.users.repositories.User.save", side_effect=Exception) 42 | def test_should_raises_exception(self, client, mongo): 43 | with pytest.raises(Exception) as exc: 44 | UserMongoRepository().insert(data={ 45 | 'full_name': 'some-full-name', 46 | 'email': 'some-email', 47 | 'cpf_cnpj': 'some-cpf', 48 | 'date_of_birth': 'some-date', 49 | 'password': 'some-password' 50 | }) 51 | 52 | @mock.patch("apps.users.repositories.User.save") 53 | def test_should_return_user_instance(self, SaveMock, client, mongo): 54 | model = UserMongoRepository().insert(data={ 55 | 'full_name': 'some-full-name', 56 | 'email': 'some-email', 57 | 'cpf_cnpj': 'some-cpf', 58 | 'date_of_birth': 'some-date', 59 | 'password': 'some-password' 60 | }) 61 | 62 | assert model.email == 'some-email' 63 | assert isinstance(model, User) is True 64 | 65 | 66 | class TestAdminRepoInsert: 67 | def setup_method(self): 68 | AdminFactory.reset_sequence() 69 | 70 | def teardown_method(self): 71 | Admin.objects.delete() 72 | 73 | @mock.patch("apps.users.repositories.Admin.save", side_effect=NotUniqueError) 74 | def test_should_raises_not_unique_exception(self, client, mongo): 75 | with pytest.raises(UserMongoNotUniqueException) as exc: 76 | AdminMongoRepository().insert(data={ 77 | 'full_name': 'some-full-name', 78 | 'email': 'some-email', 79 | 'password': 'some-password' 80 | }) 81 | 82 | @mock.patch("apps.users.repositories.Admin.save", side_effect=ValidationError) 83 | def test_should_raises_validation_exception(self, client, mongo): 84 | with pytest.raises(UserMongoValidationErrorException) as exc: 85 | AdminMongoRepository().insert(data={ 86 | 'full_name': 'some-full-name', 87 | 'email': 'some-email', 88 | 'password': 'some-password' 89 | }) 90 | 91 | @mock.patch("apps.users.repositories.Admin.save", side_effect=Exception) 92 | def test_should_raises_exception(self, client, mongo): 93 | with pytest.raises(Exception) as exc: 94 | AdminMongoRepository().insert(data={ 95 | 'full_name': 'some-full-name', 96 | 'email': 'some-email', 97 | 'password': 'some-password' 98 | }) 99 | 100 | @mock.patch("apps.users.repositories.Admin.save") 101 | def test_should_return_user_instance(self, SaveMock, client, mongo): 102 | model = AdminMongoRepository().insert(data={ 103 | 'full_name': 'Admin', 104 | 'email': 'some-email@admin.com', 105 | 'password': 'some-password' 106 | }) 107 | 108 | assert model.email == 'some-email@admin.com' 109 | assert isinstance(model, Admin) is True 110 | 111 | 112 | class TestAdminRepoGetUserByCpf: 113 | def setup_method(self): 114 | AdminFactory.reset_sequence() 115 | UserFactory.reset_sequence() 116 | 117 | def teardown_method(self): 118 | Admin.objects.delete() 119 | User.objects.delete() 120 | 121 | # @mock.patch("apps.users.repositories.User.objects") 122 | def test_should_return_an_user(self, client, mongo): 123 | user = UserFactory.create() 124 | # GetMock.get.return_value = user 125 | model = AdminMongoRepository().get_user_by_cpf_cnpj(user.cpf_cnpj) 126 | assert model.cpf_cnpj == user.cpf_cnpj 127 | 128 | @mock.patch("apps.users.repositories.User.objects.get", side_effect=DoesNotExist) 129 | def test_should_raises_does_not_exist(self, GetMock, client, mongo): 130 | with pytest.raises(UserMongoDoesNotExistException) as exc: 131 | AdminMongoRepository().get_user_by_cpf_cnpj('some-cpf') 132 | 133 | @mock.patch("apps.users.repositories.User.objects") 134 | def test_should_raises_field_does_not_exist(self, ObjectsMock, client, mongo): 135 | ObjectsMock.get.side_effect = FieldDoesNotExist() 136 | with pytest.raises(UserMongoFieldsDoesNotExistException) as exc: 137 | AdminMongoRepository().get_user_by_cpf_cnpj('some-cpf') 138 | 139 | @mock.patch("apps.users.repositories.User.objects") 140 | def test_should_raises_exception(self, GetMock, client, mongo): 141 | GetMock.get.side_effect = Exception() 142 | with pytest.raises(Exception) as exc: 143 | AdminMongoRepository().get_user_by_cpf_cnpj('some-cpf') 144 | -------------------------------------------------------------------------------- /tests/users/test_resources.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import pytest 3 | import mongomock 4 | from unittest import mock 5 | from mongoengine.errors import NotUniqueError, ValidationError 6 | 7 | from json import dumps, loads 8 | 9 | from apps.extensions.messages import MSG_NO_DATA, MSG_INVALID_DATA, MSG_PASSWORD_DIDNT_MATCH 10 | from apps.extensions.messages import MSG_FIELD_REQUIRED, MSG_RESOURCE_CREATED, MSG_ALREADY_EXISTS 11 | from apps.extensions.responses import resp_data_invalid 12 | from apps.users.models import User 13 | 14 | class TestSignUp: 15 | 16 | def setup_method(self): 17 | self.data = {} 18 | self.ENDPOINT = '/users' 19 | 20 | def teardown_method(self): 21 | User.objects.delete() 22 | 23 | def test_response_422_when_empty_payload(self, client): 24 | resp = client.post( 25 | self.ENDPOINT, 26 | json={}, 27 | content_type='application/json' 28 | ) 29 | 30 | assert resp.status_code == 422 31 | assert resp.json.get('message') == 'The input data is wrong' 32 | 33 | def test_response_422_when_data_is_not_valid(self, client): 34 | resp = client.post( 35 | self.ENDPOINT, 36 | json=dict(foo='bar'), 37 | content_type='application/json' 38 | ) 39 | assert resp.status_code == 422 40 | 41 | def test_message_when_password_is_not_valid(self, client): 42 | resp = client.post( 43 | self.ENDPOINT, 44 | json=dict( 45 | full_name='bar', 46 | email='t@t.com', 47 | password='123', 48 | confirm_password='456', 49 | cpf_cnpj='11653754605', 50 | date_of_birth='2010-11-12' 51 | ), 52 | content_type='application/json' 53 | ) 54 | response = resp.json 55 | assert response.get('errors').get('password')[0] == MSG_PASSWORD_DIDNT_MATCH 56 | 57 | def test_message_required_when_fullname_not_in_payload(self, client): 58 | resp = client.post( 59 | self.ENDPOINT, 60 | json=dict(email='teste@teste.com', password='123456', confirm_password='123456'), 61 | content_type='application/json' 62 | ) 63 | print(resp.json) 64 | assert resp.json.get('errors').get('full_name')[0] == MSG_FIELD_REQUIRED 65 | 66 | def test_message_required_when_email_not_in_payload(self, client): 67 | resp = client.post( 68 | self.ENDPOINT, 69 | json=dict(full_name='teste', password='123456', confirm_password='123456'), 70 | content_type='application/json' 71 | ) 72 | assert resp.json.get('errors').get('email')[0] == MSG_FIELD_REQUIRED 73 | 74 | @mock.patch("apps.users.repositories.User") 75 | def test_responses_already_exists(self, UserMock, client, mongo): 76 | UserMock.return_value.save.side_effect = NotUniqueError( 77 | "Some error occurred" 78 | ) 79 | 80 | resp = client.post( 81 | self.ENDPOINT, 82 | json=dict( 83 | full_name='teste', 84 | email='teste@teste.com', 85 | password='123456', 86 | confirm_password='123456', 87 | cpf_cnpj='11653754605', 88 | date_of_birth='2010-11-12' 89 | ), 90 | content_type='application/json' 91 | ) 92 | 93 | assert resp.status_code == 409 94 | assert resp.json.get('message') == MSG_ALREADY_EXISTS.format('usuário') 95 | 96 | def test_responses_ok(self, client, mongo): 97 | resp = client.post( 98 | self.ENDPOINT, 99 | json=dict( 100 | full_name='teste', 101 | email='teste@teste.com', 102 | password='123456', 103 | confirm_password='123456', 104 | cpf_cnpj='11653754605', 105 | date_of_birth='2010-11-12' 106 | ), 107 | content_type='application/json' 108 | ) 109 | 110 | assert resp.status_code == 200 111 | assert resp.json.get('message') == MSG_RESOURCE_CREATED.format('Usuário') 112 | 113 | def test_responses_ok_with_cpf_masked(self, client, mongo): 114 | resp = client.post( 115 | self.ENDPOINT, 116 | json=dict( 117 | full_name='teste', 118 | email='teste@teste.com', 119 | password='123456', 120 | confirm_password='123456', 121 | cpf_cnpj='116.537.546-05', 122 | date_of_birth='2010-11-12' 123 | ), 124 | content_type='application/json' 125 | ) 126 | 127 | assert resp.status_code == 200 128 | assert resp.json.get('message') == MSG_RESOURCE_CREATED.format('Usuário') 129 | --------------------------------------------------------------------------------