Skip to content

Feature/#14 #21

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f8e055d
Create Subscription schema
Diego-Espindola Jul 2, 2025
5967821
Create main.py
Diego-Espindola Jul 2, 2025
3422f24
Correção Lista tags e mensagem de erro
Diego-Espindola Jul 3, 2025
d64b61a
Update schemas.py
Diego-Espindola Jul 8, 2025
ef0548e
Merge remote-tracking branch 'upstream/main' into diego
Diego-Espindola Jul 17, 2025
15edfed
Delete schemas.py
Diego-Espindola Jul 17, 2025
955338a
feat(schemas): Adicionar classe subscription aos schemas
Diego-Espindola Jul 17, 2025
9003ea9
Feat(schemas): Criar classe Subscription - pydantic
Diego-Espindola Jul 17, 2025
a280ecf
Delete main.py
Diego-Espindola Jul 17, 2025
61cb8db
feat(auth): autenticação OAuth2 + JWT com fake DB para testes
Diego-Espindola Jul 17, 2025
c8b379f
feat(authentication): adicionar rotas com jwt e community
Diego-Espindola Jul 19, 2025
1d71f58
refactor(schemas): renomear para community e atualizar o TokenPayload…
Diego-Espindola Jul 19, 2025
ee5f76f
fix(auth): melhorar create_access_token para aceitar o TokenPayload m…
Diego-Espindola Jul 19, 2025
06b000c
chore(database): fake user class
Diego-Espindola Jul 19, 2025
148bdd6
feat: Criar endpoint de login
Diego-Espindola Jul 22, 2025
a193715
feat: Criar endpoint de login
Diego-Espindola Jul 22, 2025
241c5f2
Merge remote-tracking branch 'upstream/main' into diego
Diego-Espindola Jul 22, 2025
df78a50
feat: criar endpoint de login
Diego-Espindola Jul 22, 2025
2c80deb
feat(db): estrutura inicial do banco com modelo Community
Diego-Espindola Jul 26, 2025
a44a32a
feat(db): adiciona lógica de seed com comunidade exemplo
Diego-Espindola Jul 26, 2025
952bc9f
feat(auth): integra autenticação à base de dados usando SQLModel
Diego-Espindola Jul 26, 2025
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
59 changes: 59 additions & 0 deletions app/routers/authentication.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from app.services import auth
from app.services.database.community import get_community_by_username # Atualizar após banco de dados
from app.schemas import Token, TokenPayload, Community
import jwt
from jwt.exceptions import InvalidTokenError

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/authentication/token")

def setup():
router = APIRouter(prefix='/authentication', tags=['authentication'])

async def authenticate_community(username: str, password: str):
# Valida se o usuário existe e se a senha está correta
db_user = await get_community_by_username(username)
if not db_user or not auth.verify_password(password, db_user.password):
return None
return db_user

@router.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
# Rota de login: valida credenciais e retorna token JWT
community = await authenticate_community(form_data.username, form_data.password)
if not community:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Credenciais inválidas"
)
# Community ex: email='[email protected]' id=1 username='alice' full_name="Alice in the Maravilha's world" password='$2b$12$cA3fzLrRCmLp1aKn6ULhF.sQfaPQ70EoJU3Q0Szf6e4/YaVsKAAHS'
payload = TokenPayload(username=community.username)
token, expires_in = auth.create_access_token(data=payload)
return {
"access_token": token,
"token_type": "Bearer",
"expires_in": expires_in
}


@router.get("/me", response_model=Community)
async def get_current_community(token: str = Depends(oauth2_scheme)):
# Rota protegida: retorna dados do usuário atual com base no token
creds_exc = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token inválido",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload_dict = jwt.decode(token, auth.SECRET_KEY, algorithms=[auth.ALGORITHM])
payload = TokenPayload(**payload_dict)
except (InvalidTokenError, ValueError):
raise creds_exc

community = get_community_by_username(payload.sub)
if not community:
raise creds_exc
return community

return router # Retorna o router configurado com as rotas de autenticação
4 changes: 4 additions & 0 deletions app/routers/router.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
from fastapi import APIRouter
from app.routers.healthcheck.routes import setup as healthcheck_router_setup
from app.routers.authentication import setup as authentication_router_setup


def setup_router() -> APIRouter:
router = APIRouter()
router.include_router(healthcheck_router_setup(), prefix='')
router.include_router(authentication_router_setup(), prefix='')

return router
35 changes: 34 additions & 1 deletion app/schemas.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,39 @@
from pydantic import BaseModel, HttpUrl
from datetime import datetime
from typing import List
from enum import Enum


## User Class
class Community(BaseModel):
username: str
full_name: str
email: str

class CommunityInDB(Community):
password: str

class Token(BaseModel):
access_token: str
token_type: str
expires_in: int

class TokenPayload(BaseModel):
username: str

## Subscription Class
class TagEnum(str, Enum):
bug_fix = "bug_fix"
update = "update"
deprecate = "deprecate"
new_feature = "new_feature"
security_fix = "security_fix"

class Subscription(BaseModel):
tags: List[TagEnum]
libraries_list: List[str]

## News
class News(BaseModel):
description: str
tag: str
Expand All @@ -11,4 +44,4 @@ class Library(BaseModel):
logo: HttpUrl
version: str
release_date: datetime
release_doc_url: HttpUrl
release_doc_url: HttpUrl
40 changes: 40 additions & 0 deletions app/services/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from passlib.context import CryptContext
from datetime import datetime, timedelta, timezone
from app.schemas import TokenPayload
import jwt
import os

SECRET_KEY = os.getenv("SECRET_KEY", "default_fallback_key")
ALGORITHM = os.getenv("ALGORITHM", "HS256")
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", 20))

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def verify_password(plain, hashed):
# Verifica se a senha passada bate com a hash da comunidade
print(plain, hashed)
return pwd_context.verify(plain, hashed)

