Skip to content

Commit 6f8507c

Browse files
authored
Merge pull request #43 from PythonFloripa/feature/#42
Feature/#42
2 parents e732292 + 16bcbb7 commit 6f8507c

File tree

6 files changed

+188
-71
lines changed

6 files changed

+188
-71
lines changed

app/routers/authentication.py

Lines changed: 86 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,119 @@
1-
from fastapi import APIRouter, Depends, HTTPException, status, Request
2-
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
3-
from sqlmodel.ext.asyncio.session import AsyncSession
1+
from typing import Annotated
2+
43
import jwt
4+
from fastapi import APIRouter, Depends, HTTPException, Request, status
5+
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
56
from jwt.exceptions import InvalidTokenError
7+
from sqlmodel.ext.asyncio.session import AsyncSession
68

9+
from app.schemas import Community, Token, TokenPayload
710
from app.services import auth
8-
from app.schemas import Token, TokenPayload, Community
911
from app.services.database.models import Community as DBCommunity
1012
from app.services.database.orm.community import get_community_by_username
1113

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

16+
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
2633

34+
async def get_current_community(
35+
request: Request,
36+
token: Annotated[str, Depends(oauth2_scheme)],
37+
) -> DBCommunity:
38+
credentials_exception = HTTPException(
39+
status_code=status.HTTP_401_UNAUTHORIZED,
40+
detail="Could not validate credentials",
41+
headers={"WWW-Authenticate": "Bearer"},
42+
)
2743

28-
#### Teste
44+
try:
45+
payload = jwt.decode(
46+
token, auth.SECRET_KEY, algorithms=[auth.ALGORITHM]
47+
)
48+
username = payload.get("sub")
49+
if username is None:
50+
raise credentials_exception
51+
token_data = TokenPayload(username=username)
52+
except InvalidTokenError:
53+
raise credentials_exception
54+
session: AsyncSession = request.app.db_session_factory
55+
community = await get_community_by_username(
56+
session=session, username=token_data.username
57+
)
58+
if community is None:
59+
raise credentials_exception
60+
61+
return community
62+
63+
async def get_current_active_community(
64+
current_user: Annotated[DBCommunity, Depends(get_current_community)],
65+
) -> DBCommunity:
66+
# A função simplesmente retorna o usuário.
67+
# Pode ser estendido futuramente para verificar um status "ativo".
68+
return current_user
69+
70+
# Teste
2971

3072
@router.post("/create_commumity")
31-
async def create_community(request: Request ):
73+
async def create_community(request: Request):
3274
password = "123Asd!@#"
33-
hashed_password=auth.hash_password(password)
34-
community = DBCommunity(username="username", email="[email protected]", password=hashed_password)
75+
hashed_password = auth.hash_password(password)
76+
community = DBCommunity(
77+
username="username",
78+
79+
password=hashed_password,
80+
)
3581
session: AsyncSession = request.app.db_session_factory
3682
session.add(community)
3783
await session.commit()
3884
await session.refresh(community)
39-
return {'msg':'succes? '}
40-
#### Teste
85+
return {"msg": "succes? "}
86+
87+
# Teste
4188

4289
@router.post("/token", response_model=Token)
43-
async def login_for_access_token(request: Request , form_data: OAuth2PasswordRequestForm = Depends() ) :
90+
async def login_for_access_token(
91+
request: Request, form_data: OAuth2PasswordRequestForm = Depends()
92+
):
4493
# Rota de login: valida credenciais e retorna token JWT
45-
community = await authenticate_community( request, form_data.username, form_data.password)
94+
community = await authenticate_community(
95+
request, form_data.username, form_data.password
96+
)
4697
if not community:
4798
raise HTTPException(
4899
status_code=status.HTTP_401_UNAUTHORIZED,
49-
detail="Credenciais inválidas"
100+
detail="Credenciais inválidas",
50101
)
51102
payload = TokenPayload(username=community.username)
52103
token, expires_in = auth.create_access_token(data=payload)
53104
return {
54105
"access_token": token,
55106
"token_type": "Bearer",
56-
"expires_in": expires_in
107+
"expires_in": expires_in,
57108
}
58-
return router # Retorna o router configurado com as rotas de autenticação
109+
110+
@router.get("/me", response_model=Community)
111+
async def read_community_me(
112+
current_community: Annotated[
113+
DBCommunity, Depends(get_current_active_community)
114+
],
115+
):
116+
# Rota para obter informações do usuário autenticado
117+
return current_community
118+
119+
return router # Retorna o router configurado com as rotas de autenticação

app/routers/libraries/routes.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
from fastapi import APIRouter, Request, status
22
from pydantic import BaseModel
3-
from services.database.orm.library import insert_library
43

54
from app.schemas import Library as LibrarySchema
65
from app.services.database.models.libraries import Library
6+
from app.services.database.orm.library import insert_library
77

88

99
class LibraryResponse(BaseModel):

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

tests/test_auth.py

Lines changed: 77 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
import pytest
22
import pytest_asyncio
3+
from fastapi import status
4+
from httpx import AsyncClient
35
from services.database.models import Community
4-
from sqlmodel import select
56
from sqlmodel.ext.asyncio.session import AsyncSession
67

