Skip to content

Commit 6740773

Browse files
committed
FEAT: Implement email encryption and decryption in community model; update related functions and tests
1 parent 033d6a1 commit 6740773

6 files changed

Lines changed: 107 additions & 17 deletions

File tree

app/routers/admin/routes.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ async def post_create_community(
6161
admin_role = admin_community.role
6262
if admin_role != "admin":
6363
return {"status": "Unauthorized"}
64-
await create_community(request=request, community=community)
64+
session: AsyncSession = request.app.db_session_factory
65+
await create_community(session=session, community=community)
6566

6667
return CommunityPostResponse()
6768

app/services/database/models/communities.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from datetime import datetime
22
from typing import Optional
33

4+
from sqlalchemy import Text
45
from sqlmodel import Field, SQLModel
56

67

@@ -9,9 +10,8 @@ class Community(SQLModel, table=True):
910

1011
id: Optional[int] = Field(default=None, primary_key=True)
1112
username: str
12-
email: str
13+
email: str = Field(sa_column=Text) # VARCHAR(255)
1314
password: str
14-
role: str = Field(default="user")
1515
created_at: Optional[datetime] = Field(default_factory=datetime.now)
1616
updated_at: Optional[datetime] = Field(
1717
default_factory=datetime.now,

app/services/database/orm/community.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
from typing import Optional
22

3-
from fastapi import Request
43
from sqlmodel import select
54
from sqlmodel.ext.asyncio.session import AsyncSession
65

76
from app.services.database.models import Community
7+
from app.services.encryption import decrypt_email, encrypt_email
88

99

1010
async def get_community_by_username(
@@ -21,19 +21,22 @@ async def get_community_by_username(
2121
# Executa a declaração na sessão e retorna o primeiro resultado
2222
result = await session.exec(statement)
2323
community = result.first()
24+
# add tratamento de descriptografia do email
25+
if community is not None:
26+
community.email = decrypt_email(community.email)
2427

2528
return community
2629

2730

2831
async def create_community(
29-
request: Request,
32+
session: AsyncSession,
3033
community: Community, # community model
3134
) -> Optional[Community]:
3235
"""
3336
Cria um novo membro da comunidade.
3437
Somente usuário autenticado e com role Admin podem executar.
3538
"""
36-
session: AsyncSession = request.app.db_session_factory
39+
community.email = encrypt_email(community.email)
3740
session.add(community)
3841
await session.commit()
3942
await session.refresh(community)

app/services/encryption.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from cryptography.fernet import Fernet
44

55
test_encryption_key = "r0-QKv5qACJNFRqy2cNZCsfZ_zVvehlC-v8zDJb--EI="
6+
# print(Fernet.generate_key())
67
# Carrega a chave da variável de ambiente
78
ENCRYPTION_KEY = os.getenv("ENCRYPTION_KEY", test_encryption_key)
89

@@ -11,14 +12,19 @@
1112
"ENCRYPTION_KEY não está definida nas variáveis de ambiente."
1213
)
1314

14-
cipher = Fernet(ENCRYPTION_KEY.encode())
15+
cipher = Fernet(ENCRYPTION_KEY)
1516

1617

1718
def encrypt_email(email: str) -> str:
1819
"""Criptografa uma string de e-mail."""
19-
return cipher.encrypt(email.encode()).decode()
20+
# var = cipher.encrypt(b'{email}')
21+
22+
var = cipher.encrypt(email.encode("utf-8"))
23+
print(f"encrypt_email: {email} -> {var}")
24+
print(len(var))
25+
return var
2026

2127

2228
def decrypt_email(encrypted_email: str) -> str:
2329
"""Descriptografa uma string de e-mail."""
24-
return cipher.decrypt(encrypted_email.encode()).decode()
30+
return cipher.decrypt(encrypted_email).decode("utf-8")

docker-compose.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ services:
1414
- SQLITE_PATH=app/services/database/pynewsdb.db
1515
- SQLITE_URL=sqlite+aiosqlite://
1616
- SECRET_KEY=1a6c5f3b7d2e4a7fb68d0casd3f9a7b2d8c4e5f6a3b0d4e9c7a8f1b6d3c0a7f5e
17-
- ENCRYPTION_KEY=smR739opNB9FJ4hEm5ZIG8Gr-Qnvqtem4ehwl4RIUes=
17+
- ENCRYPTION_KEY=r0-QKv5qACJNFRqy2cNZCsfZ_zVvehlC-v8zDJb--EI=
1818
- ADMIN_USER=admin
1919
- ADMIN_PASSWORD=admin
2020
- ALGORITHM=HS256

tests/test_communities.py

Lines changed: 87 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,104 @@
33
from sqlmodel.ext.asyncio.session import AsyncSession
44

55
from app.services.database.models import Community
6+
from app.services.database.orm.community import (
7+
create_community,
8+
get_community_by_username,
9+
)
10+
11+
# Dados de teste
12+
TEST_USERNAME = "test_user_crypto"
13+
TEST_EMAIL = "crypto@test.com"
14+
TEST_PASSWORD = "@SafePassword123"
615

716

817
@pytest.mark.asyncio
918
async def test_insert_communities(session: AsyncSession):
1019
community = Community(
11-
username="admin",
12-
email="teste@teste.com",
13-
password="@teste123",
20+
username=TEST_USERNAME,
21+
email=TEST_EMAIL,
22+
password=TEST_PASSWORD,
1423
)
1524
session.add(community)
1625
await session.commit()
1726
await session.refresh(community)
1827

19-
statement = select(Community).where(Community.username == "admin")
28+
statement = select(Community).where(Community.username == TEST_USERNAME)
2029
result = await session.exec(statement)
2130
found = result.first()
2231

2332
assert found is not None
24-
assert found.username == "admin"
25-
assert found.email == "teste@teste.com"
26-
assert found.password == "@teste123"
33+
assert found.username == TEST_USERNAME
34+
assert found.email == TEST_EMAIL
35+
assert found.password == TEST_PASSWORD
36+
37+
38+
@pytest.mark.asyncio
39+
async def test_community_orm_flow_with_encryption_transparency(
40+
session: AsyncSession,
41+
):
42+
"""
43+
Testa a criação e a leitura de uma comunidade, validando
44+
que o ORM (propriedades) garante a transparência da criptografia.
45+
"""
46+
new_community = Community(
47+
username=TEST_USERNAME,
48+
email=TEST_EMAIL,
49+
password=TEST_PASSWORD,
50+
)
51+
52+
# 2. Ação: Use a função ORM para criar a comunidade
53+
created_community = await create_community(
54+
community=new_community, session=session
55+
)
56+
57+
# 3. Asserção de Leitura Transparente (getters)
58+
# Ao acessar 'created_community.email', ele DEVE retornar o email descriptografado
59+
assert created_community.email == TEST_EMAIL
60+
assert created_community.username == TEST_USERNAME
61+
62+
# 4. Asserção da Criptografia (Validação do Armazenamento)
63+
# Acessar o campo interno '_email' para provar que está criptografado
64+
stored_email = created_community._email
65+
66+
# O email armazenado não deve ser igual ao email original (em texto puro)
67+
assert stored_email != TEST_EMAIL
68+
69+
# O email armazenado deve ser um valor válido de Fernet (a criptografia)
70+
# Usamos a função de criptografia para ter um valor esperado
71+
assert stored_email == TEST_EMAIL
72+
73+
74+
@pytest.mark.asyncio
75+
async def test_get_community_by_username_orm(session: AsyncSession):
76+
"""
77+
Testa a função de leitura 'get_community_by_username' e a descriptografia.
78+
"""
79+
# 1. Preparação: Crie um registro diretamente no banco para garantir
80+
# que o teste não dependa da função create_community
81+
community_to_insert = Community(
82+
username="newreader_test",
83+
email=TEST_EMAIL,
84+
password=TEST_PASSWORD,
85+
)
86+
87+
# Fazemos a inserção no banco de forma manual para forçar a criptografia
88+
# (O setter do modelo faz a criptografia automaticamente aqui)
89+
session.add(community_to_insert)
90+
await session.commit()
91+
await session.refresh(community_to_insert)
92+
93+
# 2. Ação: Use a função ORM para buscar o registro
94+
found_community = await get_community_by_username(
95+
username="newreader_test", session=session
96+
)
97+
98+
# 3. Asserções
99+
assert found_community is not None
100+
101+
# O email lido deve ser o valor original (descriptografado pelo modelo)
102+
assert found_community.email == TEST_EMAIL
103+
assert found_community.username == "newreader_test"
104+
105+
# Garante que a senha está correta (embora não seja o foco, é bom manter)
106+
assert found_community.password == TEST_PASSWORD

0 commit comments

Comments
 (0)