Skip to content

Commit 0866c95

Browse files
committed
Merge branch 'main' into feature/#32
2 parents 9635c57 + 19637ef commit 0866c95

File tree

9 files changed

+403
-8
lines changed

9 files changed

+403
-8
lines changed

app/main.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
from contextlib import asynccontextmanager
33

44
from fastapi import FastAPI
5+
from slowapi import _rate_limit_exceeded_handler
56

67
from app.routers.router import setup_router as setup_router_v2
78
from app.services.database.database import AsyncSessionLocal, init_db
9+
from app.services.limiter import limiter
810

911
logger = logging.getLogger(__name__)
1012

@@ -27,6 +29,9 @@ async def lifespan(app: FastAPI):
2729
)
2830

2931

32+
app.state.limiter = limiter
33+
app.add_exception_handler(429, _rate_limit_exceeded_handler)
34+
3035
app.include_router(setup_router_v2(), prefix="/api")
3136

3237
logger.info("PyNews Server Starter")

app/routers/authentication.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from app.services import auth
1111
from app.services.database.models import Community as DBCommunity
1212
from app.services.database.orm.community import get_community_by_username
13+
from app.services.limiter import limiter
1314

1415
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/authentication/token")
1516

@@ -89,6 +90,7 @@ async def create_community(request: Request):
8990
# Teste
9091

9192
@router.post("/token", response_model=Token)
93+
@limiter.limit("60/minute")
9294
async def login_for_access_token(
9395
request: Request, form_data: OAuth2PasswordRequestForm = Depends()
9496
):
@@ -110,7 +112,9 @@ async def login_for_access_token(
110112
}
111113

