Skip to content

Commit 3caa3f7

Browse files
authored
Merge pull request #58 from PythonFloripa/feature/#33
Feature/#33
2 parents e295c0e + fd3ffeb commit 3caa3f7

File tree

19 files changed

+923
-417
lines changed

19 files changed

+923
-417
lines changed

.vscode/settings.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,10 @@
99
},
1010
"python-envs.defaultEnvManager": "ms-python.python:poetry",
1111
"python-envs.defaultPackageManager": "ms-python.python:poetry",
12-
"python-envs.pythonProjects": []
12+
"python-envs.pythonProjects": [],
13+
"python.testing.pytestArgs": [
14+
"tests"
15+
],
16+
"python.testing.unittestEnabled": false,
17+
"python.testing.pytestEnabled": true
1318
}

app/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from fastapi import FastAPI
55
from slowapi import _rate_limit_exceeded_handler
66

7+
from app.routers.admin.routes import create_admin
78
from app.routers.router import setup_router as setup_router_v2
89
from app.services.database.database import AsyncSessionLocal, init_db
910
from app.services.limiter import limiter
@@ -16,6 +17,7 @@ async def lifespan(app: FastAPI):
1617
# add check db file and create if not found
1718
await init_db()
1819
app.db_session_factory = AsyncSessionLocal()
20+
await create_admin(app.db_session_factory)
1921
try:
2022
yield
2123
finally:

app/routers/admin/__init__.py

Whitespace-only changes.

app/routers/admin/routes.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import os
2+
from typing import Annotated
3+
4+
from fastapi import APIRouter, Depends, Request, status
5+
from sqlmodel.ext.asyncio.session import AsyncSession
6+
7+
from app.routers.authentication import get_current_active_community
8+
from app.schemas import CommunityPostResponse
9+
from app.services import auth
10+
from app.services.database.models import Community as DBCommunity # Precisa?
11+
from app.services.database.orm.community import create_community
12+
from app.services.limiter import limiter
13+
14+
# ADMIN_USER = os.getenv("ADMIN_USER")
15+
# ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD")
16+
17+
18+
async def create_admin(session: AsyncSession):
19+
ADMIN_USER = os.getenv("ADMIN_USER")
20+
ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD")
21+
ADMIN_EMAIL = os.getenv("ADMIN_EMAIL")
22+
password = ADMIN_PASSWORD
23+
hashed_password = auth.hash_password(password)
24+
community = DBCommunity(
25+
username=ADMIN_USER,
26+
email=ADMIN_EMAIL,
27+
password=hashed_password,
28+
role="admin",
29+
)
30+
await create_community(session=session, community=community)
31+
32+
return {"msg": "Admin successfully created"}
33+
34+
35+
def setup():
36+
router = APIRouter(prefix="/admin", tags=["admin"])
37+
38+
@router.post(
39+
"/create_community",
40+
response_model=CommunityPostResponse,
41+
status_code=status.HTTP_201_CREATED,
42+
summary="Create Community endpoint",
43+
description="Create Community and returns a confirmation message",
44+
)
45+
@limiter.limit("60/minute")
46+
async def post_create_community(
47+
request: Request,
48+
admin_community: Annotated[
49+
DBCommunity, Depends(get_current_active_community)
50+
],
51+
community: DBCommunity,
52+
):
53+
"""
54+
Server Admin endpoint that creates Community and returns a confirmation
55+
message.
56+
"""
57+
admin_role = admin_community.role
58+
if admin_role != "admin":
59+
return {"status": "Unauthorized"}
60+
session: AsyncSession = request.app.db_session_factory
61+
await create_community(session=session, community=community)
62+
63+
return CommunityPostResponse()
64+
65+
return router

