Skip to content

Commit 53eaedc

Browse files
add create notification endpoint + refactor jwt token validation
Co-authored-by: violeta <50527601+violetaperezandrade@users.noreply.github.com>
1 parent 11a88c8 commit 53eaedc

File tree

9 files changed

+138
-27
lines changed

9 files changed

+138
-27
lines changed

app/controller/Users.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,17 @@ async def handle_login(self, auth_code: str):
6464
headers={"x-access-token": f"{jwt_token}"},
6565
)
6666

67-
def handle_update_user(self, update_data: dict, request: Request):
68-
user_id = self.users_service.retrieve_user_id(request)
67+
def handle_update_user(self, update_data: dict, user_id: int):
6968
self.users_service.update_user(user_id, update_data)
7069
return {
7170
"message": "User updated successfully",
7271
"status": status.HTTP_200_OK,
7372
}
73+
74+
def handle_create_notification(self, notification_data: dict,
75+
user_id: int):
76+
self.users_service.create_notification(user_id, notification_data)
77+
return {
78+
"message": "Notification created successfully",
79+
"status": status.HTTP_201_CREATED,
80+
}

app/docker/tablas.sql

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,22 @@ VALUES ('Agus', 'agus@fi.uba.ar', TO_DATE('1999-01-29', 'YYYY-MM-DD'), '{"lat":
2929
-- ('Sofi', 'sofi@fi.uba.ar', TO_DATE('1998-04-26', 'YYYY-MM-DD'), '{"lat": 1190, "long": 500}', 'QWEQWE'),
3030
-- ('Violeta', 'violeta@fi.uba.ar', TO_DATE('1998-05-12', 'YYYY-MM-DD'), '{"lat": 330, "long": 2333}', 'ZXZXZX');
3131

32+
DROP TABLE IF EXISTS users_service.alarms CASCADE;
33+
3234
CREATE TABLE IF NOT EXISTS users_service.alarms (
3335
id SERIAL PRIMARY KEY,
3436
id_user INT NOT NULL REFERENCES users_service.users(id) ON DELETE CASCADE,
35-
datetime TIMESTAMP WITH TIME ZONE NOT NULL,
37+
date_time TIMESTAMP WITH TIME ZONE NOT NULL,
3638
content VARCHAR(128) NOT NULL
3739
);
38-
39-
CREATE INDEX idx_alarms_datetime ON users_service.alarms(datetime);
40+
CREATE INDEX idx_alarms_date_time ON users_service.alarms(date_time);
4041

4142
INSERT INTO
42-
users_service.alarms (id_user, datetime, content)
43-
VALUES (1, '2024-04-29 02:15:00-03', 'Wake up 1!'),
44-
(2, '2024-04-29 02:15:00-03', 'Wake up 2!'),
45-
(3, '2024-04-29 02:25:00-03', 'Wake up 3!'),
46-
(4, '2024-04-29 02:25:00-03', 'Wake up 4!');
43+
users_service.alarms (id_user, date_time, content)
44+
VALUES (1, '2024-04-30 00:50:00-03', 'Wake up 1!'),
45+
(1, '2024-04-30 00:50:00-03', 'Wake up 2!'),
46+
(1, '2024-04-30 00:50:00-03', 'Wake up 3!'),
47+
(1, '2024-04-30 00:50:00-03', 'Wake up 4!');
4748

4849
-- SELECT users_service.alarms.id, users_service.alarms.content, users_service.users.device_token
4950
-- FROM users_service.alarms

app/main.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
1-
from fastapi import FastAPI, Request, Query
1+
from fastapi import Depends, FastAPI, Query
22
from controller.Users import UsersController
33
from service.Users import UsersService
44
from repository.Users import UsersRepository
5-
from schemas.Schemas import CreateUserSchema, UpdateUserSchema
6-
from schemas.Schemas import LoginRequest
75
from typing import List, Annotated, Union
6+
from schemas.Schemas import (
7+
CreateUserSchema,
8+
UpdateUserSchema,
9+
LoginRequest,
10+
CreateNotificationSchema
11+
)
12+
from security.JWTBearer import get_current_user_id
13+
from logs import init_logging
14+
logger = init_logging('user-repository')
815

916
app = FastAPI()
1017
users_repository = UsersRepository()
@@ -17,13 +24,14 @@ def root():
1724
return {"message": "users service"}
1825

1926

20-
@app.get("/users/{user_id}")
21-
def get_users(user_id: int):
27+
@app.get("/users/me")
28+
def get_users(user_id: Annotated[int, Depends(get_current_user_id)]):
2229
return users_controller.handle_get_user(user_id)
2330

2431

2532
@app.get("/users")
26-
def get_all_users(ids: Annotated[Union[List[str], None], Query()] = None):
33+
def get_all_users(_: Annotated[int, Depends(get_current_user_id)],
34+
ids: Annotated[Union[List[str], None], Query()] = None):
2735
return users_controller.handle_get_all_users(ids)
2836

2937

@@ -39,6 +47,16 @@ async def login_with_google(request: LoginRequest):
3947

4048
@app.patch("/users/me")
4149
async def update_user(update_data: UpdateUserSchema,
42-
request: Request):
43-
return users_controller.handle_update_user(update_data.dict(),
44-
request)
50+
user_id: Annotated[int, Depends(get_current_user_id)]):
51+
return users_controller.handle_update_user(update_data.dict(), user_id)
52+
53+
54+
@app.post("/users/me/notification")
55+
async def create_notification(
56+
user_id: Annotated[int,
57+
Depends(get_current_user_id)],
58+
create_notification: CreateNotificationSchema
59+
):
60+
logger.info(f"Creating notification for user {user_id}")
61+
return users_controller.handle_create_notification(
62+
create_notification.dict(), user_id)