def get_password_hash(password):
# Retorna a senha em hash para salvar no banco de dados
return pwd_context.hash(password)

def create_access_token(data: TokenPayload, expires_delta: timedelta | None = None):
"""
Gera um token JWT contendo os dados do usuário (payload) e uma data de expiração.

Parâmetros:
- data (TokenPayload): Dicionário com os dados que serão codificados no token. Deve conter a chave 'sub' com o identificador do usuário.
- expires_delta (timedelta | None): Tempo até o token expirar. Se não fornecido, usará o padrão de 20 minutos.

Retorna:
- str: Token JWT assinado.
- int: tempo de expiração em segundos
"""
if not expires_delta:
expires_delta = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
expire = datetime.now(timezone.utc) + expires_delta
to_encode = {"sub": data.username, "exp": expire}
token = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)

return token, int(expires_delta.total_seconds())
12 changes: 12 additions & 0 deletions app/services/database/community.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# app/services/database/community.py
from sqlmodel import select
from sqlalchemy.exc import NoResultFound
from app.services.database.model.community_model import Community
from app.services.database.database import get_session

async def get_community_by_username(username: str):
async for session in get_session():
stmt = select(Community).where(Community.username == username)
result = await session.exec(stmt)
user = result.one_or_none()
return user
31 changes: 19 additions & 12 deletions app/services/database/database.py
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
# Crie a fábrica de sessões UMA VEZ no escopo global do módulo.
# Esta é a variável é injetada para obter sessões nas chamadas.
AsyncSessionLocal = sessionmaker(
engine, class_=AsyncSession, expire_on_commit=False, echo= True # expire_on_commit=False é importante!
engine, class_=AsyncSession, expire_on_commit=False # expire_on_commit=False é importante!
)

# --- Modelo de Teste Temporário (SQLModel) ---
Expand All @@ -44,21 +44,28 @@ def __repr__(self):
return f"<TestEntry(id={self.id}, message='{self.message}')>"


# --- Funções de Inicialização e Sessão do Banco de Dados ---
# --- Funções de Inicialização e Sessão do Banco de Dados ---
async def init_db():
"""
Inicializa o banco de dados:
1. Verifica se o arquivo do banco de dados existe.
2. Se não existir, cria o arquivo e todas as tabelas definidas nos modelos SQLModel nos imports e acima.
1. Cria todas as tabelas definidas (caso não existam).
2. Insere o usuário de teste 'alice' via seeder, se necessário.
"""
if not os.path.exists(DATABASE_FILE):
logger.info(f"Arquivo de banco de dados '{DATABASE_FILE}' não encontrado. Criando novo banco de dados e tabelas.")
async with engine.begin() as conn:
# SQLModel.metadata.create_all é síncrono e precisa ser executado via run_sync
await conn.run_sync(SQLModel.metadata.create_all)
logger.info("Tabelas criadas com sucesso.")
else:
logger.info(f"Arquivo de banco de dados '{DATABASE_FILE}' já existe. Conectando.")
logger.info("Inicializando banco de dados...")

# ✅ Importa os models para registrar no SQLModel.metadata
from app.services.database.model import community_model # importa o módulo inteiro, não só a classe

# 🔧 Garante que todas as tabelas (incluindo 'community') sejam criadas, se não existirem
async with engine.begin() as conn:
await conn.run_sync(SQLModel.metadata.create_all)

logger.info("Tabelas do banco de dados verificadas/criadas com sucesso.")

# ✅ Executa o seeder para inserir o usuário 'alice', se necessário
from app.services.database.seeder import insert_test_community
await insert_test_community()


async def get_session() -> AsyncGenerator[AsyncSession, None]:
"""
Expand Down
9 changes: 9 additions & 0 deletions app/services/database/model/community_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from sqlmodel import SQLModel, Field
from typing import Optional

class Community(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
username: str = Field(index=True, nullable=False, unique=True)
full_name: Optional[str] = None
email: Optional[str] = None
password: str # senha hashed
11 changes: 11 additions & 0 deletions app/services/database/run_seeder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# run_seeder.py -- para rodar manualmente
import asyncio
from app.services.database.seeder import insert_test_community
from app.services.database.database import init_db

async def main():
await init_db()
await insert_test_community()

if __name__ == "__main__":
asyncio.run(main())
25 changes: 25 additions & 0 deletions app/services/database/seeder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# app/services/database/seeder.py
# PARA INSERIR INFORMAÇÕES DE TESTES NO BANCO

async def insert_test_community():
from app.services.database.database import AsyncSessionLocal # import local para evitar circular import
from app.services.database.model.community_model import Community
from app.services.auth import get_password_hash
from sqlmodel import select


async with AsyncSessionLocal() as session:
result = await session.exec(select(Community).where(Community.username == "alice"))
if result.first():
return

user = Community(
username="alice",
full_name="Alice in the Maravilha's world",
email="[email protected]",
password=get_password_hash("secret123")
)
session.add(user)
await session.commit()
print("Usuário de teste 'alice' criado com sucesso.")

3 changes: 3 additions & 0 deletions docker-compose.yaml
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ services:
- PYTHONPATH=/server
- SQLITE_PATH=app/services/database/pynewsdb.db
- SQLITE_URL=sqlite+aiosqlite://
- SECRET_KEY=1a6c5f3b7d2e4a7fb68d0casd3f9a7b2d8c4e5f6a3b0d4e9c7a8f1b6d3c0a7f5e
- ALGORITHM=HS256
- ACCESS_TOKEN_EXPIRE_MINUTES=20
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/api/healthcheck"]
Expand Down
Binary file added pynewsdb.db
Binary file not shown.