From 20116d2a178ded47e4d6c4329e3d69c4a801bb0f Mon Sep 17 00:00:00 2001 From: Stephanie Castro Date: Thu, 2 May 2024 17:11:27 -0300 Subject: [PATCH 01/32] =?UTF-8?q?altera=C3=A7=C3=B5es=20no=20readme=20e=20?= =?UTF-8?q?docker-compose?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/README.md b/README.md index adc30bd..4450600 100644 --- a/README.md +++ b/README.md @@ -1 +1,32 @@ # stars-api + +############## Documentação ############## + +# ambiente virtual + python3 -m venv env + +# ativação do ambiente + source env/bin/activate + +# install nas dependencias + pip install -r requirements.txt + +# Atulização nas dependencias + pip freeze > requirements.txt + +# Comando para criar a imagem docker no projeto + docker build -t backoffice -f .docker/Dockerfile . + + # Configurações de vulnerabilidade da imagem sugerida pelo docker + docker scout cves local://backoffice_fastapi:latest + docker scout recommendations local://backoffice_fastapi:latest + +# Comando para checar se a imagem foi criada + docker images + +# Executar o container e verificar se esta em execução + docker run -d -p 80:80 backoffice + docker ps + +# Parar o servidor + docker stop 65d05c5e44806478fd97914e8ecdb61a3a1b530686b20640da7c68e5717ec7a3 From 8909f83cd1475ecf17f82a11c2b7452402b0ea4c Mon Sep 17 00:00:00 2001 From: Stephanie Castro Date: Thu, 2 May 2024 17:54:34 -0300 Subject: [PATCH 02/32] update --- README.md | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 4450600..13364d6 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,34 @@ # stars-api -############## Documentação ############## +# Documentação -# ambiente virtual +############## ambiente virtual ############## python3 -m venv env -# ativação do ambiente +############## ativação do ambiente ############## source env/bin/activate -# install nas dependencias +############## install nas dependencias ############## pip install -r requirements.txt -# Atulização nas dependencias +############## Atulização nas dependencias ############## pip freeze > requirements.txt -# Comando para criar a imagem docker no projeto - docker build -t backoffice -f .docker/Dockerfile . +############## Comando para criar a imagem docker no projeto ############## + docker build -t backoffice_soujunior -f .docker/Dockerfile . - # Configurações de vulnerabilidade da imagem sugerida pelo docker - docker scout cves local://backoffice_fastapi:latest - docker scout recommendations local://backoffice_fastapi:latest +############## Configurações de vulnerabilidade da imagem sugerida pelo docker ############## + docker scout cves local://backoffice_soujunior:latest + docker scout recommendations local://backoffice_soujunior:latest -# Comando para checar se a imagem foi criada +############## Comando para checar se a imagem foi criada ############## docker images -# Executar o container e verificar se esta em execução - docker run -d -p 80:80 backoffice +############## Executar o container e verificar se esta em execução ############## + docker run -d -p 80:80 backoffice_soujunior docker ps -# Parar o servidor +docker-compose up + +############## Parar o servidor ############## docker stop 65d05c5e44806478fd97914e8ecdb61a3a1b530686b20640da7c68e5717ec7a3 From e9873853eb9f29cbef6f6f5e9bc524d0138f7679 Mon Sep 17 00:00:00 2001 From: Stephanie Castro Date: Thu, 2 May 2024 18:48:42 -0300 Subject: [PATCH 03/32] update readme --- README.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 13364d6..538ccff 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,10 @@ # Documentação -############## ambiente virtual ############## +>> ambiente virtual << python3 -m venv env -############## ativação do ambiente ############## +ativação do ambiente ############## source env/bin/activate ############## install nas dependencias ############## @@ -15,11 +15,11 @@ pip freeze > requirements.txt ############## Comando para criar a imagem docker no projeto ############## - docker build -t backoffice_soujunior -f .docker/Dockerfile . + docker build -t backoffice -f .docker/Dockerfile . ############## Configurações de vulnerabilidade da imagem sugerida pelo docker ############## - docker scout cves local://backoffice_soujunior:latest - docker scout recommendations local://backoffice_soujunior:latest + docker scout cves local://backoffice:latest + docker scout recommendations local://backoffice:latest ############## Comando para checar se a imagem foi criada ############## docker images @@ -27,8 +27,15 @@ ############## Executar o container e verificar se esta em execução ############## docker run -d -p 80:80 backoffice_soujunior docker ps - + +############## Comandos para criar os containers ############## docker-compose up +docker-compose ps + +############## Comandos docker ############## +docker-compose stop +docker-compose start +docker-compose restart ############## Parar o servidor ############## docker stop 65d05c5e44806478fd97914e8ecdb61a3a1b530686b20640da7c68e5717ec7a3 From de3775f08335783a7208f8d5ee2dc76683c8227f Mon Sep 17 00:00:00 2001 From: Stephanie Castro Date: Thu, 2 May 2024 18:49:57 -0300 Subject: [PATCH 04/32] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 538ccff..6981a0c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # Documentação ->> ambiente virtual << +### ambiente virtual python3 -m venv env ativação do ambiente ############## From 4633962cb2e595cec73f671145eebdc530d23ced Mon Sep 17 00:00:00 2001 From: Stephanie Castro Date: Thu, 2 May 2024 18:52:40 -0300 Subject: [PATCH 05/32] update readme --- README.md | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 6981a0c..1b01235 100644 --- a/README.md +++ b/README.md @@ -5,37 +5,49 @@ ### ambiente virtual python3 -m venv env -ativação do ambiente ############## + +### ativação do ambiente source env/bin/activate -############## install nas dependencias ############## + +### install nas dependencias pip install -r requirements.txt -############## Atulização nas dependencias ############## + +#### Atulização nas dependencias pip freeze > requirements.txt -############## Comando para criar a imagem docker no projeto ############## + +#### Comando para criar a imagem docker no projeto docker build -t backoffice -f .docker/Dockerfile . -############## Configurações de vulnerabilidade da imagem sugerida pelo docker ############## + +#### Configurações de vulnerabilidade da imagem sugerida pelo docker docker scout cves local://backoffice:latest docker scout recommendations local://backoffice:latest - -############## Comando para checar se a imagem foi criada ############## + + +### Comando para checar se a imagem foi criada docker images -############## Executar o container e verificar se esta em execução ############## + +### Executar o container e verificar se esta em execução docker run -d -p 80:80 backoffice_soujunior docker ps -############## Comandos para criar os containers ############## + +### Comandos para criar os containers docker-compose up docker-compose ps -############## Comandos docker ############## + + +### Comandos docker docker-compose stop docker-compose start docker-compose restart -############## Parar o servidor ############## + + +### Parar o servidor docker stop 65d05c5e44806478fd97914e8ecdb61a3a1b530686b20640da7c68e5717ec7a3 From 614674c279508b63064473413f52f0f32ba07c2d Mon Sep 17 00:00:00 2001 From: Stephanie Castro Date: Thu, 2 May 2024 18:58:20 -0300 Subject: [PATCH 06/32] update readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1b01235..1425657 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # stars-api # Documentação +## Este projeto é um aplicativo Python com FastApi com todo o ambiente de execução encapsulado em Docker. ### ambiente virtual python3 -m venv env From 8bd9bc16cc56918ba66f7fc1ce2a5159b76aee86 Mon Sep 17 00:00:00 2001 From: Stephanie Castro Date: Thu, 2 May 2024 19:34:06 -0300 Subject: [PATCH 07/32] update readme and docker compose --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1425657..af698ed 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ ### Comandos para criar os containers -docker-compose up +docker-compose up docker-compose ps @@ -49,6 +49,9 @@ docker-compose start docker-compose restart +### Porta e swagger +http://localhost:8000/docs + ### Parar o servidor docker stop 65d05c5e44806478fd97914e8ecdb61a3a1b530686b20640da7c68e5717ec7a3 From 13c69b4a08e23b50df82a02ed9e1f39ed0bed9cc Mon Sep 17 00:00:00 2001 From: Stephanie Castro Date: Thu, 2 May 2024 20:09:35 -0300 Subject: [PATCH 08/32] update --- app/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/main.py b/app/main.py index d4d4a18..6d3492a 100644 --- a/app/main.py +++ b/app/main.py @@ -43,7 +43,7 @@ def get_db(): finally: db.close() - +print('teste') @app.post("/users/", response_model=schemas.User) def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)): db_user = crud.get_user_by_email(db, email=user.email) From d0f8fb5e85d63ce3df21ddd8316f66cb47ab8b29 Mon Sep 17 00:00:00 2001 From: Stephanie Castro Date: Thu, 2 May 2024 20:12:19 -0300 Subject: [PATCH 09/32] update --- .docker/docker-compose.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.docker/docker-compose.yml b/.docker/docker-compose.yml index 051f046..354ec02 100644 --- a/.docker/docker-compose.yml +++ b/.docker/docker-compose.yml @@ -7,11 +7,17 @@ services: environment: PORT: 8000 DEBUG: 1 + DB_DRIVER: mysql+mysqlconnector + DB_USERNAME: mysql + DB_PASSWORD: mysql + DB_HOST: mysql_database + DB_PORT: 3306 + DB_DATABASE: db ports: - '8000:80' volumes: - ../app:/code/app - restart: "no" + restart: on-failure command: uvicorn app.main:app --host 0.0.0.0 --port 80 --reload depends_on: mysql_database: @@ -20,7 +26,7 @@ services: mysql_database: image: mysql:8.3 restart: unless-stopped - command: --default-authentication-plugin=mysql_native_password + command: --default-authentication-plugin=caching_sha2_password environment: MYSQL_DATABASE: 'db' MYSQL_USER: 'mysql' From 58b857932b8ed134c1159dab628d66a5825fdb38 Mon Sep 17 00:00:00 2001 From: Stephanie Castro Date: Fri, 3 May 2024 13:32:34 -0300 Subject: [PATCH 10/32] update --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index af698ed..254d57a 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ #### Configurações de vulnerabilidade da imagem sugerida pelo docker docker scout cves local://backoffice:latest + docker scout recommendations local://backoffice:latest @@ -39,13 +40,16 @@ ### Comandos para criar os containers docker-compose up + docker-compose ps ### Comandos docker docker-compose stop + docker-compose start + docker-compose restart From 0e39a765f557e399b8109400edcbc989e3cf6277 Mon Sep 17 00:00:00 2001 From: JessiAraujo Date: Fri, 3 May 2024 20:13:08 -0300 Subject: [PATCH 11/32] update --- .docker/docker-compose.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.docker/docker-compose.yml b/.docker/docker-compose.yml index 051f046..d4316bd 100644 --- a/.docker/docker-compose.yml +++ b/.docker/docker-compose.yml @@ -7,11 +7,17 @@ services: environment: PORT: 8000 DEBUG: 1 + DB_DRIVER: mysql+mysqlconnector + DB_USERNAME: mysql + DB_PASSWORD: mysql + DB_HOST: mysql_database + DB_PORT: 3306 + DB_DATABASE: db ports: - '8000:80' volumes: - ../app:/code/app - restart: "no" + restart: on-failure command: uvicorn app.main:app --host 0.0.0.0 --port 80 --reload depends_on: mysql_database: @@ -20,7 +26,7 @@ services: mysql_database: image: mysql:8.3 restart: unless-stopped - command: --default-authentication-plugin=mysql_native_password + command: --default-authentication-plugin=caching_sha2_password environment: MYSQL_DATABASE: 'db' MYSQL_USER: 'mysql' @@ -37,4 +43,4 @@ services: volumes: mysql_database: - name: mysql_database + name: mysql_database \ No newline at end of file From edac752131f18479836a520bd2c5d6caeca82609 Mon Sep 17 00:00:00 2001 From: Stephanie Castro Date: Wed, 8 May 2024 21:27:37 -0300 Subject: [PATCH 12/32] update requirements --- requirements.txt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index cdc8a96..d436257 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,15 @@ -alembic==1.13.1 +annotated-types==0.6.0 asgiref==3.8.1 certifi==2024.2.2 click==8.1.7 fastapi==0.68.2 greenlet==3.0.3 h11==0.14.0 -pydantic==1.10.14 +install==1.3.5 +mysql-connector-python==8.3.0 +pydantic==2.7.1 +pydantic-settings==2.2.1 +pydantic_core==2.18.2 python-dateutil==2.9.0.post0 python-dotenv==1.0.1 setuptools==69.1.1 @@ -16,5 +20,4 @@ starlette==0.14.2 typing_extensions==4.10.0 urllib3==2.2.1 uvicorn==0.15.0 -wheel==0.43.0 -mysql-connector-python==8.3.0 +wheel==0.43.0 \ No newline at end of file From c53a4475985b99736b1178a2b154a0a159d9576f Mon Sep 17 00:00:00 2001 From: Jessica Araujo <109446432+JessiAraujo@users.noreply.github.com> Date: Mon, 13 May 2024 17:50:56 -0300 Subject: [PATCH 13/32] Feat: Create python-app.yml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adicionando GitHub Actions e verificações de formatação com a lib Black, teste com pytest e coverage e estilo. --- .github/workflows/python-app.yml | 54 ++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .github/workflows/python-app.yml diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml new file mode 100644 index 0000000..48651a8 --- /dev/null +++ b/.github/workflows/python-app.yml @@ -0,0 +1,54 @@ +# This workflow will install Python dependencies, run tests and lint with a single version of Python +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python + + +name: Python application + +on: + push: + branches: [ "develop" ] + pull_request: + branches: [ "develop" ] + +permissions: + contents: read + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.12 + uses: actions/setup-python@v3 + with: + python-version: "3.12" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 pytest + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + + - name: Format with Black + run: black . + + + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + + - name: Verify with flake8 + run: flake8 . + + - name: Test with pytest + run: | + pytest + + - name: Test with pytest and coverage + run: | + pytest --cov=. --cov-report=term From b37e3cd7a0d870bb75dbaabcec9c73f1c5d5dc85 Mon Sep 17 00:00:00 2001 From: Jessica Araujo <109446432+JessiAraujo@users.noreply.github.com> Date: Mon, 13 May 2024 19:06:57 -0300 Subject: [PATCH 14/32] Doc: Update README.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adcionando orientações para instalação das verificações de Qualidade de código. --- README.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/README.md b/README.md index adc30bd..cc18dbf 100644 --- a/README.md +++ b/README.md @@ -1 +1,50 @@ # stars-api +### Adicionando Verificações de Qualidade de Código + +1. **Adicionar Verificação de Formatação com Black e Padrão PEP8:** + + O PEP 8 define regras para a indentação, uso de espaços em branco, nomes de variáveis, entre outros aspectos do estilo de código. Ao adicionar a verificação do Black no GitHub Actions, estaremos garantindo que o código esteja formatado de acordo com as recomendações do PEP 8. + + - Para instalar o Black, utilize o comando: + ```bash + pip install black + ``` + - Para executar o Black, utilize: + ```bash + black + ``` + +2. **Adicionar Verificação do Pytest com 100% de Cobertura:** + + O Pytest é uma biblioteca de testes unitários que permite escrever testes simples e escaláveis em Python. Ele fornece suporte para detecção automática de testes, relatórios detalhados e plugins personalizados. + + **Cobertura:** + A biblioteca Coverage é usada para medir a cobertura de testes do código-fonte Python. Ela ajuda a identificar áreas do código que não estão sendo testadas, fornecendo relatórios sobre a porcentagem de código coberto pelos testes. + + - Para instalar o Pytest e o pytest-cov, utilize o comando: + ```bash + pip install pytest pytest-cov + ``` + - Para executar os testes com o Pytest e calcular a cobertura de código, utilize o pytest-cov diretamente no comando Pytest: + ```bash + pytest --cov=. + ``` + - Para cobrir todo o código ou + ```bash + pytest --cov=app + ``` + - para cobrir apenas o diretório app. + +3. **Adicionar Verificação do Flake8:** + + O Flake8 é uma ferramenta de verificação de código que combina as funcionalidades de outras ferramentas populares. Ele verifica o estilo do código, identifica problemas potenciais e fornece sugestões de melhoria. + + - Para instalar o Flake8, utilize o comando: + ```bash + pip install flake8 + ``` + - Para verificar seu código com o Flake8, utilize o seguinte comando: + ```bash + flake8 + ``` + From 605fbb3e8e961b4855d2021a4b04fb420b87a355 Mon Sep 17 00:00:00 2001 From: Stephanie Castro Date: Tue, 14 May 2024 20:39:20 -0300 Subject: [PATCH 15/32] =?UTF-8?q?esbo=C3=A7o=20mapeamento=20de=20variaveis?= =?UTF-8?q?=20de=20ambiente?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/crud.py | 4 ++-- app/database.py | 9 ++++++++- app/schemas.py | 4 ++-- app/settings.py | 13 +++++++++++++ 4 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 app/settings.py diff --git a/app/crud.py b/app/crud.py index 23f5caa..d1aa86b 100644 --- a/app/crud.py +++ b/app/crud.py @@ -57,7 +57,7 @@ def get_volunteers_by_email(db: Session, skip: int = 0, limit: int = 100, email: models.Volunteer.id, models.Volunteer.name, func.replace( - models.Volunteer.email, + models.Volunteer.email, func.substr(models.Volunteer.email, 1, func.instr(models.Volunteer.email, '@') - 1), '***').label("masked_email"), models.Volunteer.is_active, @@ -85,5 +85,5 @@ def get_jobtitles(db: Session, skip: int = 0, limit: int = 100): return db.query(models.JobTitle).offset(skip).limit(limit).all() def get_volunteer_by_email(db: Session, email: str): - return db.query(models.Volunteer).filter(models.Volunteer.email == email).first() + return db.query(models.Volunteer).filter(models.Volunteer.email == email).first() diff --git a/app/database.py b/app/database.py index f226669..8562121 100644 --- a/app/database.py +++ b/app/database.py @@ -4,10 +4,17 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from dotenv import load_dotenv +from .settings import Settings load_dotenv() +settings = Settings() -SQLALCHEMY_DATABASE_URL = f"{os.getenv('DB_DRIVER')}://{os.getenv('DB_USERNAME')}:{os.getenv('DB_PASSWORD')}@{os.getenv('DB_HOST')}:{os.getenv('DB_PORT')}/{os.getenv('DB_DATABASE')}" +#SQLALCHEMY_DATABASE_URL = f"{os.getenv('DB_DRIVER')}://{os.getenv('DB_USERNAME')}:{os.getenv('DB_PASSWORD')}@{os.getenv('DB_HOST')}:{os.getenv('DB_PORT')}/{os.getenv('DB_DATABASE')}" + +SQLALCHEMY_DATABASE_URL = ( + f"{settings.DB_DRIVER}://{settings.DB_USERNAME}:{settings.DB_PASSWORD}" + f"@{settings.DB_HOST}:{settings.DB_PORT}/{settings.DB_DATABASE}" +) engine = create_engine(SQLALCHEMY_DATABASE_URL) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) diff --git a/app/schemas.py b/app/schemas.py index 1513954..4aac939 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -54,11 +54,11 @@ class VolunteerCreate(VolunteerBase): email: str masked_email: Optional[str] = None is_active: Optional[bool] = True - jobtitle_id: int + jobtitle_id: int class Volunteer(VolunteerBase): id: int - jobtitle_id: int + jobtitle_id: int masked_email: Optional[str] = None diff --git a/app/settings.py b/app/settings.py new file mode 100644 index 0000000..e1066ec --- /dev/null +++ b/app/settings.py @@ -0,0 +1,13 @@ +from pydantic import BaseSettings + +class Settings(BaseSettings): + DB_DRIVER: str = 'mysql+mysqlconnector' + DB_USERNAME: str = 'mysql' + DB_PASSWORD: str = 'mysql' + DB_HOST: str = 'mysql_database' + DB_PORT: int = 3306 + DB_DATABASE: str = 'db' + + class Config: + env_file = '.env' + env_file_encoding = 'utf-8' \ No newline at end of file From 009700b54d75fe3aa17b249554a0dce70bd0f0cf Mon Sep 17 00:00:00 2001 From: Stephanie Castro Date: Wed, 15 May 2024 21:50:43 -0300 Subject: [PATCH 16/32] =?UTF-8?q?vers=C3=A3o=20final=20pydantic-settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/crud.py | 2 +- app/database.py | 8 +------- app/requirements.txt | 47 ++++++++++++++++++++++++++++++++++++++++++++ app/settings.py | 21 ++++++++++---------- 4 files changed, 60 insertions(+), 18 deletions(-) create mode 100644 app/requirements.txt diff --git a/app/crud.py b/app/crud.py index d1aa86b..54765e3 100644 --- a/app/crud.py +++ b/app/crud.py @@ -42,7 +42,7 @@ def get_volunteers(db: Session, skip: int = 0, limit: int = 100): models.Volunteer.id, models.Volunteer.name, func.replace( - models.Volunteer.email, + models.Volunteer.email, func.substr(models.Volunteer.email, 1, func.instr(models.Volunteer.email, '@') - 1), '***').label("masked_email"), models.Volunteer.is_active, diff --git a/app/database.py b/app/database.py index 8562121..3eb9c01 100644 --- a/app/database.py +++ b/app/database.py @@ -3,13 +3,7 @@ from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker -from dotenv import load_dotenv -from .settings import Settings - -load_dotenv() -settings = Settings() - -#SQLALCHEMY_DATABASE_URL = f"{os.getenv('DB_DRIVER')}://{os.getenv('DB_USERNAME')}:{os.getenv('DB_PASSWORD')}@{os.getenv('DB_HOST')}:{os.getenv('DB_PORT')}/{os.getenv('DB_DATABASE')}" +from .settings import settings SQLALCHEMY_DATABASE_URL = ( f"{settings.DB_DRIVER}://{settings.DB_USERNAME}:{settings.DB_PASSWORD}" diff --git a/app/requirements.txt b/app/requirements.txt new file mode 100644 index 0000000..e7b5769 --- /dev/null +++ b/app/requirements.txt @@ -0,0 +1,47 @@ +annotated-types==0.6.0 +anyio==4.3.0 +asgiref==3.8.1 +certifi==2024.2.2 +click==8.1.7 +dnspython==2.6.1 +email_validator==2.1.1 +fastapi==0.111.0 +fastapi-cli==0.0.3 +greenlet==3.0.3 +h11==0.14.0 +httpcore==1.0.5 +httptools==0.6.1 +httpx==0.27.0 +idna==3.7 +install==1.3.5 +Jinja2==3.1.4 +markdown-it-py==3.0.0 +MarkupSafe==2.1.5 +mdurl==0.1.2 +mysql-connector-python==8.4.0 +orjson==3.10.3 +pydantic==2.7.1 +pydantic-settings==2.2.1 +pydantic_core==2.18.2 +Pygments==2.18.0 +python-dateutil==2.9.0.post0 +python-dotenv==1.0.1 +python-multipart==0.0.9 +PyYAML==6.0.1 +rich==13.7.1 +setuptools==69.5.1 +shellingham==1.5.4 +sib-api-v3-sdk==7.6.0 +six==1.16.0 +sniffio==1.3.1 +SQLAlchemy==2.0.30 +starlette==0.37.2 +typer==0.12.3 +typing_extensions==4.11.0 +ujson==5.10.0 +urllib3==2.2.1 +uvicorn==0.29.0 +uvloop==0.19.0 +watchfiles==0.21.0 +websockets==12.0 +wheel==0.43.0 diff --git a/app/settings.py b/app/settings.py index e1066ec..5677f1c 100644 --- a/app/settings.py +++ b/app/settings.py @@ -1,13 +1,14 @@ -from pydantic import BaseSettings +from pydantic_settings import BaseSettings class Settings(BaseSettings): - DB_DRIVER: str = 'mysql+mysqlconnector' - DB_USERNAME: str = 'mysql' - DB_PASSWORD: str = 'mysql' - DB_HOST: str = 'mysql_database' - DB_PORT: int = 3306 - DB_DATABASE: str = 'db' + DB_DRIVER: str + DB_USERNAME: str + DB_PASSWORD: str + DB_HOST: str + DB_PORT: int + DB_DATABASE: str - class Config: - env_file = '.env' - env_file_encoding = 'utf-8' \ No newline at end of file + class config: + env_prexix = 'DB_' + +settings = Settings() \ No newline at end of file From 4b6cb032e4da671e615f61e6ddbb054cb5172f21 Mon Sep 17 00:00:00 2001 From: JessiAraujo Date: Fri, 17 May 2024 16:35:36 -0300 Subject: [PATCH 17/32] Atualiza workflow para falhar se a cobertura estiver abaixo de 100% --- .github/workflows/python-app.yml | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 48651a8..fae60a4 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -24,8 +24,7 @@ jobs: uses: actions/setup-python@v3 with: python-version: "3.12" - - - name: Install dependencies + - name: Install dependencies run: | python -m pip install --upgrade pip pip install flake8 pytest @@ -34,7 +33,6 @@ jobs: - name: Format with Black run: black . - - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names @@ -42,13 +40,6 @@ jobs: # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Verify with flake8 - run: flake8 . - - - name: Test with pytest - run: | - pytest - - name: Test with pytest and coverage run: | - pytest --cov=. --cov-report=term + pytest --cov=app --cov-report=term --cov-fail-under=100 \ No newline at end of file From 3d5a09165b69b12bb1f821068c2b85fb3ccbdd6f Mon Sep 17 00:00:00 2001 From: JessiAraujo Date: Fri, 24 May 2024 20:40:16 -0300 Subject: [PATCH 18/32] =?UTF-8?q?Atualiza=C3=A7=C3=A3o=20do=20.gitignore?= =?UTF-8?q?=20para=20ignorar=20arquivos=20desnecess=C3=A1rios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index ae98c39..cadd53b 100644 --- a/.gitignore +++ b/.gitignore @@ -175,3 +175,6 @@ pyrightconfig.json # End of https://www.toptal.com/developers/gitignore/api/python +app/some_module.py +pytest.ini +tests/ From c2e39a775966445c3839157e28588ae47d8f3fbe Mon Sep 17 00:00:00 2001 From: JessiAraujo Date: Wed, 29 May 2024 22:40:33 -0300 Subject: [PATCH 19/32] =?UTF-8?q?Feat:=20Incluindo=20pasta=20tests=20e=20a?= =?UTF-8?q?rquivo=20pytest.ini=20para=20rodar=20testes=20unit=C3=A1rios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 --- pytest.ini | 4 ++++ 2 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 pytest.ini diff --git a/.gitignore b/.gitignore index cadd53b..ae98c39 100644 --- a/.gitignore +++ b/.gitignore @@ -175,6 +175,3 @@ pyrightconfig.json # End of https://www.toptal.com/developers/gitignore/api/python -app/some_module.py -pytest.ini -tests/ diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..3cf59f2 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +addopts = --cov=app --cov-report=term --cov-fail-under=100 +testpaths = tests +pythonpath = . \ No newline at end of file From 49faf2cb4d9f78e411fcfc7d1e4d1210487bdc54 Mon Sep 17 00:00:00 2001 From: JessiAraujo Date: Wed, 29 May 2024 23:23:47 -0300 Subject: [PATCH 20/32] =?UTF-8?q?Feat:=20Incluindo=20arquivo=20.gitkeep=20?= =?UTF-8?q?para=20enviar=20a=20pasta=20tests=20para=20o=20reposit=C3=B3rio?= =?UTF-8?q?=20remoto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/.gitkeep diff --git a/tests/.gitkeep b/tests/.gitkeep new file mode 100644 index 0000000..e69de29 From 5dd971258c0fddcd37040130ffc96c8c34fee718 Mon Sep 17 00:00:00 2001 From: Stephanie Castro Date: Tue, 4 Jun 2024 19:26:52 -0300 Subject: [PATCH 21/32] pydantic-settings --- Dockerfile | 11 +++++++++++ README.md | 6 ++++++ app/crud.py | 7 +++---- app/database.py | 5 ++++- app/main.py | 4 ++-- app/requirements.txt | 47 -------------------------------------------- app/settings.py | 16 +++++++-------- docker-compose.yml | 44 +++++++++++++++++++++++++++++++++++++++++ requirements.txt | 46 +++++++++++++++++++++---------------------- 9 files changed, 100 insertions(+), 86 deletions(-) create mode 100644 Dockerfile delete mode 100644 app/requirements.txt create mode 100644 docker-compose.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..736ae11 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.12 + +WORKDIR /code + +COPY ./requirements.txt /code/requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt + +COPY ./app /code/app + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "80"] diff --git a/README.md b/README.md index 254d57a..ae41b1f 100644 --- a/README.md +++ b/README.md @@ -59,3 +59,9 @@ http://localhost:8000/docs ### Parar o servidor docker stop 65d05c5e44806478fd97914e8ecdb61a3a1b530686b20640da7c68e5717ec7a3 + +## Subir containers +docker compose up + +### Parar containers +docker compose down \ No newline at end of file diff --git a/app/crud.py b/app/crud.py index 54765e3..ffd2306 100644 --- a/app/crud.py +++ b/app/crud.py @@ -42,7 +42,7 @@ def get_volunteers(db: Session, skip: int = 0, limit: int = 100): models.Volunteer.id, models.Volunteer.name, func.replace( - models.Volunteer.email, + models.Volunteer.email, func.substr(models.Volunteer.email, 1, func.instr(models.Volunteer.email, '@') - 1), '***').label("masked_email"), models.Volunteer.is_active, @@ -57,7 +57,7 @@ def get_volunteers_by_email(db: Session, skip: int = 0, limit: int = 100, email: models.Volunteer.id, models.Volunteer.name, func.replace( - models.Volunteer.email, + models.Volunteer.email, func.substr(models.Volunteer.email, 1, func.instr(models.Volunteer.email, '@') - 1), '***').label("masked_email"), models.Volunteer.is_active, @@ -85,5 +85,4 @@ def get_jobtitles(db: Session, skip: int = 0, limit: int = 100): return db.query(models.JobTitle).offset(skip).limit(limit).all() def get_volunteer_by_email(db: Session, email: str): - return db.query(models.Volunteer).filter(models.Volunteer.email == email).first() - + return db.query(models.Volunteer).filter(models.Volunteer.email == email).first() \ No newline at end of file diff --git a/app/database.py b/app/database.py index 3eb9c01..67707be 100644 --- a/app/database.py +++ b/app/database.py @@ -3,7 +3,10 @@ from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker -from .settings import settings +from .settings import Settings + +settings = Settings() + SQLALCHEMY_DATABASE_URL = ( f"{settings.DB_DRIVER}://{settings.DB_USERNAME}:{settings.DB_PASSWORD}" diff --git a/app/main.py b/app/main.py index 2edf22b..b9f50eb 100644 --- a/app/main.py +++ b/app/main.py @@ -13,7 +13,7 @@ load_dotenv() from . import crud, models, schemas -from .database import SessionLocal, engine +from app.database import SessionLocal, engine models.Base.metadata.create_all(bind=engine) @@ -166,4 +166,4 @@ def send_email(email, name): except ApiException as e: print("Exception when calling AccountApi->get_account: %s\n" % e) - return + return \ No newline at end of file diff --git a/app/requirements.txt b/app/requirements.txt deleted file mode 100644 index e7b5769..0000000 --- a/app/requirements.txt +++ /dev/null @@ -1,47 +0,0 @@ -annotated-types==0.6.0 -anyio==4.3.0 -asgiref==3.8.1 -certifi==2024.2.2 -click==8.1.7 -dnspython==2.6.1 -email_validator==2.1.1 -fastapi==0.111.0 -fastapi-cli==0.0.3 -greenlet==3.0.3 -h11==0.14.0 -httpcore==1.0.5 -httptools==0.6.1 -httpx==0.27.0 -idna==3.7 -install==1.3.5 -Jinja2==3.1.4 -markdown-it-py==3.0.0 -MarkupSafe==2.1.5 -mdurl==0.1.2 -mysql-connector-python==8.4.0 -orjson==3.10.3 -pydantic==2.7.1 -pydantic-settings==2.2.1 -pydantic_core==2.18.2 -Pygments==2.18.0 -python-dateutil==2.9.0.post0 -python-dotenv==1.0.1 -python-multipart==0.0.9 -PyYAML==6.0.1 -rich==13.7.1 -setuptools==69.5.1 -shellingham==1.5.4 -sib-api-v3-sdk==7.6.0 -six==1.16.0 -sniffio==1.3.1 -SQLAlchemy==2.0.30 -starlette==0.37.2 -typer==0.12.3 -typing_extensions==4.11.0 -ujson==5.10.0 -urllib3==2.2.1 -uvicorn==0.29.0 -uvloop==0.19.0 -watchfiles==0.21.0 -websockets==12.0 -wheel==0.43.0 diff --git a/app/settings.py b/app/settings.py index 5677f1c..59b239d 100644 --- a/app/settings.py +++ b/app/settings.py @@ -1,14 +1,12 @@ from pydantic_settings import BaseSettings class Settings(BaseSettings): - DB_DRIVER: str - DB_USERNAME: str - DB_PASSWORD: str - DB_HOST: str - DB_PORT: int - DB_DATABASE: str + DB_DRIVER: str = 'mysql+mysqlconnector' + DB_USERNAME: str = 'mysql' + DB_PASSWORD: str = 'mysql' + DB_HOST: str = 'mysql_database' + DB_PORT: int = 3306 + DB_DATABASE: str = 'db' class config: - env_prexix = 'DB_' - -settings = Settings() \ No newline at end of file + env_prefix = 'DB_' \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..87b1c0a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,44 @@ +services: + stars-api: + build: . + container_name: stars-api + environment: + PORT: 8000 + DEBUG: 1 + DB_DRIVER: mysql+mysqlconnector + DB_USERNAME: mysql + DB_PASSWORD: mysql + DB_HOST: mysql_database + DB_PORT: 3306 + DB_DATABASE: db + ports: + - '8000:80' + volumes: + - ./app:/code/app + restart: on-failure + command: uvicorn app.main:app --host 0.0.0.0 --port 80 --reload + depends_on: + mysql_database: + condition: service_healthy + + mysql_database: + image: mysql:8.3 + restart: unless-stopped + command: --default-authentication-plugin=caching_sha2_password + environment: + MYSQL_DATABASE: 'db' + MYSQL_USER: 'mysql' + MYSQL_PASSWORD: 'mysql' + MYSQL_ROOT_PASSWORD: 'mysql' + ports: + - '3306:3306' + volumes: + - mysql_database:/var/lib/mysql + healthcheck: + test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"] + timeout: 20s + retries: 10 + +volumes: + mysql_database: + name: mysql_database diff --git a/requirements.txt b/requirements.txt index d436257..7f8aa80 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,23 +1,23 @@ -annotated-types==0.6.0 -asgiref==3.8.1 -certifi==2024.2.2 -click==8.1.7 -fastapi==0.68.2 -greenlet==3.0.3 -h11==0.14.0 -install==1.3.5 -mysql-connector-python==8.3.0 -pydantic==2.7.1 -pydantic-settings==2.2.1 -pydantic_core==2.18.2 -python-dateutil==2.9.0.post0 -python-dotenv==1.0.1 -setuptools==69.1.1 -sib-api-v3-sdk==7.6.0 -six==1.16.0 -SQLAlchemy==2.0.29 -starlette==0.14.2 -typing_extensions==4.10.0 -urllib3==2.2.1 -uvicorn==0.15.0 -wheel==0.43.0 \ No newline at end of file +annotated-types>=0.6.0 +asgiref>=3.8.1 +certifi>=2024.2.2 +click>=8.1.7 +fastapi>=0.68.2 +greenlet>=3.0.3 +h11>=0.14.0 +install>=1.3.5 +mysql-connector-python>=8.3.0 +pydantic>=2.7.1 +pydantic-settings>=2.2.1 +pydantic_core>=2.18.2 +python-dateutil>=2.9.0.post0 +python-dotenv>=1.0.1 +setuptools>=69.1.1 +sib-api-v3-sdk>=7.6.0 +six>=1.16.0 +SQLAlchemy>=2.0.29 +starlette>=0.14.2 +typing_extensions>=4.10.0 +urllib3>=2.2.1 +uvicorn>=0.15.0 +wheel>=0.43.0 \ No newline at end of file From 186a40cf64ccbf757031bc609966425c75807dd1 Mon Sep 17 00:00:00 2001 From: Stephanie Castro Date: Wed, 5 Jun 2024 22:15:13 -0300 Subject: [PATCH 22/32] =?UTF-8?q?ajustes=20de=20vers=C3=A3o=20pydantic-set?= =?UTF-8?q?tings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/database.py | 6 +++--- app/main.py | 1 - app/settings.py | 25 +++++++++++++++---------- requirements.txt | 45 ++++++++++++++++++++++----------------------- 4 files changed, 40 insertions(+), 37 deletions(-) diff --git a/app/database.py b/app/database.py index 67707be..1fb7bc7 100644 --- a/app/database.py +++ b/app/database.py @@ -3,10 +3,10 @@ from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker -from .settings import Settings - -settings = Settings() +from .settings import settings +from dotenv import load_dotenv +load_dotenv() SQLALCHEMY_DATABASE_URL = ( f"{settings.DB_DRIVER}://{settings.DB_USERNAME}:{settings.DB_PASSWORD}" diff --git a/app/main.py b/app/main.py index b9f50eb..881f9b2 100644 --- a/app/main.py +++ b/app/main.py @@ -42,7 +42,6 @@ def get_db(): finally: db.close() -print('teste') @app.post("/users/", response_model=schemas.User) def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)): db_user = crud.get_user_by_email(db, email=user.email) diff --git a/app/settings.py b/app/settings.py index 59b239d..431a60a 100644 --- a/app/settings.py +++ b/app/settings.py @@ -1,12 +1,17 @@ -from pydantic_settings import BaseSettings +from pydantic_settings import BaseSettings, SettingsConfigDict +from dotenv import load_dotenv +import os -class Settings(BaseSettings): - DB_DRIVER: str = 'mysql+mysqlconnector' - DB_USERNAME: str = 'mysql' - DB_PASSWORD: str = 'mysql' - DB_HOST: str = 'mysql_database' - DB_PORT: int = 3306 - DB_DATABASE: str = 'db' +load_dotenv() +class __Settings(BaseSettings): + DB_DRIVER: str + DB_USERNAME: str + DB_PASSWORD: str + DB_HOST: str + DB_PORT: int + DB_DATABASE: str - class config: - env_prefix = 'DB_' \ No newline at end of file +def get_settings(): + return __Settings() + +settings = get_settings() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 7f8aa80..72dc956 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,23 +1,22 @@ -annotated-types>=0.6.0 -asgiref>=3.8.1 -certifi>=2024.2.2 -click>=8.1.7 -fastapi>=0.68.2 -greenlet>=3.0.3 -h11>=0.14.0 -install>=1.3.5 -mysql-connector-python>=8.3.0 -pydantic>=2.7.1 -pydantic-settings>=2.2.1 -pydantic_core>=2.18.2 -python-dateutil>=2.9.0.post0 -python-dotenv>=1.0.1 -setuptools>=69.1.1 -sib-api-v3-sdk>=7.6.0 -six>=1.16.0 -SQLAlchemy>=2.0.29 -starlette>=0.14.2 -typing_extensions>=4.10.0 -urllib3>=2.2.1 -uvicorn>=0.15.0 -wheel>=0.43.0 \ No newline at end of file +alembic==1.13.1 +asgiref==3.8.1 +certifi==2024.2.2 +click==8.1.7 +fastapi==0.68.2 +greenlet==3.0.3 +h11==0.14.0 +pydantic==1.10.14 +python-dateutil==2.9.0.post0 +python-dotenv==1.0.1 +setuptools==69.1.1 +sib-api-v3-sdk==7.6.0 +six==1.16.0 +SQLAlchemy==2.0.29 +starlette==0.14.2 +typing_extensions==4.10.0 +urllib3==2.2.1 +uvicorn==0.15.0 +wheel==0.43.0 +mysql-connector-python==8.3.0 +pydantic-settings==2.2.1 +load-dotenv==0.1.0 \ No newline at end of file From fada3412b2f98c072cd99ec6faaf18691c3354d1 Mon Sep 17 00:00:00 2001 From: Stephanie Castro Date: Thu, 6 Jun 2024 18:36:35 -0300 Subject: [PATCH 23/32] =?UTF-8?q?ajustes=20de=20importa=C3=A7=C3=A3o=20de?= =?UTF-8?q?=20libs=20e=20implementa=C3=A7=C3=A3o=20de=20model=5Fconfig?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/database.py | 2 -- app/main.py | 3 --- app/settings.py | 8 ++------ requirements.txt | 1 - 4 files changed, 2 insertions(+), 12 deletions(-) diff --git a/app/database.py b/app/database.py index 1fb7bc7..4df7d40 100644 --- a/app/database.py +++ b/app/database.py @@ -4,9 +4,7 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from .settings import settings -from dotenv import load_dotenv -load_dotenv() SQLALCHEMY_DATABASE_URL = ( f"{settings.DB_DRIVER}://{settings.DB_USERNAME}:{settings.DB_PASSWORD}" diff --git a/app/main.py b/app/main.py index 881f9b2..a43941b 100644 --- a/app/main.py +++ b/app/main.py @@ -7,11 +7,8 @@ import sib_api_v3_sdk from sib_api_v3_sdk.rest import ApiException from pprint import pprint -from dotenv import load_dotenv from fastapi.middleware.cors import CORSMiddleware -load_dotenv() - from . import crud, models, schemas from app.database import SessionLocal, engine diff --git a/app/settings.py b/app/settings.py index 431a60a..60aa0b8 100644 --- a/app/settings.py +++ b/app/settings.py @@ -1,8 +1,5 @@ from pydantic_settings import BaseSettings, SettingsConfigDict -from dotenv import load_dotenv -import os -load_dotenv() class __Settings(BaseSettings): DB_DRIVER: str DB_USERNAME: str @@ -11,7 +8,6 @@ class __Settings(BaseSettings): DB_PORT: int DB_DATABASE: str -def get_settings(): - return __Settings() + model_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8') -settings = get_settings() \ No newline at end of file +settings = __Settings() # type:ignore \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 72dc956..5203071 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,4 +19,3 @@ uvicorn==0.15.0 wheel==0.43.0 mysql-connector-python==8.3.0 pydantic-settings==2.2.1 -load-dotenv==0.1.0 \ No newline at end of file From e6b31659156d24a3f331f0ffef418d60a0c43222 Mon Sep 17 00:00:00 2001 From: Stephanie Castro Date: Fri, 14 Jun 2024 22:09:56 -0300 Subject: [PATCH 24/32] estrutura para implementar o auth jwt --- app/auth.py | 21 +++++++++++++++++++++ app/crud.py | 4 ++-- app/main.py | 26 +++++++++++++++++++++----- app/models.py | 1 - app/schemas.py | 4 ++-- requirements.txt | 1 + 6 files changed, 47 insertions(+), 10 deletions(-) create mode 100644 app/auth.py diff --git a/app/auth.py b/app/auth.py new file mode 100644 index 0000000..9bee1dc --- /dev/null +++ b/app/auth.py @@ -0,0 +1,21 @@ +from typing import Annotated, Union # type:ignore +from fastapi import Depends +from pydantic import BaseModel +from fastapi.security import OAuth2PasswordBearer + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + +class UserAuth(BaseModel): + username: str + email: str + is_active: Union[bool, None] = None + +# Auth +def test_decoder(token): + return UserAuth( + username = token + "test_decoder", email = "test@soujunior.com" + ) + +async def get_test_user(token: Annotated[str, Depends(oauth2_scheme)]): + user = test_decoder(token) + return user \ No newline at end of file diff --git a/app/crud.py b/app/crud.py index ffd2306..a030fd4 100644 --- a/app/crud.py +++ b/app/crud.py @@ -70,7 +70,7 @@ def create_volunteer(db: Session, volunteer: schemas.Volunteer, jobtitle_id: int db_volunteer = models.Volunteer( name=volunteer.name, - email=volunteer.email, + email=volunteer.email, # type:ignore linkedin=volunteer.linkedin, is_active=volunteer.is_active, jobtitle_id=jobtitle_id @@ -85,4 +85,4 @@ def get_jobtitles(db: Session, skip: int = 0, limit: int = 100): return db.query(models.JobTitle).offset(skip).limit(limit).all() def get_volunteer_by_email(db: Session, email: str): - return db.query(models.Volunteer).filter(models.Volunteer.email == email).first() \ No newline at end of file + return db.query(models.Volunteer).filter(models.Volunteer.email == email).first() diff --git a/app/main.py b/app/main.py index a43941b..414f92c 100644 --- a/app/main.py +++ b/app/main.py @@ -4,14 +4,18 @@ from fastapi import Depends, FastAPI, HTTPException from sqlalchemy.orm import Session -import sib_api_v3_sdk -from sib_api_v3_sdk.rest import ApiException +import sib_api_v3_sdk # type:ignore +from sib_api_v3_sdk.rest import ApiException # type:ignore from pprint import pprint from fastapi.middleware.cors import CORSMiddleware -from . import crud, models, schemas +from app import crud, models, schemas from app.database import SessionLocal, engine +from app.auth import UserAuth, get_test_user, oauth2_scheme +from typing import Annotated # type:ignore +from fastapi import Depends + models.Base.metadata.create_all(bind=engine) app = FastAPI() @@ -30,6 +34,18 @@ allow_headers=["*"], ) +# Testando Auth2 +from fastapi import Depends, FastAPI + +@app.get("/items2/") +async def read_items(token: str = Depends(oauth2_scheme)): + return {"token": token} + +@app.get("/users/me") +async def read_users_me(current_user: Annotated[UserAuth, Depends(get_test_user)]): + return current_user + +# final Auth2 # Dependency def get_db(): @@ -68,7 +84,7 @@ def create_item_for_user( return crud.create_user_item(db=db, item=item, user_id=user_id) -@app.get("/items/", response_model=list[schemas.Item]) +@app.get("/items/", response_model=list[schemas.Item]) # type:ignore def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): items = crud.get_items(db, skip=skip, limit=limit) return items @@ -119,7 +135,7 @@ def create_volunteer(volunteer: schemas.VolunteerCreate, db: Session = Depends(g raise HTTPException(status_code=400, detail="Email already registered") vol = crud.create_volunteer( - db=db, volunteer=volunteer, jobtitle_id=volunteer.jobtitle_id + db=db, volunteer=volunteer, jobtitle_id=volunteer.jobtitle_id # type:ignore ) # send_email(volunteer.email, volunteer.name) return vol diff --git a/app/models.py b/app/models.py index 3223398..11e1d7d 100644 --- a/app/models.py +++ b/app/models.py @@ -3,7 +3,6 @@ from .database import Base - class User(Base): __tablename__ = "users" diff --git a/app/schemas.py b/app/schemas.py index 4aac939..82c15d8 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -1,5 +1,5 @@ from pydantic import BaseModel -from typing import Optional +from typing import Optional, Union class ItemBase(BaseModel): @@ -53,7 +53,7 @@ class VolunteerCreate(VolunteerBase): name: str email: str masked_email: Optional[str] = None - is_active: Optional[bool] = True + is_active: Optional[bool] = True # type:ignore jobtitle_id: int class Volunteer(VolunteerBase): diff --git a/requirements.txt b/requirements.txt index 5203071..710048c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,3 +19,4 @@ uvicorn==0.15.0 wheel==0.43.0 mysql-connector-python==8.3.0 pydantic-settings==2.2.1 +python-multpart=0.0.9 \ No newline at end of file From dcb87f5e563a5f739c833d9fe4b89f2ec2761f50 Mon Sep 17 00:00:00 2001 From: Stephanie Castro Date: Thu, 20 Jun 2024 22:05:57 -0300 Subject: [PATCH 25/32] auth implementation --- alembic/env.py | 2 +- app/auth.py | 38 +++++++++++++++++++++++++++++++++++--- app/crud.py | 1 + app/main.py | 26 ++++++++++++++++---------- app/models.py | 1 + app/schemas.py | 1 + requirements.txt | 1 - 7 files changed, 55 insertions(+), 15 deletions(-) diff --git a/alembic/env.py b/alembic/env.py index fc4fa78..ad7cd67 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -81,4 +81,4 @@ def run_migrations_online() -> None: if context.is_offline_mode(): run_migrations_offline() else: - run_migrations_online() + run_migrations_online() \ No newline at end of file diff --git a/app/auth.py b/app/auth.py index 9bee1dc..bf62aef 100644 --- a/app/auth.py +++ b/app/auth.py @@ -1,15 +1,25 @@ from typing import Annotated, Union # type:ignore -from fastapi import Depends +from fastapi import Depends, HTTPException, status from pydantic import BaseModel -from fastapi.security import OAuth2PasswordBearer +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + class UserAuth(BaseModel): username: str email: str is_active: Union[bool, None] = None +class UserInDB(UserAuth): + hashed_password: str + # Auth def test_decoder(token): return UserAuth( @@ -18,4 +28,26 @@ def test_decoder(token): async def get_test_user(token: Annotated[str, Depends(oauth2_scheme)]): user = test_decoder(token) - return user \ No newline at end of file + return user + +def fake_hashed_password(password: str): + return "fakehashed" + password + +def get_user(db, username: str): + if username in db: + user_dict = db[username] + return UserInDB(**user_dict) + +def fake_decoder_token(token): + user = get_user(get_db, token) + +async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): + user = fake_decoder_token(token) + if not user: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid authentication credentials") + return user + +async def get_current_activate_user(current_user: Annotated[UserAuth, Depend(get_current_user)],): + if current_user.is_active: + raise HTTPException(status_code=400, detail="Inactive user") + return current_user \ No newline at end of file diff --git a/app/crud.py b/app/crud.py index a030fd4..fdec4dd 100644 --- a/app/crud.py +++ b/app/crud.py @@ -20,6 +20,7 @@ def get_users(db: Session, skip: int = 0, limit: int = 100): def create_user(db: Session, user: schemas.UserCreate): fake_hashed_password = user.password + "notreallyhashed" db_user = models.User(email=user.email, hashed_password=fake_hashed_password) + db_username = models.User(username=user.username, hashed_password=fake_hashed_password) db.add(db_user) db.commit() db.refresh(db_user) diff --git a/app/main.py b/app/main.py index 414f92c..d5d3f39 100644 --- a/app/main.py +++ b/app/main.py @@ -14,7 +14,8 @@ from app.auth import UserAuth, get_test_user, oauth2_scheme from typing import Annotated # type:ignore -from fastapi import Depends +from fastapi.security import OAuth2PasswordRequestForm +from fastapi import Depends, FastAPI models.Base.metadata.create_all(bind=engine) @@ -34,8 +35,16 @@ allow_headers=["*"], ) +# Dependency +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + + # Testando Auth2 -from fastapi import Depends, FastAPI @app.get("/items2/") async def read_items(token: str = Depends(oauth2_scheme)): @@ -45,15 +54,12 @@ async def read_items(token: str = Depends(oauth2_scheme)): async def read_users_me(current_user: Annotated[UserAuth, Depends(get_test_user)]): return current_user -# final Auth2 +app.post("/token") +async def login(form_data: Annotated[OAuth2PasswordRequestForm, db: Depends(get_db)]): + #user_dict = db.get(form_data.username) + pass -# Dependency -def get_db(): - db = SessionLocal() - try: - yield db - finally: - db.close() +# final Auth2 @app.post("/users/", response_model=schemas.User) def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)): diff --git a/app/models.py b/app/models.py index 11e1d7d..d5722b7 100644 --- a/app/models.py +++ b/app/models.py @@ -8,6 +8,7 @@ class User(Base): id = Column(Integer, primary_key=True) email = Column(String(320), unique=True, index=True) + username = Column(String(320), unique=True, index=True) hashed_password = Column(String(255)) is_active = Column(Boolean, default=True) diff --git a/app/schemas.py b/app/schemas.py index 82c15d8..c468d97 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -24,6 +24,7 @@ class UserBase(BaseModel): class UserCreate(UserBase): + username: str password: str diff --git a/requirements.txt b/requirements.txt index 710048c..5203071 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,4 +19,3 @@ uvicorn==0.15.0 wheel==0.43.0 mysql-connector-python==8.3.0 pydantic-settings==2.2.1 -python-multpart=0.0.9 \ No newline at end of file From f046c5277858bf2d8720c748a87440e52500a85b Mon Sep 17 00:00:00 2001 From: Stephanie Castro Date: Fri, 21 Jun 2024 16:55:32 -0300 Subject: [PATCH 26/32] implementation auth jwt --- app/auth.py | 2 +- app/main.py | 23 +++++++++++++++++------ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/app/auth.py b/app/auth.py index bf62aef..2b3ac8c 100644 --- a/app/auth.py +++ b/app/auth.py @@ -47,7 +47,7 @@ async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid authentication credentials") return user -async def get_current_activate_user(current_user: Annotated[UserAuth, Depend(get_current_user)],): +async def get_current_activate_user(current_user: Annotated[UserAuth, Depends(get_current_user)],): if current_user.is_active: raise HTTPException(status_code=400, detail="Inactive user") return current_user \ No newline at end of file diff --git a/app/main.py b/app/main.py index d5d3f39..4419d29 100644 --- a/app/main.py +++ b/app/main.py @@ -12,13 +12,15 @@ from app import crud, models, schemas from app.database import SessionLocal, engine -from app.auth import UserAuth, get_test_user, oauth2_scheme +from app.auth import UserAuth, get_test_user, oauth2_scheme, UserInDB from typing import Annotated # type:ignore from fastapi.security import OAuth2PasswordRequestForm from fastapi import Depends, FastAPI +from app.models import User models.Base.metadata.create_all(bind=engine) + app = FastAPI() origins = [ @@ -46,6 +48,20 @@ def get_db(): # Testando Auth2 +app.post("/token") +async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()], db: Session = Depends(get_db)): + user = db.query(User).filter(User.username == form_data.username).first() + + if not user: + raise HTTPException(status_code=400, detail="Incorrect username or password") + userAuth = UserInDB(**user) + hashed_password = db.query(User).filter(User.password == form_data.password).first() + + if not hashed_password == user.hashed_password: + raise HTTPException(status_code=400, detail="Incorrect username or password") + + return {"access_token": user.username, "token_type": "bearer"} + @app.get("/items2/") async def read_items(token: str = Depends(oauth2_scheme)): return {"token": token} @@ -54,11 +70,6 @@ async def read_items(token: str = Depends(oauth2_scheme)): async def read_users_me(current_user: Annotated[UserAuth, Depends(get_test_user)]): return current_user -app.post("/token") -async def login(form_data: Annotated[OAuth2PasswordRequestForm, db: Depends(get_db)]): - #user_dict = db.get(form_data.username) - pass - # final Auth2 @app.post("/users/", response_model=schemas.User) From 125f2d5d8150b657dbfd20b1d96e8338cf4de35e Mon Sep 17 00:00:00 2001 From: Stephanie Castro Date: Wed, 26 Jun 2024 23:09:42 -0300 Subject: [PATCH 27/32] implementation of new functions for JWT authentication --- app/auth.py | 95 ++++++++++++++++++++++++++++++++++--------------- app/crud.py | 23 +++++++----- app/main.py | 43 +++++++++++----------- app/settings.py | 5 ++- 4 files changed, 108 insertions(+), 58 deletions(-) diff --git a/app/auth.py b/app/auth.py index 2b3ac8c..46c8764 100644 --- a/app/auth.py +++ b/app/auth.py @@ -2,15 +2,26 @@ from fastapi import Depends, HTTPException, status from pydantic import BaseModel from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from sqlalchemy.orm import Session +from jose import JWTError, jwt +from passlib.context import CryptContext +from app.database import SessionLocal +from app.crud import get_user_by_username +from datetime import datetime, timedelta +from app.settings import settings +from app.main import get_db oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") -def get_db(): - db = SessionLocal() - try: - yield db - finally: - db.close() +# config de criptografia de senhas +pwd_context = CryptContext(schemas=["bcrypt"], deprecated="auto") + +class Token(BaseModel): + access_token: str + token_type: str + +class TokenData(BaseModel): + username: Union[str, None] = None class UserAuth(BaseModel): username: str @@ -20,34 +31,60 @@ class UserAuth(BaseModel): class UserInDB(UserAuth): hashed_password: str -# Auth -def test_decoder(token): - return UserAuth( - username = token + "test_decoder", email = "test@soujunior.com" - ) +def verify_password(plain_password, hashed_password): + return pwd_context.verify(plain_password, hashed_password) -async def get_test_user(token: Annotated[str, Depends(oauth2_scheme)]): - user = test_decoder(token) - return user +def get_password_hash(password): + return pwd.context.hash(password) -def fake_hashed_password(password: str): - return "fakehashed" + password +def get_user(db: Session, username: str): + return get_user_by_username(db, username) + +def authenticate_user(db: Session, username: str, password: str): + user = get_user(db, username) + if not user: + return False + if not verify_password(password, user.hashed_password): + return False + return user -def get_user(db, username: str): - if username in db: - user_dict = db[username] - return UserInDB(**user_dict) +def create_access_token(data: dict,expires_delta: Union[timedelta, None] = None, settings = settings): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, settings.SECRETE_KEY, settings.ALGORITHM) + return encoded_jwt -def fake_decoder_token(token): - user = get_user(get_db, token) +async def get_current_user(settings, token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)): + credentials_exception = HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}) -async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]): - user = fake_decoder_token(token) - if not user: - raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid authentication credentials") + try: + payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) + username: Union[str, None] = payload.get("sub") + if username is None: + raise credentials_exception + token_data = TokenData(username=username) + except JWTError: + raise credentials_exception + if token_data.username is None: + raise credentials_exception + user = get_user(db, token_data.username) + if user is None: + raise credentials_exception return user -async def get_current_activate_user(current_user: Annotated[UserAuth, Depends(get_current_user)],): - if current_user.is_active: +async def get_current_active_user(current_user: UserAuth = Depends(get_current_user)): + if not current_user.is_active: raise HTTPException(status_code=400, detail="Inactive user") - return current_user \ No newline at end of file + return current_user + +def get_db_session(): + db = SessionLocal() + try: + yield db + finally: + db.close() \ No newline at end of file diff --git a/app/crud.py b/app/crud.py index fdec4dd..59b28df 100644 --- a/app/crud.py +++ b/app/crud.py @@ -1,26 +1,30 @@ from sqlalchemy.orm import Session - from sqlalchemy import create_engine, Column, Integer, String, Boolean, func - from . import models, schemas +from app.auth import get_password_hash def get_user(db: Session, user_id: int): return db.query(models.User).filter(models.User.id == user_id).first() - - + def get_user_by_email(db: Session, email: str): return db.query(models.User).filter(models.User.email == email).first() +def get_user_by_username(db: Session, username: str): + return db.query(models.User).filter(models.User.username == username).first() def get_users(db: Session, skip: int = 0, limit: int = 100): return db.query(models.User).offset(skip).limit(limit).all() def create_user(db: Session, user: schemas.UserCreate): - fake_hashed_password = user.password + "notreallyhashed" - db_user = models.User(email=user.email, hashed_password=fake_hashed_password) - db_username = models.User(username=user.username, hashed_password=fake_hashed_password) + hashed_password = get_password_hash(user.password) + db_user = models.User( + username=user.username, + email=user.email, + hashed_password=hashed_password, + is_active=True, + ) db.add(db_user) db.commit() db.refresh(db_user) @@ -65,6 +69,7 @@ def get_volunteers_by_email(db: Session, skip: int = 0, limit: int = 100, email: models.Volunteer.jobtitle_id, ).filter(models.Volunteer.email == email).first() + def create_volunteer(db: Session, volunteer: schemas.Volunteer, jobtitle_id: int): # print(volunteer.jobtitle_id[0].id) # return @@ -82,8 +87,10 @@ def create_volunteer(db: Session, volunteer: schemas.Volunteer, jobtitle_id: int print("db_volunteer",db_volunteer) return db_volunteer + def get_jobtitles(db: Session, skip: int = 0, limit: int = 100): return db.query(models.JobTitle).offset(skip).limit(limit).all() + def get_volunteer_by_email(db: Session, email: str): - return db.query(models.Volunteer).filter(models.Volunteer.email == email).first() + return db.query(models.Volunteer).filter(models.Volunteer.email == email).first() \ No newline at end of file diff --git a/app/main.py b/app/main.py index 4419d29..531f1e1 100644 --- a/app/main.py +++ b/app/main.py @@ -1,7 +1,7 @@ from __future__ import print_function import os -from fastapi import Depends, FastAPI, HTTPException +from fastapi import Depends, FastAPI, HTTPException, status from sqlalchemy.orm import Session import sib_api_v3_sdk # type:ignore @@ -12,11 +12,13 @@ from app import crud, models, schemas from app.database import SessionLocal, engine -from app.auth import UserAuth, get_test_user, oauth2_scheme, UserInDB +from app.settings import settings +from app.auth import UserAuth, oauth2_scheme, UserInDB, authenticate_user, create_access_token, get_current_user, get_current_active_user, Token from typing import Annotated # type:ignore from fastapi.security import OAuth2PasswordRequestForm from fastapi import Depends, FastAPI from app.models import User +from datetime import datetime, timedelta models.Base.metadata.create_all(bind=engine) @@ -46,26 +48,27 @@ def get_db(): db.close() -# Testando Auth2 - -app.post("/token") -async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()], db: Session = Depends(get_db)): - user = db.query(User).filter(User.username == form_data.username).first() +# Testando Auth +@app.post("/token", response_model=Token) +async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): + user = authenticate_user(db, form_data.username, form_data.password) if not user: - raise HTTPException(status_code=400, detail="Incorrect username or password") - userAuth = UserInDB(**user) - hashed_password = db.query(User).filter(User.password == form_data.password).first() - - if not hashed_password == user.hashed_password: - raise HTTPException(status_code=400, detail="Incorrect username or password") - - return {"access_token": user.username, "token_type": "bearer"} - -@app.get("/items2/") -async def read_items(token: str = Depends(oauth2_scheme)): - return {"token": token} - + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + + if settings.ACCESS_TOKEN_EXPIRE_MINUTES is None: + raise ValueError("ACCESS_TOKEN_EXPIRE_MINUTES cannot be None") + + access_token_expires = timedelta(minutes=float(settings.ACCESS_TOKEN_EXPIRE_MINUTES)) + access_token = create_access_token( + data={"sub": user.username}, expires_delta=access_token_expires, settings=settings + ) + return {"access_token": access_token, "token_type": "bearer"} + @app.get("/users/me") async def read_users_me(current_user: Annotated[UserAuth, Depends(get_test_user)]): return current_user diff --git a/app/settings.py b/app/settings.py index 60aa0b8..9116a29 100644 --- a/app/settings.py +++ b/app/settings.py @@ -1,5 +1,5 @@ from pydantic_settings import BaseSettings, SettingsConfigDict - +from typing import Union class __Settings(BaseSettings): DB_DRIVER: str DB_USERNAME: str @@ -7,6 +7,9 @@ class __Settings(BaseSettings): DB_HOST: str DB_PORT: int DB_DATABASE: str + SECRETE_KEY: str + ALGORITHM: str + ACCESS_TOKEN_EXPIRE_MINUTES: float model_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8') From a863ead5cccb57212be9be54b093312063468542 Mon Sep 17 00:00:00 2001 From: Stephanie Castro Date: Thu, 27 Jun 2024 22:31:15 -0300 Subject: [PATCH 28/32] new implementation --- app/auth.py | 60 +++++++++++++++++----------------------------- app/crud.py | 4 +--- app/database.py | 8 +++++++ app/main.py | 34 +++++++------------------- app/models.py | 2 +- app/schemas.py | 19 +++++++++++++-- app/settings.py | 6 ++--- app/utils.py | 5 ++++ docker-compose.yml | 3 +++ requirements.txt | 56 ++++++++++++++++++++++++++++++++++++------- 10 files changed, 115 insertions(+), 82 deletions(-) create mode 100644 app/utils.py diff --git a/app/auth.py b/app/auth.py index 46c8764..79d0292 100644 --- a/app/auth.py +++ b/app/auth.py @@ -1,41 +1,26 @@ -from typing import Annotated, Union # type:ignore +from typing import Annotated, Union from fastapi import Depends, HTTPException, status from pydantic import BaseModel from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from sqlalchemy.orm import Session from jose import JWTError, jwt from passlib.context import CryptContext -from app.database import SessionLocal -from app.crud import get_user_by_username +from app.database import get_db, SessionLocal from datetime import datetime, timedelta from app.settings import settings -from app.main import get_db +from app.schemas import UserAuth, TokenData +from app.utils import get_user_by_username oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") -# config de criptografia de senhas -pwd_context = CryptContext(schemas=["bcrypt"], deprecated="auto") - -class Token(BaseModel): - access_token: str - token_type: str - -class TokenData(BaseModel): - username: Union[str, None] = None - -class UserAuth(BaseModel): - username: str - email: str - is_active: Union[bool, None] = None - -class UserInDB(UserAuth): - hashed_password: str +# Configuração de criptografia de senhas +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") def verify_password(plain_password, hashed_password): return pwd_context.verify(plain_password, hashed_password) def get_password_hash(password): - return pwd.context.hash(password) + return pwd_context.hash(password) def get_user(db: Session, username: str): return get_user_by_username(db, username) @@ -48,43 +33,42 @@ def authenticate_user(db: Session, username: str, password: str): return False return user -def create_access_token(data: dict,expires_delta: Union[timedelta, None] = None, settings = settings): +def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): to_encode = data.copy() if expires_delta: expire = datetime.utcnow() + expires_delta else: - expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) + expire = datetime.utcnow() + timedelta(minutes=settings.JWT_EXPIRE_MINUTES) to_encode.update({"exp": expire}) - encoded_jwt = jwt.encode(to_encode, settings.SECRETE_KEY, settings.ALGORITHM) + encoded_jwt = jwt.encode(to_encode, settings.JWT_SECRETE_KEY, algorithm=settings.PASSWORD_HASH_ALGORITHM) # Corrigido para SECRET_KEY return encoded_jwt -async def get_current_user(settings, token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)): - credentials_exception = HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, - detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}) +def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) try: - payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) - username: Union[str, None] = payload.get("sub") + payload = jwt.decode(token, settings.JWT_SECRETE_KEY, algorithms=[settings.PASSWORD_HASH_ALGORITHM]) # Corrigido para SECRET_KEY e ALGORITHM + username: Union[str, None] = payload.get("sub") if username is None: raise credentials_exception token_data = TokenData(username=username) except JWTError: raise credentials_exception + if token_data.username is None: raise credentials_exception + user = get_user(db, token_data.username) if user is None: raise credentials_exception + return user async def get_current_active_user(current_user: UserAuth = Depends(get_current_user)): if not current_user.is_active: raise HTTPException(status_code=400, detail="Inactive user") - return current_user - -def get_db_session(): - db = SessionLocal() - try: - yield db - finally: - db.close() \ No newline at end of file + return current_user \ No newline at end of file diff --git a/app/crud.py b/app/crud.py index 59b28df..b25d2a3 100644 --- a/app/crud.py +++ b/app/crud.py @@ -2,6 +2,7 @@ from sqlalchemy import create_engine, Column, Integer, String, Boolean, func from . import models, schemas from app.auth import get_password_hash +from app.utils import get_user_by_username def get_user(db: Session, user_id: int): @@ -10,9 +11,6 @@ def get_user(db: Session, user_id: int): def get_user_by_email(db: Session, email: str): return db.query(models.User).filter(models.User.email == email).first() -def get_user_by_username(db: Session, username: str): - return db.query(models.User).filter(models.User.username == username).first() - def get_users(db: Session, skip: int = 0, limit: int = 100): return db.query(models.User).offset(skip).limit(limit).all() diff --git a/app/database.py b/app/database.py index 4df7d40..8388a6d 100644 --- a/app/database.py +++ b/app/database.py @@ -16,3 +16,11 @@ Base = declarative_base() metadata = Base.metadata + +# Dependency +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() \ No newline at end of file diff --git a/app/main.py b/app/main.py index 531f1e1..3434dff 100644 --- a/app/main.py +++ b/app/main.py @@ -5,19 +5,18 @@ from sqlalchemy.orm import Session import sib_api_v3_sdk # type:ignore -from sib_api_v3_sdk.rest import ApiException # type:ignore +from sib_api_v3_sdk.rest import ApiException # type:ignore from pprint import pprint from fastapi.middleware.cors import CORSMiddleware from app import crud, models, schemas -from app.database import SessionLocal, engine +from app.database import SessionLocal, engine, get_db from app.settings import settings -from app.auth import UserAuth, oauth2_scheme, UserInDB, authenticate_user, create_access_token, get_current_user, get_current_active_user, Token -from typing import Annotated # type:ignore +from app.auth import oauth2_scheme, authenticate_user, create_access_token, get_current_user, get_current_active_user +from typing import Annotated from fastapi.security import OAuth2PasswordRequestForm from fastapi import Depends, FastAPI -from app.models import User from datetime import datetime, timedelta models.Base.metadata.create_all(bind=engine) @@ -39,18 +38,7 @@ allow_headers=["*"], ) -# Dependency -def get_db(): - db = SessionLocal() - try: - yield db - finally: - db.close() - - -# Testando Auth - -@app.post("/token", response_model=Token) +@app.post("/token", response_model=schemas.Token) async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): user = authenticate_user(db, form_data.username, form_data.password) if not user: @@ -60,20 +48,14 @@ async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends( headers={"WWW-Authenticate": "Bearer"}, ) - if settings.ACCESS_TOKEN_EXPIRE_MINUTES is None: + if settings.JWT_EXPIRE_MINUTES is None: raise ValueError("ACCESS_TOKEN_EXPIRE_MINUTES cannot be None") - access_token_expires = timedelta(minutes=float(settings.ACCESS_TOKEN_EXPIRE_MINUTES)) + access_token_expires = timedelta(minutes=float(settings.JWT_EXPIRE_MINUTES)) access_token = create_access_token( data={"sub": user.username}, expires_delta=access_token_expires, settings=settings ) return {"access_token": access_token, "token_type": "bearer"} - -@app.get("/users/me") -async def read_users_me(current_user: Annotated[UserAuth, Depends(get_test_user)]): - return current_user - -# final Auth2 @app.post("/users/", response_model=schemas.User) def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)): @@ -104,7 +86,7 @@ def create_item_for_user( return crud.create_user_item(db=db, item=item, user_id=user_id) -@app.get("/items/", response_model=list[schemas.Item]) # type:ignore +@app.get("/items/", response_model=list[schemas.Item]) def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): items = crud.get_items(db, skip=skip, limit=limit) return items diff --git a/app/models.py b/app/models.py index d5722b7..c2e788b 100644 --- a/app/models.py +++ b/app/models.py @@ -42,7 +42,7 @@ class Volunteer(Base): id = Column(Integer, primary_key=True) name = Column(String(45), index=True) linkedin = Column(String(3072), index=True) - email = Column(String(320), index=True) + email = Column(String(255), index=True) is_active = Column(Boolean, default=True) jobtitle_id = Column(Integer, ForeignKey("jobtitle.id")) diff --git a/app/schemas.py b/app/schemas.py index c468d97..504965c 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -48,13 +48,13 @@ class VolunteerBase(BaseModel): name: str linkedin: str # email: str - is_active: bool + is_active: Optional[bool] class VolunteerCreate(VolunteerBase): name: str email: str masked_email: Optional[str] = None - is_active: Optional[bool] = True # type:ignore + is_active: Optional[bool] = True jobtitle_id: int class Volunteer(VolunteerBase): @@ -66,3 +66,18 @@ class Volunteer(VolunteerBase): class Config: orm_mode = True +class Token(BaseModel): + access_token: str + token_type: str + +class TokenData(BaseModel): + username: Union[str, None] = None + +class UserAuth(BaseModel): + username: str + email: str + is_active: Union[bool, None] = None + +class UserInDB(UserAuth): + hashed_password: str + diff --git a/app/settings.py b/app/settings.py index 9116a29..e523331 100644 --- a/app/settings.py +++ b/app/settings.py @@ -7,9 +7,9 @@ class __Settings(BaseSettings): DB_HOST: str DB_PORT: int DB_DATABASE: str - SECRETE_KEY: str - ALGORITHM: str - ACCESS_TOKEN_EXPIRE_MINUTES: float + JWT_SECRETE_KEY: str + PASSWORD_HASH_ALGORITHM: str + JWT_EXPIRE_MINUTES: int model_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8') diff --git a/app/utils.py b/app/utils.py new file mode 100644 index 0000000..97e6c97 --- /dev/null +++ b/app/utils.py @@ -0,0 +1,5 @@ +from sqlalchemy.orm import Session +from app import models + +def get_user_by_username(db: Session, username: str): + return db.query(models.User).filter(models.User.username == username).first() \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 87b1c0a..f2e5a08 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,9 @@ services: DB_HOST: mysql_database DB_PORT: 3306 DB_DATABASE: db + JWT_SECRETE_KEY: testestestestetsteste + PASSWORD_HASH_ALGORITHM: HS256 + JWT_EXPIRE_MINUTES: 30 ports: - '8000:80' volumes: diff --git a/requirements.txt b/requirements.txt index 5203071..bfa7c59 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,21 +1,59 @@ alembic==1.13.1 +annotated-types==0.6.0 +anyio==4.3.0 asgiref==3.8.1 certifi==2024.2.2 click==8.1.7 -fastapi==0.68.2 +dnspython==2.6.1 +ecdsa==0.19.0 +email_validator==2.1.1 +fastapi==0.111.0 +fastapi-cli==0.0.3 greenlet==3.0.3 h11==0.14.0 -pydantic==1.10.14 +httpcore==1.0.5 +httptools==0.6.1 +httpx==0.27.0 +idna==3.7 +install==1.3.5 +Jinja2==3.1.4 +jose==1.0.0 +load-dotenv==0.1.0 +Mako==1.3.5 +markdown-it-py==3.0.0 +MarkupSafe==2.1.5 +mdurl==0.1.2 +mysql-connector-python==8.4.0 +orjson==3.10.3 +passlib==1.7.4 +pyasn1==0.6.0 +pydantic==2.7.1 +pydantic-settings==2.2.1 +pydantic_core==2.18.2 +Pygments==2.18.0 python-dateutil==2.9.0.post0 python-dotenv==1.0.1 -setuptools==69.1.1 +python-jose==3.3.0 +python-multipart==0.0.9 +PyYAML==6.0.1 +rich==13.7.1 +rsa==4.9 +setuptools==69.5.1 +shellingham==1.5.4 sib-api-v3-sdk==7.6.0 six==1.16.0 -SQLAlchemy==2.0.29 -starlette==0.14.2 -typing_extensions==4.10.0 +sniffio==1.3.1 +SQLAlchemy==2.0.30 +starlette==0.37.2 +typer==0.12.3 +types-passlib==1.7.7.20240327 +types-pyasn1==0.6.0.20240402 +types-python-jose==3.3.4.20240106 +typing_extensions==4.11.0 +ujson==5.10.0 urllib3==2.2.1 -uvicorn==0.15.0 +uvicorn==0.29.0 +uvloop==0.19.0 +watchfiles==0.21.0 +websockets==12.0 wheel==0.43.0 -mysql-connector-python==8.3.0 -pydantic-settings==2.2.1 From 5194be554efe3dac3c9bc529f55888294a85584b Mon Sep 17 00:00:00 2001 From: Stephanie Castro Date: Fri, 28 Jun 2024 20:36:18 -0300 Subject: [PATCH 29/32] final version jwt authentication implementation --- app/auth.py | 25 +++++++++++-------------- app/crud.py | 25 ++----------------------- app/main.py | 18 ++++++++++-------- app/schemas.py | 6 +----- app/utils.py | 11 ++++++++++- 5 files changed, 34 insertions(+), 51 deletions(-) diff --git a/app/auth.py b/app/auth.py index 79d0292..27a6a8e 100644 --- a/app/auth.py +++ b/app/auth.py @@ -16,31 +16,28 @@ # Configuração de criptografia de senhas pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") -def verify_password(plain_password, hashed_password): +def verify_password(plain_password: str, hashed_password: str) -> bool: return pwd_context.verify(plain_password, hashed_password) -def get_password_hash(password): +def get_password_hash(password: str) -> str: return pwd_context.hash(password) -def get_user(db: Session, username: str): - return get_user_by_username(db, username) - -def authenticate_user(db: Session, username: str, password: str): - user = get_user(db, username) +def authenticate_user(db: Session, username: str, password: str) -> bool: + user = get_user_by_username(db, username) if not user: return False if not verify_password(password, user.hashed_password): return False return user -def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None): - to_encode = data.copy() +def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None) -> str: + if expires_delta: expire = datetime.utcnow() + expires_delta else: expire = datetime.utcnow() + timedelta(minutes=settings.JWT_EXPIRE_MINUTES) - to_encode.update({"exp": expire}) - encoded_jwt = jwt.encode(to_encode, settings.JWT_SECRETE_KEY, algorithm=settings.PASSWORD_HASH_ALGORITHM) # Corrigido para SECRET_KEY + data.update({"exp": expire}) + encoded_jwt = jwt.encode(data, settings.JWT_SECRETE_KEY, algorithm=settings.PASSWORD_HASH_ALGORITHM) return encoded_jwt def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)): @@ -51,7 +48,7 @@ def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends( ) try: - payload = jwt.decode(token, settings.JWT_SECRETE_KEY, algorithms=[settings.PASSWORD_HASH_ALGORITHM]) # Corrigido para SECRET_KEY e ALGORITHM + payload = jwt.decode(token, settings.JWT_SECRETE_KEY, algorithms=[settings.PASSWORD_HASH_ALGORITHM]) username: Union[str, None] = payload.get("sub") if username is None: raise credentials_exception @@ -62,13 +59,13 @@ def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends( if token_data.username is None: raise credentials_exception - user = get_user(db, token_data.username) + user = get_user_by_username(db, token_data.username) if user is None: raise credentials_exception return user -async def get_current_active_user(current_user: UserAuth = Depends(get_current_user)): +def get_current_active_user(current_user: UserAuth = Depends(get_current_user)): if not current_user.is_active: raise HTTPException(status_code=400, detail="Inactive user") return current_user \ No newline at end of file diff --git a/app/crud.py b/app/crud.py index b25d2a3..b01d499 100644 --- a/app/crud.py +++ b/app/crud.py @@ -2,17 +2,6 @@ from sqlalchemy import create_engine, Column, Integer, String, Boolean, func from . import models, schemas from app.auth import get_password_hash -from app.utils import get_user_by_username - - -def get_user(db: Session, user_id: int): - return db.query(models.User).filter(models.User.id == user_id).first() - -def get_user_by_email(db: Session, email: str): - return db.query(models.User).filter(models.User.email == email).first() - -def get_users(db: Session, skip: int = 0, limit: int = 100): - return db.query(models.User).offset(skip).limit(limit).all() def create_user(db: Session, user: schemas.UserCreate): @@ -68,21 +57,11 @@ def get_volunteers_by_email(db: Session, skip: int = 0, limit: int = 100, email: ).filter(models.Volunteer.email == email).first() -def create_volunteer(db: Session, volunteer: schemas.Volunteer, jobtitle_id: int): - # print(volunteer.jobtitle_id[0].id) - # return - - db_volunteer = models.Volunteer( - name=volunteer.name, - email=volunteer.email, # type:ignore - linkedin=volunteer.linkedin, - is_active=volunteer.is_active, - jobtitle_id=jobtitle_id - ) +def create_volunteer(db: Session, volunteer: schemas.VolunteerBase, jobtitle_id: int): + db_volunteer = models.Volunteer(**volunteer.dict()) db.add(db_volunteer) db.commit() db.refresh(db_volunteer) - print("db_volunteer",db_volunteer) return db_volunteer diff --git a/app/main.py b/app/main.py index 3434dff..2db0615 100644 --- a/app/main.py +++ b/app/main.py @@ -4,13 +4,13 @@ from fastapi import Depends, FastAPI, HTTPException, status from sqlalchemy.orm import Session -import sib_api_v3_sdk # type:ignore -from sib_api_v3_sdk.rest import ApiException # type:ignore +import sib_api_v3_sdk +from sib_api_v3_sdk.rest import ApiException from pprint import pprint from fastapi.middleware.cors import CORSMiddleware from app import crud, models, schemas -from app.database import SessionLocal, engine, get_db +from app.database import SessionLocal, engine from app.settings import settings from app.auth import oauth2_scheme, authenticate_user, create_access_token, get_current_user, get_current_active_user @@ -18,6 +18,8 @@ from fastapi.security import OAuth2PasswordRequestForm from fastapi import Depends, FastAPI from datetime import datetime, timedelta +from app.database import get_db +from app import utils models.Base.metadata.create_all(bind=engine) @@ -39,7 +41,7 @@ ) @app.post("/token", response_model=schemas.Token) -async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): +async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)) -> dict: user = authenticate_user(db, form_data.username, form_data.password) if not user: raise HTTPException( @@ -53,13 +55,13 @@ async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends( access_token_expires = timedelta(minutes=float(settings.JWT_EXPIRE_MINUTES)) access_token = create_access_token( - data={"sub": user.username}, expires_delta=access_token_expires, settings=settings + data={"sub": user.username} ) return {"access_token": access_token, "token_type": "bearer"} @app.post("/users/", response_model=schemas.User) def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)): - db_user = crud.get_user_by_email(db, email=user.email) + db_user = utils.get_user_by_email(db, email=user.email) if db_user: raise HTTPException(status_code=400, detail="Email already registered") return crud.create_user(db=db, user=user) @@ -67,13 +69,13 @@ def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)): @app.get("/users/", response_model=list[schemas.User]) def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): - users = crud.get_users(db, skip=skip, limit=limit) + users = utils.get_users(db, skip=skip, limit=limit) return users @app.get("/users/{user_id}", response_model=schemas.User) def read_user(user_id: int, db: Session = Depends(get_db)): - db_user = crud.get_user(db, user_id=user_id) + db_user = utils.get_user(db, user_id=user_id) if db_user is None: raise HTTPException(status_code=404, detail="User not found") return db_user diff --git a/app/schemas.py b/app/schemas.py index 504965c..aa5d8e2 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -47,7 +47,7 @@ class Config: class VolunteerBase(BaseModel): name: str linkedin: str - # email: str + email: str is_active: Optional[bool] class VolunteerCreate(VolunteerBase): @@ -62,10 +62,6 @@ class Volunteer(VolunteerBase): jobtitle_id: int masked_email: Optional[str] = None - - class Config: - orm_mode = True - class Token(BaseModel): access_token: str token_type: str diff --git a/app/utils.py b/app/utils.py index 97e6c97..8edc5b4 100644 --- a/app/utils.py +++ b/app/utils.py @@ -2,4 +2,13 @@ from app import models def get_user_by_username(db: Session, username: str): - return db.query(models.User).filter(models.User.username == username).first() \ No newline at end of file + return db.query(models.User).filter(models.User.username == username).first() + +def get_user(db: Session, user_id: int): + return db.query(models.User).filter(models.User.id == user_id).first() + +def get_user_by_email(db: Session, email: str): + return db.query(models.User).filter(models.User.email == email).first() + +def get_users(db: Session, skip: int = 0, limit: int = 100): + return db.query(models.User).offset(skip).limit(limit).all() From 01340b608379a09f2f2e557869f2029e50068f96 Mon Sep 17 00:00:00 2001 From: Stephanie Castro Date: Wed, 24 Jul 2024 21:37:57 -0300 Subject: [PATCH 30/32] ajustes apontados no PR --- README.md | 52 +++++++++++++++----------------------------- app/auth.py | 28 ++++++++++++------------ app/crud.py | 4 ++-- app/main.py | 20 ++++++++--------- app/models.py | 1 - app/settings.py | 12 +++++------ app/utils.py | 6 +++--- requirements.txt | 56 ++++++++---------------------------------------- 8 files changed, 61 insertions(+), 118 deletions(-) diff --git a/README.md b/README.md index ae41b1f..88a4cdf 100644 --- a/README.md +++ b/README.md @@ -1,67 +1,49 @@ # stars-api -# Documentação +# Documentação ## Este projeto é um aplicativo Python com FastApi com todo o ambiente de execução encapsulado em Docker. -### ambiente virtual +### ambiente virtual python3 -m venv env - -### ativação do ambiente +### ativação do ambiente source env/bin/activate - -### install nas dependencias +### install nas dependencias pip install -r requirements.txt - -#### Atulização nas dependencias +#### Atulização nas dependencias pip freeze > requirements.txt - -#### Comando para criar a imagem docker no projeto +#### Comando para criar a imagem docker no projeto docker build -t backoffice -f .docker/Dockerfile . - -#### Configurações de vulnerabilidade da imagem sugerida pelo docker +#### Configurações de vulnerabilidade da imagem sugerida pelo docker docker scout cves local://backoffice:latest - docker scout recommendations local://backoffice:latest - -### Comando para checar se a imagem foi criada +### Comando para checar se a imagem foi criada docker images - -### Executar o container e verificar se esta em execução - docker run -d -p 80:80 backoffice_soujunior +### Executar o container e verificar se esta em execução + docker run -d -p 80:80 nome_do_container docker ps - - -### Comandos para criar os containers -docker-compose up +### Comandos para criar/subir os containers +docker-compose up docker-compose ps +### Parar containers +docker compose down - -### Comandos docker +### Comandos uteis docker docker-compose stop - -docker-compose start - +docker-compose start docker-compose restart - ### Porta e swagger http://localhost:8000/docs - -### Parar o servidor +### Parar o servidor docker stop 65d05c5e44806478fd97914e8ecdb61a3a1b530686b20640da7c68e5717ec7a3 -## Subir containers -docker compose up - -### Parar containers -docker compose down \ No newline at end of file diff --git a/app/auth.py b/app/auth.py index 27a6a8e..8061af2 100644 --- a/app/auth.py +++ b/app/auth.py @@ -1,4 +1,4 @@ -from typing import Annotated, Union +from typing import Annotated, Union from fastapi import Depends, HTTPException, status from pydantic import BaseModel from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm @@ -16,14 +16,14 @@ # Configuração de criptografia de senhas pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") -def verify_password(plain_password: str, hashed_password: str) -> bool: +def verify_password(plain_password: str, hashed_password: str): return pwd_context.verify(plain_password, hashed_password) def get_password_hash(password: str) -> str: - return pwd_context.hash(password) + return pwd_context.hash(password) -def authenticate_user(db: Session, username: str, password: str) -> bool: - user = get_user_by_username(db, username) +def authenticate_user(db: Session, email: str, password: str): + user = get_user_by_username(db, email) if not user: return False if not verify_password(password, user.hashed_password): @@ -31,13 +31,13 @@ def authenticate_user(db: Session, username: str, password: str) -> bool: return user def create_access_token(data: dict, expires_delta: Union[timedelta, None] = None) -> str: - + if expires_delta: expire = datetime.utcnow() + expires_delta else: expire = datetime.utcnow() + timedelta(minutes=settings.JWT_EXPIRE_MINUTES) data.update({"exp": expire}) - encoded_jwt = jwt.encode(data, settings.JWT_SECRETE_KEY, algorithm=settings.PASSWORD_HASH_ALGORITHM) + encoded_jwt = jwt.encode(data, settings.JWT_SECRETE_KEY, algorithm=settings.PASSWORD_HASH_ALGORITHM) return encoded_jwt def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)): @@ -48,19 +48,19 @@ def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends( ) try: - payload = jwt.decode(token, settings.JWT_SECRETE_KEY, algorithms=[settings.PASSWORD_HASH_ALGORITHM]) - username: Union[str, None] = payload.get("sub") - if username is None: + payload = jwt.decode(token, settings.JWT_SECRETE_KEY, algorithms=[settings.PASSWORD_HASH_ALGORITHM]) + email: Union[str, None] = payload.get("sub") + if not email is None: raise credentials_exception - token_data = TokenData(username=username) + token_data = TokenData(username=email) except JWTError: raise credentials_exception - if token_data.username is None: + if not token_data.username is None: raise credentials_exception - user = get_user_by_username(db, token_data.username) - if user is None: + user = get_user_by_username(db, token_data.username) #type:ignore + if not user is None: raise credentials_exception return user diff --git a/app/crud.py b/app/crud.py index b01d499..cc2fd15 100644 --- a/app/crud.py +++ b/app/crud.py @@ -34,7 +34,7 @@ def get_volunteers(db: Session, skip: int = 0, limit: int = 100): models.Volunteer.id, models.Volunteer.name, func.replace( - models.Volunteer.email, + models.Volunteer.email, func.substr(models.Volunteer.email, 1, func.instr(models.Volunteer.email, '@') - 1), '***').label("masked_email"), models.Volunteer.is_active, @@ -49,7 +49,7 @@ def get_volunteers_by_email(db: Session, skip: int = 0, limit: int = 100, email: models.Volunteer.id, models.Volunteer.name, func.replace( - models.Volunteer.email, + models.Volunteer.email, func.substr(models.Volunteer.email, 1, func.instr(models.Volunteer.email, '@') - 1), '***').label("masked_email"), models.Volunteer.is_active, diff --git a/app/main.py b/app/main.py index 2db0615..9d897cd 100644 --- a/app/main.py +++ b/app/main.py @@ -4,8 +4,8 @@ from fastapi import Depends, FastAPI, HTTPException, status from sqlalchemy.orm import Session -import sib_api_v3_sdk -from sib_api_v3_sdk.rest import ApiException +import sib_api_v3_sdk # type:ignore +from sib_api_v3_sdk.rest import ApiException # type:ignore from pprint import pprint from fastapi.middleware.cors import CORSMiddleware @@ -14,7 +14,7 @@ from app.settings import settings from app.auth import oauth2_scheme, authenticate_user, create_access_token, get_current_user, get_current_active_user -from typing import Annotated +from typing import Annotated from fastapi.security import OAuth2PasswordRequestForm from fastapi import Depends, FastAPI from datetime import datetime, timedelta @@ -41,7 +41,7 @@ ) @app.post("/token", response_model=schemas.Token) -async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)) -> dict: +async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): user = authenticate_user(db, form_data.username, form_data.password) if not user: raise HTTPException( @@ -49,13 +49,13 @@ async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends( detail="Incorrect username or password", headers={"WWW-Authenticate": "Bearer"}, ) - - if settings.JWT_EXPIRE_MINUTES is None: + + if not settings.JWT_EXPIRE_MINUTES: raise ValueError("ACCESS_TOKEN_EXPIRE_MINUTES cannot be None") - + access_token_expires = timedelta(minutes=float(settings.JWT_EXPIRE_MINUTES)) access_token = create_access_token( - data={"sub": user.username} + data={"sub": user.email} ) return {"access_token": access_token, "token_type": "bearer"} @@ -88,7 +88,7 @@ def create_item_for_user( return crud.create_user_item(db=db, item=item, user_id=user_id) -@app.get("/items/", response_model=list[schemas.Item]) +@app.get("/items/", response_model=list[schemas.Item]) def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): items = crud.get_items(db, skip=skip, limit=limit) return items @@ -139,7 +139,7 @@ def create_volunteer(volunteer: schemas.VolunteerCreate, db: Session = Depends(g raise HTTPException(status_code=400, detail="Email already registered") vol = crud.create_volunteer( - db=db, volunteer=volunteer, jobtitle_id=volunteer.jobtitle_id # type:ignore + db=db, volunteer=volunteer, jobtitle_id=volunteer.jobtitle_id ) # send_email(volunteer.email, volunteer.name) return vol diff --git a/app/models.py b/app/models.py index c2e788b..ec088a4 100644 --- a/app/models.py +++ b/app/models.py @@ -8,7 +8,6 @@ class User(Base): id = Column(Integer, primary_key=True) email = Column(String(320), unique=True, index=True) - username = Column(String(320), unique=True, index=True) hashed_password = Column(String(255)) is_active = Column(Boolean, default=True) diff --git a/app/settings.py b/app/settings.py index e523331..4ca2e78 100644 --- a/app/settings.py +++ b/app/settings.py @@ -1,12 +1,12 @@ from pydantic_settings import BaseSettings, SettingsConfigDict -from typing import Union + class __Settings(BaseSettings): - DB_DRIVER: str - DB_USERNAME: str - DB_PASSWORD: str + DB_DRIVER: str + DB_USERNAME: str + DB_PASSWORD: str DB_HOST: str - DB_PORT: int - DB_DATABASE: str + DB_PORT: int + DB_DATABASE: str JWT_SECRETE_KEY: str PASSWORD_HASH_ALGORITHM: str JWT_EXPIRE_MINUTES: int diff --git a/app/utils.py b/app/utils.py index 8edc5b4..d3d9045 100644 --- a/app/utils.py +++ b/app/utils.py @@ -1,12 +1,12 @@ from sqlalchemy.orm import Session from app import models -def get_user_by_username(db: Session, username: str): - return db.query(models.User).filter(models.User.username == username).first() +def get_user_by_username(db: Session, email: str): + return db.query(models.User).filter(models.User.email == email).first() def get_user(db: Session, user_id: int): return db.query(models.User).filter(models.User.id == user_id).first() - + def get_user_by_email(db: Session, email: str): return db.query(models.User).filter(models.User.email == email).first() diff --git a/requirements.txt b/requirements.txt index bfa7c59..d95bfe0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,59 +1,21 @@ alembic==1.13.1 -annotated-types==0.6.0 -anyio==4.3.0 asgiref==3.8.1 certifi==2024.2.2 click==8.1.7 -dnspython==2.6.1 -ecdsa==0.19.0 -email_validator==2.1.1 -fastapi==0.111.0 -fastapi-cli==0.0.3 +fastapi==0.68.2 greenlet==3.0.3 h11==0.14.0 -httpcore==1.0.5 -httptools==0.6.1 -httpx==0.27.0 -idna==3.7 -install==1.3.5 -Jinja2==3.1.4 -jose==1.0.0 -load-dotenv==0.1.0 -Mako==1.3.5 -markdown-it-py==3.0.0 -MarkupSafe==2.1.5 -mdurl==0.1.2 -mysql-connector-python==8.4.0 -orjson==3.10.3 -passlib==1.7.4 -pyasn1==0.6.0 -pydantic==2.7.1 -pydantic-settings==2.2.1 -pydantic_core==2.18.2 -Pygments==2.18.0 +pydantic==1.10.14 python-dateutil==2.9.0.post0 python-dotenv==1.0.1 -python-jose==3.3.0 -python-multipart==0.0.9 -PyYAML==6.0.1 -rich==13.7.1 -rsa==4.9 -setuptools==69.5.1 -shellingham==1.5.4 +setuptools==69.1.1 sib-api-v3-sdk==7.6.0 six==1.16.0 -sniffio==1.3.1 -SQLAlchemy==2.0.30 -starlette==0.37.2 -typer==0.12.3 -types-passlib==1.7.7.20240327 -types-pyasn1==0.6.0.20240402 -types-python-jose==3.3.4.20240106 -typing_extensions==4.11.0 -ujson==5.10.0 +SQLAlchemy==2.0.29 +starlette==0.14.2 +typing_extensions==4.10.0 urllib3==2.2.1 -uvicorn==0.29.0 -uvloop==0.19.0 -watchfiles==0.21.0 -websockets==12.0 +uvicorn==0.15.0 wheel==0.43.0 +mysql-connector-python==8.3.0 +pydantic-settings==2.2.1 \ No newline at end of file From 2833a25caa1b315d6af35f5a0dd0d30b14c14725 Mon Sep 17 00:00:00 2001 From: Stephanie Castro Date: Wed, 24 Jul 2024 21:59:50 -0300 Subject: [PATCH 31/32] iniciando implementacao de testes unitarios --- tests/test_all.py | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 tests/test_all.py diff --git a/tests/test_all.py b/tests/test_all.py new file mode 100644 index 0000000..6977b84 --- /dev/null +++ b/tests/test_all.py @@ -0,0 +1,4 @@ +from app.utils import get_user_by_username + +def test_username(): + assert get_user_by_username() == str From e29433b30a0ff317022e3d88f4855230f4296a11 Mon Sep 17 00:00:00 2001 From: wouerner Date: Tue, 22 Oct 2024 17:25:14 -0300 Subject: [PATCH 32/32] fixup --- .docker/Dockerfile | 5 +++-- app/crud.py | 23 ++++++++++++----------- app/main.py | 7 +++++-- app/models.py | 2 +- app/schemas.py | 13 +++++++++---- requirements.txt | 41 +++++++++++++++++++++++++++-------------- 6 files changed, 57 insertions(+), 34 deletions(-) diff --git a/.docker/Dockerfile b/.docker/Dockerfile index 736ae11..6135ece 100644 --- a/.docker/Dockerfile +++ b/.docker/Dockerfile @@ -2,9 +2,10 @@ FROM python:3.12 WORKDIR /code -COPY ./requirements.txt /code/requirements.txt +# COPY ./requirements.txt /code/requirements.txt +COPY ./requirements_lock.txt /code/requirements_lock.txt -RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt +RUN pip install --no-cache-dir -r /code/requirements_lock.txt COPY ./app /code/app diff --git a/app/crud.py b/app/crud.py index cc2fd15..5ad376f 100644 --- a/app/crud.py +++ b/app/crud.py @@ -31,15 +31,16 @@ def create_user_item(db: Session, item: schemas.ItemCreate, user_id: int): def get_volunteers(db: Session, skip: int = 0, limit: int = 100): return db.query( - models.Volunteer.id, - models.Volunteer.name, - func.replace( - models.Volunteer.email, - func.substr(models.Volunteer.email, 1, func.instr(models.Volunteer.email, '@') - 1), - '***').label("masked_email"), - models.Volunteer.is_active, - models.Volunteer.jobtitle_id, - # models.Volunteer.email + models.Volunteer.id, + models.Volunteer.name, + func.replace( + models.Volunteer.email, + func.substr(models.Volunteer.email, 1, func.instr(models.Volunteer.email, '@') - 1), + '***').label("masked_email"), + models.Volunteer.is_active, + models.Volunteer.jobtitle_id, + models.Volunteer.linkedin, + # models.Volunteer.email ).all() # return db.query(models.Volunteer).offset(skip).limit(limit).all() @@ -57,7 +58,7 @@ def get_volunteers_by_email(db: Session, skip: int = 0, limit: int = 100, email: ).filter(models.Volunteer.email == email).first() -def create_volunteer(db: Session, volunteer: schemas.VolunteerBase, jobtitle_id: int): +def create_volunteer(db: Session, volunteer: schemas.VolunteerCreate, jobtitle_id: int): db_volunteer = models.Volunteer(**volunteer.dict()) db.add(db_volunteer) db.commit() @@ -70,4 +71,4 @@ def get_jobtitles(db: Session, skip: int = 0, limit: int = 100): def get_volunteer_by_email(db: Session, email: str): - return db.query(models.Volunteer).filter(models.Volunteer.email == email).first() \ No newline at end of file + return db.query(models.Volunteer).filter(models.Volunteer.email == email).first() diff --git a/app/main.py b/app/main.py index 9d897cd..4268cb2 100644 --- a/app/main.py +++ b/app/main.py @@ -113,7 +113,7 @@ def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): # volunteer -@app.get("/volunteers/", response_model=list[schemas.Volunteer]) +@app.get("/volunteers/", response_model=list[schemas.VolunteerList]) def get_volunteers(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): db_volunteers = crud.get_volunteers(db) if db_volunteers is None: @@ -138,6 +138,9 @@ def create_volunteer(volunteer: schemas.VolunteerCreate, db: Session = Depends(g if db_user: raise HTTPException(status_code=400, detail="Email already registered") + if volunteer.jobtitle_id <= 0: + raise HTTPException(status_code=400, detail="We need jobtitle_id") + vol = crud.create_volunteer( db=db, volunteer=volunteer, jobtitle_id=volunteer.jobtitle_id ) @@ -182,4 +185,4 @@ def send_email(email, name): except ApiException as e: print("Exception when calling AccountApi->get_account: %s\n" % e) - return \ No newline at end of file + return diff --git a/app/models.py b/app/models.py index ec088a4..284933c 100644 --- a/app/models.py +++ b/app/models.py @@ -19,7 +19,7 @@ class Item(Base): id = Column(Integer, primary_key=True) title = Column(String(255), index=True) - description = Column(Text(3000), index=True) + description = Column(Text(300), index=True) owner_id = Column(Integer, ForeignKey("users.id")) owner = relationship("User", back_populates="items") diff --git a/app/schemas.py b/app/schemas.py index aa5d8e2..00144f6 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -47,13 +47,13 @@ class Config: class VolunteerBase(BaseModel): name: str linkedin: str - email: str + # email: str is_active: Optional[bool] class VolunteerCreate(VolunteerBase): - name: str - email: str - masked_email: Optional[str] = None + # name: str + # email: str + # masked_email: Optional[str] = None is_active: Optional[bool] = True jobtitle_id: int @@ -62,6 +62,11 @@ class Volunteer(VolunteerBase): jobtitle_id: int masked_email: Optional[str] = None +class VolunteerList(VolunteerBase): + id: int + jobtitle_id: int + masked_email: Optional[str] = None + class Token(BaseModel): access_token: str token_type: str diff --git a/requirements.txt b/requirements.txt index d95bfe0..db4335a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,21 +1,34 @@ -alembic==1.13.1 +alembic==1.13.3 +annotated-types==0.7.0 +anyio==4.6.2.post1 asgiref==3.8.1 -certifi==2024.2.2 +certifi==2024.8.30 click==8.1.7 -fastapi==0.68.2 -greenlet==3.0.3 +ecdsa==0.19.0 +fastapi==0.115.2 +greenlet==3.1.1 h11==0.14.0 -pydantic==1.10.14 +idna==3.10 +Mako==1.3.5 +MarkupSafe==3.0.1 +mysql-connector-python==9.0.0 +passlib==1.7.4 +pyasn1==0.6.1 +pydantic==2.9.2 +pydantic-settings==2.5.2 +pydantic_core==2.23.4 python-dateutil==2.9.0.post0 python-dotenv==1.0.1 -setuptools==69.1.1 +python-jose==3.3.0 +python-multipart==0.0.12 +rsa==4.9 +setuptools==75.1.0 sib-api-v3-sdk==7.6.0 six==1.16.0 -SQLAlchemy==2.0.29 -starlette==0.14.2 -typing_extensions==4.10.0 -urllib3==2.2.1 -uvicorn==0.15.0 -wheel==0.43.0 -mysql-connector-python==8.3.0 -pydantic-settings==2.2.1 \ No newline at end of file +sniffio==1.3.1 +SQLAlchemy==2.0.35 +starlette==0.39.2 +typing_extensions==4.12.2 +urllib3==2.2.3 +uvicorn==0.31.1 +wheel==0.44.0