Skip to content

Commit 20b456d

Browse files
committed
Merge branch 'main' into feature/#29
2 parents 307696b + 6f8507c commit 20b456d

File tree

9 files changed

+377
-95
lines changed

9 files changed

+377
-95
lines changed

app/routers/authentication.py

Lines changed: 68 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
from typing import Annotated
2+
3+
import jwt
14
from fastapi import APIRouter, Depends, HTTPException, Request, status
25
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
3-
from services.database.orm.community import get_community_by_username
6+
from jwt.exceptions import InvalidTokenError
47
from sqlmodel.ext.asyncio.session import AsyncSession
58

6-
from app.schemas import Token, TokenPayload
9+
from app.schemas import Community, Token, TokenPayload
710
from app.services import auth
811
from app.services.database.models import Community as DBCommunity
912
from app.services.database.orm.community import get_community_by_username
@@ -12,17 +15,58 @@
1215

1316

1417
def setup():
15-
router = APIRouter(prefix='/authentication', tags=['authentication'])
16-
async def authenticate_community( request: Request , username: str, password: str):
17-
# Valida se o usuário existe e se a senha está correta
18-
session: AsyncSession = request.app.db_session_factory
19-
found_community = await get_community_by_username(
20-
username=username,
21-
session= session
22-
)
23-
if not found_community or not auth.verify_password(password, found_community.password):
18+
router = APIRouter(prefix="/authentication", tags=["authentication"])
19+
20+
async def authenticate_community(
21+
request: Request, username: str, password: str
22+
):
23+
# Valida se o usuário existe e se a senha está correta
24+
session: AsyncSession = request.app.db_session_factory
25+
found_community = await get_community_by_username(
26+
username=username, session=session
27+
)
28+
if not found_community or not auth.verify_password(
29+
password, found_community.password
30+
):
2431
return None
25-
return found_community
32+
return found_community
33+
34+
# Teste
35+
async def get_current_community(
36+
request: Request,
37+
token: Annotated[str, Depends(oauth2_scheme)],
38+
) -> DBCommunity:
39+
credentials_exception = HTTPException(
40+
status_code=status.HTTP_401_UNAUTHORIZED,
41+
detail="Could not validate credentials",
42+
headers={"WWW-Authenticate": "Bearer"},
43+
)
44+
45+
try:
46+
payload = jwt.decode(
47+
token, auth.SECRET_KEY, algorithms=[auth.ALGORITHM]
48+
)
49+
username = payload.get("sub")
50+
if username is None:
51+
raise credentials_exception
52+
token_data = TokenPayload(username=username)
53+
except InvalidTokenError:
54+
raise credentials_exception
55+
session: AsyncSession = request.app.db_session_factory
56+
community = await get_community_by_username(
57+
session=session, username=token_data.username
58+
)
59+
if community is None:
60+
raise credentials_exception
61+
62+
return community
63+
64+
async def get_current_active_community(
65+
current_user: Annotated[DBCommunity, Depends(get_current_community)],
66+
) -> DBCommunity:
67+
# A função simplesmente retorna o usuário.
68+
# Pode ser estendido futuramente para verificar um status "ativo".
69+
return current_user
2670

2771
# Teste
2872

@@ -48,7 +92,9 @@ async def login_for_access_token(
4892
request: Request, form_data: OAuth2PasswordRequestForm = Depends()
4993
):
5094
# Rota de login: valida credenciais e retorna token JWT
51-
community = await authenticate_community( request, form_data.username, form_data.password)
95+
community = await authenticate_community(
96+
request, form_data.username, form_data.password
97+
)
5298
if not community:
5399
raise HTTPException(
54100
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -62,4 +108,13 @@ async def login_for_access_token(
62108
"expires_in": expires_in,
63109
}
64110

111+
@router.get("/me", response_model=Community)
112+
async def read_community_me(
113+
current_community: Annotated[
114+
DBCommunity, Depends(get_current_active_community)
115+
],
116+
):
117+
# Rota para obter informações do usuário autenticado
118+
return current_community
119+
65120
return router # Retorna o router configurado com as rotas de autenticação

app/routers/news/routes.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1-
from fastapi import APIRouter, status
1+
from fastapi import APIRouter, Request, status
22
from pydantic import BaseModel
3+
from services.database.orm.news import get_news_by_query_params
34

45

56
class NewsPostResponse(BaseModel):
67
status: str = "News Criada"
78

89

10+
class NewsGetResponse(BaseModel):
11+
status: str = "Lista de News Obtida"
12+
news_list: list = []
13+
14+
915
def setup():
1016
router = APIRouter(prefix="/news", tags=["news"])
1117

@@ -16,10 +22,30 @@ def setup():
1622
summary="News endpoint",
1723
description="Creates news and returns a confirmation message",
1824
)
19-
async def news():
25+
async def post_news():
2026
"""
2127
News endpoint that creates news and returns a confirmation message.
2228
"""
2329
return NewsPostResponse()
2430

31+
@router.get(
32+
"",
33+
response_model=NewsGetResponse,
34+
status_code=status.HTTP_200_OK,
35+
summary="Get News",
36+
description="Retrieves news filtered by user and query params",
37+
)
38+
async def get_news(request: Request):
39+
"""
40+
Get News endpoint that retrieves news filtered by user and query params.
41+
"""
42+
news_list = await get_news_by_query_params(
43+
session=request.app.db_session_factory,
44+
id=request.query_params.get("id"),
45+
user_email=request.headers.get("user-email"),
46+
category=request.query_params.get("category"),
47+
tags=request.query_params.get("tags"),
48+
)
49+
return NewsGetResponse(news_list=news_list)
50+
2551
return router

