Skip to content

Commit

Permalink
Chim/feature/setup notification repository (#284)
Browse files Browse the repository at this point in the history
Add fcm token repository and Api endpoint for register and unregister
fcm token.
Add a new domain FCMToken for representing fcm token.

-
[Jira](https://fireapp-emergiq-2024.atlassian.net/jira/software/projects/FIR/boards/2?selectedIssue=FIR-99)
-
[Documentation](https://fireapp-emergiq-2024.atlassian.net/wiki/spaces/fireapp202/pages/99713028/Notification+Feature)

merged new changes from origin/main into branch to make merging more seamless
  • Loading branch information
anannnchim authored and 5oappy committed Sep 27, 2024
1 parent 71aef6d commit 9cd3c64
Show file tree
Hide file tree
Showing 10 changed files with 207 additions and 2 deletions.
3 changes: 2 additions & 1 deletion controllers/v2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
from .diet import *
from .v2_blueprint import v2_api, v2_bp
from .unavailability import *
from .shift import *
from .shift import *
from .fcm_tokens import *
1 change: 1 addition & 0 deletions controllers/v2/fcm_tokens/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .api import FCMToken
93 changes: 93 additions & 0 deletions controllers/v2/fcm_tokens/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import logging
from controllers.v2.fcm_tokens.response_models import response_model
from controllers.v2.v2_blueprint import v2_api
from exception import InvalidTokenError
from flask_restful import Resource, reqparse, marshal_with
from repository.fcm_token_repository import FCMTokenRepository
from repository.user_repository import UserRepository
from services.jwk import requires_auth, JWKService


parser = reqparse.RequestParser()
parser.add_argument('token', type=str, required=True, help ="Token must be provided.")
parser.add_argument('device_type', type=str, required=True, help ="DeviceType must be provided.")
unregister_parser = reqparse.RequestParser()
unregister_parser.add_argument('token', type=str, required=True, help ="Token must be provided.")


class FCMToken(Resource):

token_repository: FCMTokenRepository
user_repository: UserRepository

def __init__(
self,
token_repository: FCMTokenRepository = FCMTokenRepository(),
user_repository: UserRepository = UserRepository()
):
self.token_repository = token_repository
self.user_repository = user_repository

@requires_auth
@marshal_with(response_model)
def post(self, user_id: int):

# Decode authenticated user id
authenticated_user_id = JWKService.decode_user_id()

# Check if they are matched
if authenticated_user_id != user_id:
return {"message": "User ID mismatch"}, 403

# Check if decoding fail
args = parser.parse_args()
fcm_token = args['token']
device_type = args['device_type']

try:
# 1. Check if the user exist
if not self.user_repository.check_user_exists(user_id):
return {"message": "User not found"}, 400

# 2. Register the token for the user
self.token_repository.register_token(user_id, fcm_token, device_type)
return {"message": "FCM token registered successfully"}, 200

except Exception as e:

logging.error(f"Error registering FCM token: {e}")
return {"message": "Internal server error"}, 500

@requires_auth
@marshal_with(response_model)
def delete(self, user_id: int):

# Decode authenticated user id
authenticated_user_id = JWKService.decode_user_id()

# Check if they are matched
if authenticated_user_id != user_id:
return {"message": "User ID mismatch"}, 403

args = unregister_parser.parse_args()
fcm_token = args['token']

try:
# Check if user exist
if not self.user_repository.check_user_exists(user_id):
return {"message": "User not found"}, 400

# Unregister the token
self.token_repository.unregister_token(user_id, fcm_token)
return {"message": "FCM token unregistered successfully"}, 200

except InvalidTokenError as e:
logging.error(f"Error unregistering FCM token: {e}")
return {"message": "Invalid FCM token"}, 400

except Exception as e:
logging.error(f"Error unregistering FCM token: {e}")
return {"message": "Internal server error"}, 500


v2_api.add_resource(FCMToken,'/v2/user/<int:user_id>/token')
6 changes: 6 additions & 0 deletions controllers/v2/fcm_tokens/response_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from flask_restful import fields


response_model = {
'message': fields.String
}
2 changes: 2 additions & 0 deletions domain/entity/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@
from .shift_request import ShiftRequest
from .shift_request_volunteer import ShiftRequestVolunteer
from .shift_position import ShiftPosition
from .fcm_tokens import FCMToken

18 changes: 18 additions & 0 deletions domain/entity/fcm_tokens.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from datetime import datetime
from domain.base import Base
from sqlalchemy import Column, String, ForeignKey, Integer, Boolean, TIMESTAMP
from sqlalchemy.orm import relationship


class FCMToken(Base):

__tablename__ = 'fcm_tokens'
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, ForeignKey('user.id'), name='user_id', nullable=False)
fcm_token = Column(String(255), name='fcm_token', nullable=False)
device_type = Column(String(50), name='device_type', nullable=False)
created_at = Column(TIMESTAMP, name='created_at', default=datetime.now(), nullable=False)
updated_at = Column(TIMESTAMP, name='updated_at', default=datetime.now(), onupdate=datetime.now, nullable=False)
is_active = Column(Boolean, name='is_active', default=True, nullable=False)

user = relationship("User")
1 change: 1 addition & 0 deletions exception/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from .client_exception import EventNotFoundError, InvalidArgumentError
from .invalid_token_exception import InvalidTokenError

10 changes: 10 additions & 0 deletions exception/invalid_token_exception.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from .fireapp_exception import FireAppException


class InvalidTokenError(FireAppException):
"""
A custom exception used to handle invalid tokens for the user.
"""
def __init__(self, message='Invalid token for the user'):
self.message = message
super().__init__(self.message)
57 changes: 57 additions & 0 deletions repository/fcm_token_repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import logging
from datetime import datetime
from domain.entity.fcm_tokens import FCMToken
from domain import session_scope
from exception import InvalidTokenError
from sqlalchemy.exc import SQLAlchemyError


class FCMTokenRepository:
def __init__(self):
pass

def register_token(self, user_id: int, fcm_token: str, device_type: str) -> None:

with session_scope() as session:
try:
# 1. Check if there is user given userId
existing_token = session.query(FCMToken).filter_by(user_id=user_id, fcm_token=fcm_token).first()

if existing_token:
existing_token.updated_at = datetime.now()
session.commit()
logging.info(f" A New token is registered for user {user_id}")
else:
new_token = FCMToken(
user_id=user_id,
fcm_token=fcm_token,
device_type=device_type,
created_at=datetime.now(),
updated_at=datetime.now()
)
session.add(new_token)
session.commit()
logging.info(f" A New token is registered for user {user_id}")

except Exception as e:
logging.error(f"Error registering FCM token for user {user_id}: {e}")
session.rollback()

def unregister_token(self, user_id: int, fcm_token: str) -> None:

with session_scope() as session:
try:
existing_token = session.query(FCMToken).filter_by(user_id=user_id, fcm_token=fcm_token).first()

if existing_token:
session.delete(existing_token)
session.commit()
logging.info(f" Unregistered the token for user {user_id}")
else:
logging.error(f"Invalid token for user {user_id}")
raise InvalidTokenError(f"Invalid token for user {user_id}")

except SQLAlchemyError as e:
logging.error(f"Database error while unregistering FCM token for user {user_id}:{e}")
session.rollback()
raise e
18 changes: 17 additions & 1 deletion repository/user_repository.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,20 @@
from domain import User, UserType
from domain import User, UserType, session_scope


class UserRepository:

def __init__(self):
pass

def check_user_exists(self, user_id: int) -> bool:
"""
Check if a user exists in the database
:param user_id
:return bool: True if the user exists, False otherwise
"""
with session_scope() as session:
user = session.query(User).filter_by(id=user_id).first()
return user is not None


def get_user_role(session, user_id):
Expand Down

0 comments on commit 9cd3c64

Please sign in to comment.