app/models/alarms.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ class Alarm(Base):
1616
autoincrement=True)
1717
id_user: Mapped[int] = mapped_column(ForeignKey(f"{SCHEMA}.users.id"),
1818
nullable=False, unique=True)
19-
datetime: Mapped[TIMESTAMP] = mapped_column(TIMESTAMP, nullable=False)
19+
date_time: Mapped[TIMESTAMP] = mapped_column(TIMESTAMP, nullable=False)
2020
content: Mapped[str] = mapped_column(String(128), nullable=False)
2121

2222
def __repr__(self) -> str:
2323
return (
2424
f"Alarm(id={self.id}, "
2525
f"id_user={self.id_user}, "
26-
f"datetime={self.datetime}, "
26+
f"date_time={self.date_time}, "
2727
f"content={self.content})"
2828
)

app/repository/Users.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,12 @@ def edit_user(self, user_id: int, data_to_edit: dict):
9696
self.session.commit()
9797
return user
9898

99-
def __parse_result(self, result):
100-
if not result:
101-
return []
102-
return [r.__dict__ for r in result]
99+
@withSQLExceptionsHandle()
100+
def create_notification(self, user_id: int, notification_data: dict):
101+
alarm = Alarm(**notification_data, id_user=user_id)
102+
self.session.add(alarm)
103+
self.session.commit()
104+
return alarm
103105

104106
@withSQLExceptionsHandle()
105107
def get_users_to_notify(self,
@@ -128,3 +130,8 @@ def get_users_to_notify(self,
128130
.filter(Alarm.datetime == date_time)
129131
result = query.all()
130132
return result
133+
134+
def __parse_result(self, result):
135+
if not result:
136+
return []
137+
return [r.__dict__ for r in result]

app/schemas/Schemas.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from datetime import date
2-
from pydantic import BaseModel
2+
from pydantic import BaseModel, Field, validator
33
from typing import Optional, Dict
4+
from datetime import datetime
45

56

67
class UserSchema(BaseModel):
@@ -40,3 +41,14 @@ class UpdateUserSchema(BaseModel):
4041
nickname: Optional[str] = None
4142
biography: Optional[str] = None
4243
device_token: Optional[str] = None
44+
45+
46+
class CreateNotificationSchema(BaseModel):
47+
date_time: datetime = Field(..., alias='date_time')
48+
content: str = Field(..., max_length=128)
49+
50+
@validator('date_time')
51+
def validate_date_time(cls, v):
52+
if v.minute % 5 != 0:
53+
raise ValueError('Minutes must be multiples of 5')
54+
return v

app/security/JWTBearer.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from typing import Annotated
2+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
3+
from fastapi import Request, Depends, HTTPException, status
4+
from jose import JWTError, jwt
5+
from pydantic import BaseModel
6+
from os import environ
7+
8+
JWT_SECRET = environ.get("JWT_SECRET")
9+
HASH_ALGORITHM = environ.get("HASH_ALGORITHM")
10+
TOKEN_FIELD_NAME = "x-access-token"
11+
12+
13+
class JWTBearer(HTTPBearer):
14+
def __init__(self, auto_error: bool = True):
15+
super().__init__(auto_error=auto_error)
16+
17+
async def __call__(self, request: Request):
18+
try:
19+
credentials: HTTPAuthorizationCredentials = await super().\
20+
__call__(request)
21+
if credentials.scheme != "Bearer":
22+
raise HTTPException(
23+
status_code=status.HTTP_401_UNAUTHORIZED,
24+
detail="(Bearer) Invalid scheme or token.",
25+
)
26+
return credentials.credentials
27+
28+
except Exception:
29+
access_token = request.headers.get(TOKEN_FIELD_NAME)
30+
if access_token:
31+
return access_token
32+
33+
raise HTTPException(
34+
status_code=status.HTTP_401_UNAUTHORIZED,
35+
detail="(x-access-token) Invalid scheme or token.",
36+
)
37+
38+
39+
class TokenData(BaseModel):
40+
user_id: int | None = None
41+
42+
43+
async def get_current_user_id(token: Annotated[dict, Depends(JWTBearer())]):
44+
credentials_exception = HTTPException(
45+
status_code=status.HTTP_401_UNAUTHORIZED,
46+
detail="Could not validate credentials",
47+
headers={"WWW-Authenticate": "Bearer"},
48+
)
49+
try:
50+
payload = jwt.decode(token, JWT_SECRET, algorithms=[HASH_ALGORITHM])
51+
user_id: int = payload.get("user_id")
52+
if user_id is None:
53+
raise credentials_exception
54+
token_data = TokenData(user_id=user_id)
55+
except JWTError:
56+
raise credentials_exception
57+
return token_data.user_id

app/service/Users.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ def update_user(self, user_id: int, update_data: dict):
5252
self.user_repository.rollback()
5353
raise e
5454

55+
def create_notification(self, user_id: int, notification_data: dict):
56+
try:
57+
self.user_repository.create_notification(user_id,
58+
notification_data)
59+
except Exception as e:
60+
self.user_repository.rollback()
61+
raise e
62+
5563
def _generate_nickname(self, name):
5664

5765
name_without_spaces = name.replace(" ", "")

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ pyjwt
99
httpx
1010
APScheduler>=3.10,<4.0
1111
arq
12-
pytz
12+
pytz
13+
python-jose

0 commit comments

Comments
 (0)