Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: ci-cd.yml
on:
push:
branches:
- dev

jobs:
build:

environment: API

runs-on: ubuntu-latest

steps:
- name: clone do repositorio
uses: actions/checkout@v5

- name: Configurar docker
uses: docker/setup-buildx-action@v3

- name: Construir imagem Docker
run: |
docker build -t crm:backend -f backend/Dockerfile .

- name: Login no Docker Hub
uses: docker/login-action@v3
with:
username: ${{secrets.DOCKER_USERNAME}}
password: ${{secrets.DOCKER_PASSWORD}}

- name: Deploy da imagem no docker hub
uses: docker/build-push-action@v5
with:
context: .
file: ./backend/Dockerfile
push: true
tags: kbrum/api-dockerfiles:latest
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,5 @@ cython_debug/
marimo/_static/
marimo/_lsp/
__marimo__/
.venv
.idea
16 changes: 16 additions & 0 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
FROM python:3.13.5
LABEL authors="Kayky"

WORKDIR /app

COPY pyproject.toml .
COPY uv.lock .

RUN pip install uv && uv sync

COPY backend .

EXPOSE 5000

CMD ["python", "routes.py"]

72 changes: 72 additions & 0 deletions backend/auth_conf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from datetime import datetime, timezone, timedelta

from fastapi import Depends, HTTPException
from jose import jwt, JWTError
from fastapi.security import OAuth2PasswordBearer
from passlib.context import CryptContext
import os

from sqlalchemy.orm import Session
from starlette import status

from backend.database import get_db_session, Usuario

# configuração do hash de senhas
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

# configuração do token JWT
SECRET_KEY = os.getenv("JWT_SECRET")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")

# função que faz o hash das senhas
def get_password_hash(password) -> str:
return pwd_context.hash(password)

# função que faz a verificação da senha para login
def verify_password(text_password: str, hashed_password: str) -> bool:
return pwd_context.verify(text_password, hashed_password)

# função usada para criar o token web
def create_access_token(data: dict, expires_delta: timedelta | None = None):

# Copia os dados necessarios
to_encode = data.copy()

# Define a data de expiração do token
if expires_delta:
# Se um tempo de expiração for fornecido (ex: 30 minutos),
# ele é adicionado ao tempo atual.
expire = datetime.now(timezone.utc) + expires_delta
else:
# Se nenhum tempo for fornecido, a expiração padrão é 15 minutos.
expire = datetime.now(timezone.utc) + timedelta(minutes=15)

# Adiciona a data de expiração aos dados
to_encode.update({"exp": expire})

# Codifica o token
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)

# Retorna o token codificado
return encoded_jwt

def get_current_user(db: Session = Depends(get_db_session), token: str = Depends(oauth2_scheme)):
try:
# Decodifica o token usando a chave secreta
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
# Extrai o nome de usuário do payload do token
username: str = payload.get("sub")
if username is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token inválido")
except JWTError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token inválido")

# Busca o usuário no banco de dados
user = db.query(Usuario).filter(Usuario.nome_usuario == username).first()
if user is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Usuário não encontrado")

return user
44 changes: 44 additions & 0 deletions backend/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from sqlalchemy import String, text, create_engine
from sqlalchemy.orm import declarative_base, Mapped, mapped_column, sessionmaker
from datetime import datetime
from dotenv import load_dotenv
import os

load_dotenv()

# A base é a classe que os seus modelos vão herdar
Base = declarative_base()

DATABASE_URI = os.getenv("DATABASE_URI")

engine = create_engine(DATABASE_URI, echo=True)

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

def get_db_session():
db = SessionLocal()
try:
yield db
finally:
db.close()

class Usuario(Base):

__tablename__ = 'usuarios'

