Skip to content

Commit 5a86454

Browse files
committed
refactor: enhance authentication flow with async session management and add tests for token endpoint
1 parent c4e9f41 commit 5a86454

12 files changed

Lines changed: 186 additions & 86 deletions

File tree

app/main.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ async def get_db_session():
3232
async with app.db_session_factory() as session:
3333
yield session
3434

35-
36-
app.include_router(setup_router_v2(), prefix="/api")
35+
app.include_router(setup_router_v2(get_db_session), prefix="/api")
3736

3837
logger.info("PyNews Server Starter")

app/routers/authentication.py

Lines changed: 30 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,56 @@
11
from fastapi import APIRouter, Depends, HTTPException, status
22
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
3-
from app.services import auth
4-
from app.services.database.community import get_community_by_username # Atualizar após banco de dados
5-
from app.schemas import Token, TokenPayload, Community
3+
from sqlmodel.ext.asyncio.session import AsyncSession
64
import jwt
75
from jwt.exceptions import InvalidTokenError
86

7+
from app.services import auth
8+
from app.schemas import Token, TokenPayload, Community
9+
from app.services.database.models import Community as DBCommunity
10+
from app.services.database.community import get_community_by_username
11+
912
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/authentication/token")
1013

11-
def setup():
14+
def setup(get_db_session_dep):
1215
router = APIRouter(prefix='/authentication', tags=['authentication'])
13-
14-
async def authenticate_community(username: str, password: str):
16+
async def authenticate_community(username: str, password: str, session: AsyncSession = Depends(get_db_session_dep)):
1517
# Valida se o usuário existe e se a senha está correta
16-
db_user = await get_community_by_username(username)
17-
if not db_user or not auth.verify_password(password, db_user.password):
18+
found_community = await get_community_by_username(
19+
username=username,
20+
session=session
21+
)
22+
if not found_community or not auth.verify_password(password, found_community.password):
1823
return None
19-
return db_user
24+
return found_community
25+
26+
27+
#### Teste
28+
29+
@router.post("/create_commumity")
30+
async def create_community( session: AsyncSession = Depends(get_db_session_dep)):
31+
password = "123Asd!@#"
32+
hashed_password=auth.hash_password(password)
33+
community = DBCommunity(username="username", email="username@test.com", password=hashed_password)
34+
session.add(community)
35+
await session.commit()
36+
await session.refresh(community)
37+
return {'msg':'succes? '}
38+
#### Teste
2039

2140
@router.post("/token", response_model=Token)
22-
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
41+
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), session: AsyncSession = Depends(get_db_session_dep)) :
2342
# Rota de login: valida credenciais e retorna token JWT
24-
community = await authenticate_community(form_data.username, form_data.password)
43+
community = await authenticate_community(form_data.username, form_data.password, session)
2544
if not community:
2645
raise HTTPException(
2746
status_code=status.HTTP_401_UNAUTHORIZED,
2847
detail="Credenciais inválidas"
2948
)
30-
# Community ex: email='alice@example.com' id=1 username='alice' full_name="Alice in the Maravilha's world" password='$2b$12$cA3fzLrRCmLp1aKn6ULhF.sQfaPQ70EoJU3Q0Szf6e4/YaVsKAAHS'
3149
payload = TokenPayload(username=community.username)
3250
token, expires_in = auth.create_access_token(data=payload)
3351
return {
3452
"access_token": token,
3553
"token_type": "Bearer",
3654
"expires_in": expires_in
3755
}
38-
39-
40-
@router.get("/me", response_model=Community)
41-
async def get_current_community(token: str = Depends(oauth2_scheme)):
42-
# Rota protegida: retorna dados do usuário atual com base no token
43-
creds_exc = HTTPException(
44-
status_code=status.HTTP_401_UNAUTHORIZED,
45-
detail="Token inválido",
46-
headers={"WWW-Authenticate": "Bearer"},
47-
)
48-
try:
49-
payload_dict = jwt.decode(token, auth.SECRET_KEY, algorithms=[auth.ALGORITHM])
50-
payload = TokenPayload(**payload_dict)
51-
except ( InvalidTokenError ,ValueError):
52-
raise creds_exc
53-
54-
community = get_community_by_username(payload.sub)
55-
if not community:
56-
raise creds_exc
57-
return community
58-
5956
return router # Retorna o router configurado com as rotas de autenticação