app/services/auth.py

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,36 @@
1-
#from passlib.context import CryptContext
2-
import bcrypt
1+
# from passlib.context import CryptContext
2+
import os
33
from datetime import datetime, timedelta, timezone
4-
from app.schemas import TokenPayload
4+
5+
import bcrypt
56
import jwt
6-
import os
7+
8+
from app.schemas import TokenPayload
79

810
SECRET_KEY = os.getenv("SECRET_KEY", "default_fallback_key")
911
ALGORITHM = os.getenv("ALGORITHM", "HS256")
1012
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", 20))
1113

12-
def verify_password(plain, hashed):
14+
15+
def verify_password(plain, hashed):
1316
# Verifica se a senha passada bate com a hash da comunidade
1417
return bcrypt.checkpw(
1518
bytes(plain, encoding="utf-8"),
1619
hashed,
1720
)
1821

19-
def hash_password(password):
22+
23+
def hash_password(password):
2024
# Retorna a senha em hash para salvar no banco de dados
2125
return bcrypt.hashpw(
2226
bytes(password, encoding="utf-8"),
2327
bcrypt.gensalt(),
2428
)
2529

2630

27-
28-
#pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
29-
30-
#def verify_password(plain, hashed):
31-
# # Verifica se a senha passada bate com a hash da comunidade
32-
# return pwd_context.verify(plain, hashed)
33-
#
34-
#def hash_password(password):
35-
# # Retorna a senha em hash para salvar no banco de dados
36-
# return pwd_context.hash(password)
37-
38-
def create_access_token(data: TokenPayload, expires_delta: timedelta | None = None):
31+
def create_access_token(
32+
data: TokenPayload, expires_delta: timedelta | None = None
33+
):
3934
"""
4035
Gera um token JWT contendo os dados do usuário (payload) e uma data de expiração.
4136
JWT specification says that there's a key sub (subject) that should be used to identify the user.
Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,24 @@
11
from typing import Optional
2+
23
from sqlmodel import select
34
from sqlmodel.ext.asyncio.session import AsyncSession
5+
46
from app.services.database.models import Community
57

68

79
async def get_community_by_username(
810
username: str,
9-
session: AsyncSession,) -> Optional[Community]:
11+
session: AsyncSession,
12+
) -> Optional[Community]:
1013
"""
1114
Busca e retorna um membro da comunidade pelo nome de usuário.
1215
Retorna None se o usuário não for encontrado.
1316
"""
1417
# Cria a declaração SQL para buscar a comunidade pelo nome de usuário
1518
statement = select(Community).where(Community.username == username)
16-
19+
1720
# Executa a declaração na sessão e retorna o primeiro resultado
1821
result = await session.exec(statement)
1922
community = result.first()
20-
21-
return community
23+
24+
return community

app/services/database/orm/news.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from typing import Optional
2+
3+
from services.database.models import News
4+
from sqlmodel import select
5+
from sqlmodel.ext.asyncio.session import AsyncSession
6+
7+
8+
async def get_news_by_query_params(
9+
session: AsyncSession,
10+
user_email: Optional[str] = None,
11+
category: Optional[str] = None,
12+
tags: Optional[str] = None,
13+
id: Optional[str] = None,
14+
) -> list[News]:
15+
filters = []
16+
if user_email is not None:
17+
filters.append(News.user_email == user_email)
18+
if category is not None:
19+
filters.append(News.category == category)
20+
if tags is not None:
21+
filters.append(News.tags == tags)
22+
if id is not None:
23+
filters.append(News.id == id)
24+
25+
print("user_email:", user_email)
26+
print("Filters:", filters)
27+
28+
statement = select(News).where(*filters)
29+
results = await session.exec(statement)
30+
return results.all()

tests/conftest.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
from collections.abc import AsyncGenerator, Generator
2-
from unittest.mock import AsyncMock
1+
from collections.abc import AsyncGenerator
32

43
import pytest
54
import pytest_asyncio
@@ -10,7 +9,8 @@
109
from sqlmodel.ext.asyncio.session import AsyncSession
1110

1211
from app.main import app
13-
#from app.main import get_db_session
12+
13+
# from app.main import get_db_session
1414

1515
# Importar todos os modelos SQLModel a serem usados
1616
# (necessários para as validações de modelo)
@@ -40,7 +40,9 @@ async def get_db_session_test() -> AsyncGenerator[AsyncSession, None]:
4040
async def setup_database():
4141
async with test_engine.begin() as conn:
4242
await conn.run_sync(SQLModel.metadata.create_all)
43-
yield test_engine
43+
yield test_engine
44+
async with test_engine.begin() as conn:
45+
await conn.run_sync(SQLModel.metadata.drop_all)
4446

4547

4648
@pytest_asyncio.fixture(scope="function")
@@ -50,10 +52,11 @@ async def session() -> AsyncGenerator[AsyncSession, None]:
5052
yield session
5153
await session.close()
5254

55+
5356
@pytest_asyncio.fixture
5457
async def test_app(session) -> FastAPI:
5558
mock_db_connection = session
56-
setattr(app, 'db_session_factory', mock_db_connection)
59+
setattr(app, "db_session_factory", mock_db_connection)
5760
return app
5861

5962

0 commit comments

Comments
 (0)