7-
from httpx import AsyncClient
8-
from fastapi import status
9-
from typing import Mapping
108
from app.services.auth import hash_password
119

1210
password = "123Asd!@#"
1311

14-
## gerar usuario para autenticação
12+
13+
# gerar usuario para autenticação
1514
@pytest_asyncio.fixture
1615
async def community(session: AsyncSession):
17-
hashed_password=hash_password(password)
18-
community = Community(username="username", email="[email protected]", password=hashed_password)
16+
hashed_password = hash_password(password)
17+
community = Community(
18+
username="username", email="[email protected]", password=hashed_password
19+
)
1920
session.add(community)
2021
await session.commit()
2122
await session.refresh(community)
@@ -24,23 +25,20 @@ async def community(session: AsyncSession):
2425

2526
@pytest.mark.asyncio
2627
async def test_authentication_token_endpoint(
27-
async_client: AsyncClient,
28-
community: Community # Adicionando a comunidade do fixture
28+
async_client: AsyncClient,
29+
community: Community, # Adicionando a comunidade do fixture
2930
):
3031
"""
3132
Testa o endpoint de login (/token) com credenciais válidas e inválidas.
3233
"""
3334
# 1. Teste de login com credenciais válidas
3435
# O OAuth2PasswordRequestForm espera 'username' e 'password'
35-
form_data = {
36-
"username": community.username,
37-
"password": password
38-
}
39-
36+
form_data = {"username": community.username, "password": password}
37+
4038
response = await async_client.post(
4139
"/api/authentication/token",
4240
data=form_data,
43-
headers={"Content-Type": "application/x-www-form-urlencoded"}
41+
headers={"Content-Type": "application/x-www-form-urlencoded"},
4442
)
4543

4644
# Validar a resposta
@@ -51,17 +49,77 @@ async def test_authentication_token_endpoint(
5149

5250
# 2. Teste de login com credenciais inválidas
5351
invalid_form_data = {
54-
"username": "wrong_username",
55-
"password": "wrong_password"
52+
"username": "wrong_username",
53+
"password": "wrong_password",
5654
}
5755

5856
response_invalid = await async_client.post(
5957
"/api/authentication/token",
6058
data=invalid_form_data,
61-
headers={"Content-Type": "application/x-www-form-urlencoded"}
59+
headers={"Content-Type": "application/x-www-form-urlencoded"},
6260
)
63-
61+
6462
# Validar que o status é 401 Unauthorized
6563
assert response_invalid.status_code == status.HTTP_401_UNAUTHORIZED
6664
assert response_invalid.json()["detail"] == "Credenciais inválidas"
6765

66+
67+
@pytest.mark.asyncio
68+
async def test_community_me_with_valid_token(
69+
async_client: AsyncClient, community: Community
70+
):
71+
"""
72+
Testa se o endpoint protegido /authenticate/me/ retorna os dados do usuário com um token válido.
73+
"""
74+
# 1. Obter um token de acesso primeiro
75+
form_data = {
76+
"grant_type": "password",
77+
"username": community.username,
78+
"password": password,
79+
}
80+
token_response = await async_client.post(
81+
"/api/authentication/token",
82+
data=form_data,
83+
headers={"Content-Type": "application/x-www-form-urlencoded"},
84+
)
85+
assert token_response.status_code == status.HTTP_200_OK
86+
token = token_response.json()["access_token"]
87+
88+
# 2. Acessar o endpoint protegido com o token
89+
headers = {"Authorization": f"Bearer {token}"}
90+
response = await async_client.get("/api/authentication/me", headers=headers)
91+
92+
# Validar a resposta
93+
assert response.status_code == status.HTTP_200_OK
94+
user_data = response.json()
95+
assert user_data["username"] == community.username
96+
assert user_data["email"] == community.email
97+
# Assegurar que a senha não é retornada na resposta
98+
assert "password" not in user_data
99+
100+
101+
@pytest.mark.asyncio
102+
async def test_community_me_without_token(async_client: AsyncClient):
103+
"""
104+
Testa se o endpoint protegido authentication/me/ retorna um erro 401 sem um token de acesso.
105+
"""
106+
response = await async_client.get("/api/authentication/me")
107+
108+
# Validar a resposta
109+
assert response.status_code == status.HTTP_401_UNAUTHORIZED
110+
assert "detail" in response.json()
111+
assert response.json()["detail"] == "Not authenticated"
112+
113+
114+
@pytest.mark.asyncio
115+
async def test_community_me_with_bad_token(async_client: AsyncClient):
116+
"""
117+
Testa se o endpoint protegido authentication/me/ retorna um erro 401 sem um token de acesso.
118+
"""
119+
headers = {"Authorization": "Bearer WrongToken"}
120+
response = await async_client.get("/api/authentication/me", headers=headers)
121+
122+
# Validar a resposta
123+
assert response.status_code == status.HTTP_401_UNAUTHORIZED
124+
assert "detail" in response.json()
125+
assert response.json()["detail"] == "Could not validate credentials"

0 commit comments

Comments
 (0)