112114
@router.get("/me", response_model=Community)
115+
@limiter.limit("60/minute")
113116
async def read_community_me(
117+
request: Request,
114118
current_community: Annotated[
115119
DBCommunity, Depends(get_current_active_community)
116120
],

app/routers/news/routes.py

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
1+
import os
12
from typing import Annotated
23

4+
import jwt
35
from fastapi import APIRouter, Depends, Request, status
46
from fastapi.params import Header
57
from pydantic import BaseModel
68

9+
import app.services.database.orm.news as orm_news
710
from app.routers.authentication import get_current_active_community
811
from app.schemas import News
912
from app.services.database.models import Community as DBCommunity
10-
from app.services.database.orm.news import create_news, get_news_by_query_params
13+
14+
SECRET_KEY = os.getenv("SECRET_KEY", "default_fallback_key")
15+
ALGORITHM = os.getenv("ALGORITHM", "HS256")
1116

1217

1318
class NewsPostResponse(BaseModel):
@@ -19,6 +24,19 @@ class NewsGetResponse(BaseModel):
1924
news_list: list = []
2025

2126

27+
class NewsLikeResponse(BaseModel):
28+
total_likes: int | None
29+
30+
31+
class LikeRequest(BaseModel):
32+
email: str
33+
34+
35+
def encode_email(email: str) -> str:
36+
"""Encodes the email to be safely stored in database."""
37+
return jwt.encode({"email": email}, SECRET_KEY, algorithm=ALGORITHM)
38+
39+
2240
def setup():
2341
router = APIRouter(prefix="/news", tags=["news"])
2442

@@ -42,7 +60,7 @@ async def post_news(
4260
"""
4361
news_dict = news.__dict__
4462
news_dict["user_email"] = user_email
45-
await create_news(
63+
await orm_news.create_news(
4664
session=request.app.db_session_factory, news=news_dict
4765
)
4866
return NewsPostResponse()
@@ -67,7 +85,7 @@ async def get_news(
6785
"""
6886
Get News endpoint that retrieves news filtered by user and query params.
6987
"""
70-
news_list = await get_news_by_query_params(
88+
news_list = await orm_news.get_news_by_query_params(
7189
session=request.app.db_session_factory,
7290
id=id,
7391
email=user_email,
@@ -76,4 +94,58 @@ async def get_news(
7694
)
7795
return NewsGetResponse(news_list=news_list)
7896

97+
@router.post(
98+
path="/{news_id}/like",
99+
response_model=NewsLikeResponse,
100+
status_code=status.HTTP_200_OK,
101+
summary="News like endpoint",
102+
description="Allows user to like a news item",
103+
)
104+
async def post_like(
105+
request: Request,
106+
current_community: Annotated[
107+
DBCommunity, Depends(get_current_active_community)
108+
],
109+
news_id: str,
110+
body: LikeRequest,
111+
user_email: str = Header(..., alias="user-email"),
112+
):
113+
"""
114+
News endpoint where user can set like to news item.
115+
"""
116+
encoded_email = encode_email(body.email)
117+
total_likes = await orm_news.like_news(
118+
session=request.app.db_session_factory,
119+
news_id=news_id,
120+
email=encoded_email,
121+
)
122+
return NewsLikeResponse(total_likes=total_likes)
123+
124+
@router.delete(
125+
path="/{news_id}/like",
126+
response_model=NewsLikeResponse,
127+
status_code=status.HTTP_200_OK,
128+
summary="News undo like endpoint",
129+
description="Allows user to undo a like to a news item",
130+
)
131+
async def delete_like(
132+
request: Request,
133+
current_community: Annotated[
134+
DBCommunity, Depends(get_current_active_community)
135+
],
136+
news_id: str,
137+
email: str,
138+
user_email: str = Header(..., alias="user-email"),
139+
):
140+
"""
141+
News endpoint where user can set like to news item.
142+
"""
143+
encoded_email = encode_email(email)
144+
total_likes = await orm_news.delete_like(
145+
session=request.app.db_session_factory,
146+
news_id=news_id,
147+
email=encoded_email,
148+
)
149+
return NewsLikeResponse(total_likes=total_likes)
150+
79151
return router

app/services/database/models/news.py

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,37 @@
55

66

77
class News(SQLModel, table=True):
8+
"""
9+
Represents a news article in the database.
10+
11+
Attributes:
12+
id (Optional[int]): Unique identifier for the news article.
13+
Auto-generated primary key.
14+
title (str): The headline or title of the news article.
15+
content (str): The main body or text content of the news.
16+
category (str): Category or topic classification
17+
(e.g., "Politics", "Tech").
18+
user_email (str): Email of the user who submitted or is associated
19+
with this news.
20+
source_url (str): URL pointing to the original source of the news.
21+
tags (str): Comma-separated or JSON-encoded string of tags for
22+
search/filtering.
23+
user_email_list (str): encoded list of emails of users who liked
24+
this news. Defaults to an empty list.
25+
social_media_url (str): URL to the social media post or share link
26+
for this news.
27+
likes (int): Number of likes this news article has received.
28+
Defaults to 0.
29+
30+
community_id (Optional[int]): Foreign key to the associated community
31+
(communities.id).
32+
33+
created_at (Optional[datetime]): Timestamp when the news was first
34+
created. Defaults to now.
35+
updated_at (Optional[datetime]): Timestamp when the news was last
36+
updated. Auto-updates on modification.
37+
"""
38+
839
__tablename__ = "news"
940

1041
# Campos obrigatórios e suas definições
@@ -15,18 +46,19 @@ class News(SQLModel, table=True):
1546
user_email: str
1647
source_url: str
1748
tags: str
49+
user_email_list: str = Field(default="[]")
1850
social_media_url: str
1951
likes: int = Field(default=0)
2052

2153
# Chaves estrangeiras
2254
community_id: Optional[int] = Field(
23-
default=None,
24-
foreign_key="communities.id")
55+
default=None, foreign_key="communities.id"
56+
)
2557
# library_id: Optional[int]=Field(default=None, foreign_key="libraries.id")
2658

2759
# Campos de data/hora
2860
created_at: Optional[datetime] = Field(default_factory=datetime.now)
2961
updated_at: Optional[datetime] = Field(
3062
default_factory=datetime.now,
31-
sa_column_kwargs={"onupdate": datetime.now}
63+
sa_column_kwargs={"onupdate": datetime.now},
3264
)

app/services/database/orm/news.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import ast
12
from typing import Optional
23

34
from sqlmodel import select
@@ -42,3 +43,41 @@ async def get_news_by_query_params(
4243
statement = select(News).where(*filters)
4344
results = await session.exec(statement)
4445
return results.all()
46+
47+
48+
async def like_news(
49+
session: AsyncSession, news_id: str, email: str
50+
) -> int | None:
51+
statement = select(News).where(News.id == news_id)
52+
results = await session.exec(statement)
53+
news_item = results.first()
54+
if news_item:
55+
users = ast.literal_eval(news_item.user_email_list)
56+
if email not in users:
57+
users.append(email)
58+
news_item.user_email_list = str(users)
59+
news_item.likes += 1
60+
session.add(news_item)
61+
await session.commit()
62+
await session.refresh(news_item)
63+
return news_item.likes
64+
return None
65+
66+
67+
async def delete_like(
68+
session: AsyncSession, news_id: str, email: str
69+
) -> int | None:
70+
statement = select(News).where(News.id == news_id)
71+
results = await session.exec(statement)
72+
news_item = results.first()
73+
if news_item:
74+
users = ast.literal_eval(news_item.user_email_list)
75+
if email in users:
76+
users.remove(email)
77+
news_item.user_email_list = str(users)
78+
news_item.likes -= 1
79+
session.add(news_item)
80+
await session.commit()
81+
await session.refresh(news_item)
82+
return news_item.likes
83+
return None

app/services/limiter.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from slowapi import Limiter
2+
from slowapi.util import get_remote_address
3+
4+
limiter = Limiter(key_func=get_remote_address)

0 commit comments

Comments
 (0)