app/routers/authentication.py

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -70,25 +70,6 @@ async def authenticate_community(
7070
return None
7171
return found_community
7272

73-
# Teste
74-
75-
@router.post("/create_commumity")
76-
async def create_community(request: Request):
77-
password = "123Asd!@#"
78-
hashed_password = auth.hash_password(password)
79-
community = DBCommunity(
80-
username="username",
81-
email="username@test.com",
82-
password=hashed_password,
83-
)
84-
session: AsyncSession = request.app.db_session_factory
85-
session.add(community)
86-
await session.commit()
87-
await session.refresh(community)
88-
return {"msg": "succes? "}
89-
90-
# Teste
91-
9273
@router.post("/token", response_model=Token)
9374
@limiter.limit("60/minute")
9475
async def login_for_access_token(

app/routers/router.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from fastapi import APIRouter
22

3+
from app.routers.admin.routes import setup as admin_router_setup
34
from app.routers.authentication import setup as authentication_router_setup
45
from app.routers.healthcheck.routes import setup as healthcheck_router_setup
56
from app.routers.libraries.routes import setup as libraries_router_setup
@@ -12,4 +13,5 @@ def setup_router() -> APIRouter:
1213
router.include_router(news_router_setup(), prefix="")
1314
router.include_router(authentication_router_setup(), prefix="")
1415
router.include_router(libraries_router_setup(), prefix="")
16+
router.include_router(admin_router_setup(), prefix="")
1517
return router

app/schemas.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,14 @@ class Community(BaseModel):
3434
email: str
3535

3636

37-
# Extends Community Class with hashed password
37+
# Extends Community Class with hashed password and role
3838
class CommunityInDB(Community):
3939
password: str
40+
role: str
41+
42+
43+
class CommunityPostResponse(BaseModel):
44+
status: str = "Community Criado"
4045

4146

4247
class News(BaseModel):
Lines changed: 3 additions & 1 deletion
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,10 +10,11 @@ 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
1415
created_at: Optional[datetime] = Field(default_factory=datetime.now)
1516
updated_at: Optional[datetime] = Field(
1617
default_factory=datetime.now,
1718
sa_column_kwargs={"onupdate": datetime.now},
1819
)
20+
role: str = Field(default="user") # user or admin

app/services/database/orm/community.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from sqlmodel.ext.asyncio.session import AsyncSession
55

66
from app.services.database.models import Community
7+
from app.services.encryption import decrypt_email, encrypt_email
78

89

910
async def get_community_by_username(
@@ -19,6 +20,29 @@ async def get_community_by_username(
1920

2021
# Executa a declaração na sessão e retorna o primeiro resultado
2122
result = await session.exec(statement)
22-
community = result.first()
23+
community_result = result.first()
24+
25+
# add tratamento de descriptografia do email
26+
if community_result is not None:
27+
# evitar mutação direta e cache
28+
community = community_result.model_copy()
29+
community.email = decrypt_email(community.email)
30+
else:
31+
community = None
32+
return community # add community not found treatment?
33+
34+
35+
async def create_community(
36+
session: AsyncSession,
37+
community: Community, # community model
38+
) -> Optional[Community]:
39+
"""
40+
Cria um novo membro da comunidade.
41+
Somente usuário autenticado e com role Admin podem executar.
42+
"""
43+
community.email = encrypt_email(community.email)
44+
session.add(community)
45+
await session.commit()
46+
await session.refresh(community)
2347

2448
return community

app/services/encryption.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import os
2+
3+
from cryptography.fernet import Fernet
4+
5+
test_encryption_key = "r0-QKv5qACJNFRqy2cNZCsfZ_zVvehlC-v8zDJb--EI="
6+
# print(Fernet.generate_key())
7+
# Carrega a chave da variável de ambiente
8+
ENCRYPTION_KEY = os.getenv("ENCRYPTION_KEY", test_encryption_key)
9+
10+
if not ENCRYPTION_KEY:
11+
raise RuntimeError(
12+
"ENCRYPTION_KEY não está definida nas variáveis de ambiente."
13+
)
14+
15+
cipher = Fernet(ENCRYPTION_KEY)
16+
17+
18+
def encrypt_email(email: str) -> str:
19+
"""Criptografa uma string de e-mail."""
20+
encrypted_email = cipher.encrypt(email.encode())
21+
return encrypted_email
22+
23+
24+
def decrypt_email(encrypted_email: str) -> str:
25+
"""Descriptografa uma string de e-mail."""
26+
decrypted_email = cipher.decrypt(encrypted_email).decode()
27+
return decrypted_email

0 commit comments

Comments
 (0)