app/routers/router.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
from app.routers.news.routes import setup as news_router_setup
55
from app.routers.authentication import setup as authentication_router_setup
66

7-
def setup_router() -> APIRouter:
7+
def setup_router(get_db_session_dep) -> APIRouter:
88
router = APIRouter()
99
router.include_router(healthcheck_router_setup(), prefix="")
1010
router.include_router(news_router_setup(), prefix="")
11-
router.include_router(authentication_router_setup(), prefix='')
11+
router.include_router(authentication_router_setup(get_db_session_dep), prefix='')
1212
return router

app/schemas.py

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,24 @@
33
from typing import List
44
from enum import Enum
55

6+
## News
7+
class News(BaseModel):
8+
description: str
9+
tag: str
10+
11+
class Library(BaseModel):
12+
library_name: str
13+
news: list[News]
14+
logo: HttpUrl
15+
version: str
16+
release_date: datetime
17+
release_doc_url: HttpUrl
618

7-
## User Class
19+
## Community / User Class
820
class Community(BaseModel):
921
username: str
10-
full_name: str
1122
email: str
12-
23+
## Extends Community Class with hashed password
1324
class CommunityInDB(Community):
1425
password: str
1526

@@ -32,17 +43,3 @@ class TagEnum(str, Enum):
3243
class Subscription(BaseModel):
3344
tags: List[TagEnum]
3445
libraries_list: List[str]
35-
36-
## News
37-
class News(BaseModel):
38-
description: str
39-
tag: str
40-
41-
42-
class Library(BaseModel):
43-
library_name: str
44-
news: list[News]
45-
logo: HttpUrl
46-
version: str
47-
release_date: datetime
48-
release_doc_url: HttpUrl

app/services/auth.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,16 @@
1212

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

18-
def get_password_hash(password):
17+
def hash_password(password):
1918
# Retorna a senha em hash para salvar no banco de dados
2019
return pwd_context.hash(password)
2120

2221
def create_access_token(data: TokenPayload, expires_delta: timedelta | None = None):
2322
"""
2423
Gera um token JWT contendo os dados do usuário (payload) e uma data de expiração.
25-
24+
JWT specification says that there's a key sub (subject) that should be used to identify the user.
2625
Parâmetros:
2726
- data (TokenPayload): Dicionário com os dados que serão codificados no token. Deve conter a chave 'sub' com o identificador do usuário.
2827
- expires_delta (timedelta | None): Tempo até o token expirar. Se não fornecido, usará o padrão de 20 minutos.

app/services/database/community.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
1-
# app/services/database/community.py
1+
from typing import Optional
22
from sqlmodel import select
3-
from sqlalchemy.exc import NoResultFound
4-
from app.services.database.model.community_model import Community
5-
from app.services.database.database import get_session
3+
from sqlmodel.ext.asyncio.session import AsyncSession
4+
from app.services.database.models import Community
65

7-
async def get_community_by_username(username: str):
8-
async for session in get_session():
9-
stmt = select(Community).where(Community.username == username)
10-
result = await session.exec(stmt)
11-
user = result.one_or_none()
12-
return user
6+
7+
async def get_community_by_username(
8+
username: str,
9+
session: AsyncSession,
10+
) -> Optional[Community]:
11+
"""
12+
Busca e retorna um membro da comunidade pelo nome de usuário.
13+
Retorna None se o usuário não for encontrado.
14+
"""
15+
# Cria a declaração SQL para buscar a comunidade pelo nome de usuário
16+
statement = select(Community).where(Community.username == username)
17+
18+
# Executa a declaração na sessão e retorna o primeiro resultado
19+
result = await session.exec(statement)
20+
community = result.first()
21+
22+
return community

app/services/database/database.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
engine,
3333
class_=AsyncSession,
3434
expire_on_commit=False,
35-
echo=True, # expire_on_commit=False é importante!
35+
#echo=True, # expire_on_commit=False é importante!
3636
)
3737

3838

app/services/database/model/community_model.py

Lines changed: 0 additions & 9 deletions
This file was deleted.

poetry.lock

Lines changed: 69 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pynewsdb.db

-16 KB
Binary file not shown.

0 commit comments

Comments
 (0)