# Mapeamento das colunas da sua tabela
id_usuario: Mapped[int] = mapped_column(primary_key=True) #id de usuario e primary key
nome_completo: Mapped[str] = mapped_column(String(100)) # nome completo
nome_usuario: Mapped[str] = mapped_column(String(50), unique=True) # nome de usuario para login
hash_senha: Mapped[str] = mapped_column(String(255)) # senha ja com hash
setor: Mapped[str | None] = mapped_column(String(50), nullable=True) # setor (opcinal)
criado_em: Mapped[datetime] = mapped_column(
server_default=text("CURRENT_TIMESTAMP") # quando foi criado
)
atualizado_em: Mapped[datetime] = mapped_column(
server_default=text("CURRENT_TIMESTAMP"), # quando foi modificado
onupdate=text("CURRENT_TIMESTAMP")
)

def __repr__(self) -> str:
return f"Usuario(id={self.id_usuario}, nome_usuario='{self.nome_usuario}')"
68 changes: 68 additions & 0 deletions backend/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from datetime import timedelta

import uvicorn
from fastapi import FastAPI, Depends, HTTPException, status
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from fastapi.security import OAuth2PasswordRequestForm

from auth_conf import get_password_hash, verify_password, create_access_token, ACCESS_TOKEN_EXPIRE_MINUTES, \
get_current_user
from database import get_db_session, Usuario
import schemas

# configuração do hash de senhas
app = FastAPI()

@app.get("/") # home page da api para apresentação
def root():
return {"message": "Bem vindo a API de autenticação feita com kayky azevedo, Use /login, /cadastro, ou /showUser para interagir "}


@app.post("/cadastro") #endpoint de cadastro
def cadastro(user: schemas.UserCreate, db: Session = Depends(get_db_session)):
try:
hashed_password = get_password_hash(user.password) # faz o hash
db_user = Usuario(nome_usuario=user.username,
nome_completo=user.full_name,
hash_senha=hashed_password,) # a variavel db_user recebe a classe Usuario
# que é usada na comunicação com o banco
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
except IntegrityError:
db.rollback()
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Username already exists"
)

# rota de login
@app.post("/login")
def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db_session)):
# Buscar o usuário no banco de dados
user = db.query(Usuario).filter(Usuario.nome_usuario == form_data.username).first()

# Verifica se o usuário existe e se a senha está correta
if not user or not verify_password(form_data.password, user.hash_senha):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Nome de usuário ou senha incorretos."
)

# Geraração do token de acesso
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": user.nome_usuario}, expires_delta=access_token_expires
)

# 4. Retornar o token
return {"access_token": access_token, "token_type": "bearer"}

@app.get("/showUser") # Rota protaegida
def show_user(current_user: Usuario = Depends(get_current_user)):
return {"user_info": current_user}

if __name__ == "__main__":
uvicorn.run(app, port=8000)
17 changes: 17 additions & 0 deletions backend/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from pydantic import BaseModel
from typing import Optional

# classes usadas para validação no arquivo routes.py

class UserCreate(BaseModel):
username: str
password: str
full_name: Optional[str] = None

class UserLogin(BaseModel):
username: str
password: str

class Token(BaseModel):
access_token: str
token_type: str = "bearer"
30 changes: 30 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
services:
postgres_db:
container_name: database
image: postgres:14
ports:
- "5432:5432"
dns:
- 8.8.8.8
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
CRM:
env_file:
- .env

backend-conteiner:
container_name: backend
build:
context: backend
dockerfile: Dockerfile
ports:
- "5000:5000"
networks:
CRM:

volumes:
postgres_data:

networks:
CRM:
14 changes: 14 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[project]
name = "api_autenticacao"
version = "0.1.0"
description = "API de autenticação de usuários para portfólio"
dependencies = [
"fastapi",
"uvicorn[standard]",
"passlib[bcrypt]",
"python-jose[cryptography]",
"SQLAlchemy",
"psycopg2-binary",
"python-dotenv>=1.1.1",
"python-multipart>=0.0.20",
]
Loading