diff --git a/backend/api/Auth-service/app.py b/backend/api/Auth-service/app.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/api/Auth-service/auth_service.py b/backend/api/Auth-service/auth_service.py new file mode 100644 index 0000000..b9101cc --- /dev/null +++ b/backend/api/Auth-service/auth_service.py @@ -0,0 +1,65 @@ +# auth_service.py + +from utils.jwt_manager import JWTManager +from utils.db import get_user_by_username +from passlib.context import CryptContext + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + + +class AuthService: + """ + Service class for handling authentication-related operations. + + This class provides methods for user login, token validation, and logout. + """ + def __init__(self): + """ + Initializes the AuthService with a JWTManager instance. + """ + self.jwt_manager = JWTManager() + + def login(self, username: str, password: str) -> str | None: + """ + Authenticates a user and generates a JWT token if credentials are valid + + Args: + username (str): The username of the user. + password (str): The password of the user. + + Returns: + str None: A JWT token if authentication is successful, none otherwise. + """ + user = get_user_by_username(username) + if not user: + return None + + if not pwd_context.verify(password, user["password_hash"]): + return None + + token = self.jwt_manager.generate_token({"sub": username}) + return token + + def validate_token(self, token: str) -> dict | None: + """ + Validates a JWT token and decodes its payload. + + Args: + token (str): The JWT token to validate. + + Returns: + dict None: The decoded payload if the token is valid, or None otherwise + """ + return self.jwt_manager.verify_token(token) + + def logout(self, token: str) -> bool: + """ + Logs out a user by invalidating their token. + + Args: + token (str): The token to invalidate. + + Returns: + bool: True if the logout process is successful. + """ + return True diff --git a/backend/api/Auth-service/main.py b/backend/api/Auth-service/main.py new file mode 100644 index 0000000..9b0b176 --- /dev/null +++ b/backend/api/Auth-service/main.py @@ -0,0 +1,63 @@ +from fastapi import APIRouter, HTTPException, Depends +from auth_service import AuthService +from models import LoginRequest, TokenResponse +from utils.jwt_manager import get_current_user + +router = APIRouter() +auth_service = AuthService() + + +@router.post("/login", response_model=TokenResponse) +def login_route(request: LoginRequest): + """ + Endpoint for user login. + + Args: + request (LoginRequest): The login request containing username and password. + + Returns: + TokenResponse: A response containing the access token if login is done. + + Raises: + HTTPException: If the credentials are invalid. + """ + token = auth_service.login(request.username, request.password) + if not token: + raise HTTPException(status_code=401, detail="Invalid credentials") + return TokenResponse(access_token=token) + + +@router.get("/validate") +def validate_route(user=Depends(get_current_user)): + """ + Endpoint to validate a JWT token. + + Args: + user: The user information extracted from the token (injected by Depends). + + Returns: + dict: A message indicating the token is valid and the user information. + """ + return {"message": f"Token válido. Usuario: {user['sub']}"} + + return {"message": f"Token válido. Usuario: {user['sub']}"} + + +@router.post("/logout") +def logout_route(token: str): + """ + Endpoint for user logout. + + Args: + token (str): The token to invalidate. + + Returns: + dict: A message indicating the session was closed successfully. + + Raises: + HTTPException: If the logout process fails. + """ + success = auth_service.logout(token) + if not success: + raise HTTPException(status_code=400, detail="Logout failed") + return {"message": "Sesión cerrada correctamente"} diff --git a/backend/api/Auth-service/models/schemas b/backend/api/Auth-service/models/schemas new file mode 100644 index 0000000..356dde0 --- /dev/null +++ b/backend/api/Auth-service/models/schemas @@ -0,0 +1,35 @@ +from pydantic import BaseModel + + +class LoginRequest(BaseModel): + """ + Schema for a login request. + + Attributes: + username (str): The username of the user. + password (str): The password of the user. + """ + username: str + password: str + + +class TokenResponse(BaseModel): + """ + Schema for a token response. + + Attributes: + access_token (str): The access token issued to the user. + token_type (str): The type of the token, default is "bearer". + """ + access_token: str + token_type: str = "bearer" + + +class TokenValidationRequest(BaseModel): + """ + Schema for a token validation request. + + Attributes: + token (str): The token to be validated. + """ + token: str diff --git a/backend/api/Auth-service/utils/db.py b/backend/api/Auth-service/utils/db.py new file mode 100644 index 0000000..2a9667a --- /dev/null +++ b/backend/api/Auth-service/utils/db.py @@ -0,0 +1,44 @@ +import psycopg2 +import os +from dotenv import load_dotenv + +load_dotenv() + + +def get_connection(): + """ + Establishes a connection to the PostgreSQL database. + + Returns: + psycopg2.extensions.connection: A connection object to interact with db. + """ + return psycopg2.connect( + host=os.getenv("DB_HOST"), + port=os.getenv("DB_PORT"), + user=os.getenv("DB_USER"), + password=os.getenv("DB_PASSWORD"), + dbname=os.getenv("DB_NAME") + ) + + +def get_user_by_username(username: str) -> dict | None: + """ + Retrieves a user's details from the database by their username. + + Args: + username (str): The username of the user to retrieve. + + Returns: + dict None: A dictionary containing the usernames and passwords. + """ + conn = get_connection() + try: + with conn.cursor() as cur: + cur.execute( + "SELECT username, password_hash FROM users = %s", (username,)) + row = cur.fetchone() + if row: + return {"username": row[0], "password_hash": row[1]} + finally: + conn.close() + return None diff --git a/backend/api/Auth-service/utils/jwt_manager.py b/backend/api/Auth-service/utils/jwt_manager.py new file mode 100644 index 0000000..2bb1562 --- /dev/null +++ b/backend/api/Auth-service/utils/jwt_manager.py @@ -0,0 +1,52 @@ +import jwt +from datetime import datetime, timedelta +from dotenv import load_dotenv +import os + + +load_dotenv() + +SECRET_KEY = os.getenv("JWT_SECRET", "secretkey") +ALGORITHM = "HS256" +TOKEN_EXPIRE_MINUTES = 60 + + +class JWTManager: + """ + A utility class for managing JSON Web Tokens (JWT). + + This class provides methods to generate and verify JWTs using a secret key + and specified algorithm. + """ + def generate_token(self, data: dict) -> str: + """ + Generates a JWT with the given data and expiration time. + + Args: + data (dict): The payload data to include in the token. + + Returns: + str: The encoded JWT as a string. + """ + to_encode = data.copy() + expire = datetime.utcnow() + timedelta(minutes=TOKEN_EXPIRE_MINUTES) + to_encode.update({"exp": expire}) + return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + + def verify_token(self, token: str) -> dict | None: + """ + Verifies and decodes a JWT. + + Args: + token (str): The JWT to verify. + + Returns: + dict None:The decoded payload if the token is valid, or None if no. + """ + try: + return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + except jwt.ExpiredSignatureError: + print("Expired Token") + except jwt.InvalidTokenError: + print("Invalid token") + return None diff --git a/backend/api/Notifications-service/app.py b/backend/api/Notifications-service/app.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/api/Notifications-service/main.py b/backend/api/Notifications-service/main.py new file mode 100644 index 0000000..01a9ee8 --- /dev/null +++ b/backend/api/Notifications-service/main.py @@ -0,0 +1,74 @@ +""" +Main module for the Notifications service API. + +This module defines the FastAPI application and its routes for sending emails +and push notifications. It uses the NotificationService to handle the actual +sending of notifications. + +Routes: + - POST /email: Sends an email notification. + - POST /push: Sends a push notification. +""" + +from fastapi import FastAPI, APIRouter, HTTPException +from notification_service import NotificationService +from models import EmailRequest, PushRequest + +app = FastAPI() +router = APIRouter() +service = NotificationService() + + +@router.post("/email") +def send_email(request: EmailRequest): + """ + Endpoint to send an email notification. + + Args: + request (EmailRequest): The email request containing subject, and body. + + Returns: + dict: A success message if the email is sent successfully. + + Raises: + HTTPException: If the email fails to send. + """ + success = service.send_email(request.to, request.subject, request.body) + if not success: + raise HTTPException(status_code=500, detail="Failed to send email") + return {"message": "Email sent"} + + +@router.post("/push") +def send_push(request: PushRequest): + """ + Endpoint to send a push notification. + + Args: + request(PushRequest): The push request containing user ID and message. + + Returns: + dict: A success message if the push notification is sent successfully. + + Raises: + HTTPException: If the push notification fails to send. + """ + success = service.send_push( + request.user_id, request.title, request.message) + if not success: + raise HTTPException( + status_code=500, detail="Failed to send push notification") + return {"message": "Push notification sent"} + + +app.include_router(router) + + +if __name__ == "_main_": + """ + Entry point for running the FastAPI application. + + The application is served using Uvicorn on host 0.0.0.0 and port 8000. + """ + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/backend/api/Notifications-service/models/schemas.py b/backend/api/Notifications-service/models/schemas.py new file mode 100644 index 0000000..9d3c114 --- /dev/null +++ b/backend/api/Notifications-service/models/schemas.py @@ -0,0 +1,29 @@ +from pydantic import BaseModel + + +class EmailRequest(BaseModel): + """ + Schema for an email request. + + Attributes: + to (str): The recipient's email address. + subject (str): The subject of the email. + body (str): The body content of the email. + """ + to: str + subject: str + body: str + + +class PushRequest(BaseModel): + """ + Schema for a push notification request. + + Attributes: + user_id (str): The ID of the user to receive the notification. + title (str): The title of the push notification. + message (str): The message content of the push notification. + """ + user_id: str + title: str + message: str diff --git a/backend/api/Notifications-service/notification_service.py b/backend/api/Notifications-service/notification_service.py new file mode 100644 index 0000000..94b295f --- /dev/null +++ b/backend/api/Notifications-service/notification_service.py @@ -0,0 +1,38 @@ +from utils.email_sender import send_email +from utils.push_sender import send_push_notification + + +class NotificationService: + """ + Service class for handling notifications. + + This class provides methods to send email and push notifications + using the underlying utility functions. + """ + def send_email(self, to: str, subject: str, body: str) -> bool: + """ + Sends an email notification. + + Args: + to (str): The recipient's email address. + subject (str): The subject of the email. + body (str): The body content of the email. + + Returns: + bool: True if the email was sent successfully, False otherwise. + """ + return send_email(to, subject, body) + + def send_push(self, user_id: str, title: str, message: str) -> bool: + """ + Sends a push notification. + + Args: + user_id (str): The ID of the user to receive the notification. + title (str): The title of the push notification. + message (str): The message content of the push notification. + + Returns: + bool:True if the push notification was sent successfully, False if not. + """ + return send_push_notification(user_id, title, message) diff --git a/backend/api/Notifications-service/tests/tests_notifications.py b/backend/api/Notifications-service/tests/tests_notifications.py new file mode 100644 index 0000000..8c19760 --- /dev/null +++ b/backend/api/Notifications-service/tests/tests_notifications.py @@ -0,0 +1,38 @@ +from fastapi.testclient import TestClient +from models import app + +client = TestClient(app) + + +def test_send_email_success(): + """ + Test case for sending an email notification successfully. + + Sends a POST request to the /email endpoint with valid data and + verifies that the response status code is 200 and the response + message indicates success. + """ + response = client.post("/email", json={ + "to": "test@example.com", + "subject": "Test", + "body": "This is a test email." + }) + assert response.status_code == 200 + assert response.json() == {"message": "Email sent"} + + +def test_send_push_success(): + """ + Test case for sending a push notification successfully. + + Sends a POST request to the /push endpoint with valid data and + verifies that the response status code is 200 and the response + message indicates success. + """ + response = client.post("/push", json={ + "user_id": "user123", + "title": "Hola", + "message": "Tienes una notificación " + }) + assert response.status_code == 200 + assert response.json() == {"message": "Push notification sent"} diff --git a/backend/api/Notifications-service/utils/__init__.py b/backend/api/Notifications-service/utils/__init__.py new file mode 100644 index 0000000..a80bf72 --- /dev/null +++ b/backend/api/Notifications-service/utils/__init__.py @@ -0,0 +1,20 @@ +""" +Utilities module for the Notifications service. + +This module provides utility functions for sending emails, push notifications, +and listening to message queues. + +Exports: + - send_email: Function to send an email. + - send_push_notification: Function to send a push notification. + - start_listening: Function to start listening to a message queue. +""" +from .email_sender import send_email +from .push_sender import send_push_notification +from .mq_listener import start_listening + +__all__ = [ + "send_email", + "send_push_notification", + "start_listening" +] diff --git a/backend/api/Notifications-service/utils/email_sender.py b/backend/api/Notifications-service/utils/email_sender.py new file mode 100644 index 0000000..0f5c542 --- /dev/null +++ b/backend/api/Notifications-service/utils/email_sender.py @@ -0,0 +1,36 @@ +import smtplib +from email.mime.text import MIMEText + +SMTP_SERVER = "smtp.gmail.com" +SMTP_PORT = 587 +SMTP_USER = "tu-email@gmail.com" +SMTP_PASSWORD = "tu-contraseña" + + +def send_email(to: str, subject: str, body: str) -> bool: + """ + Sends an email using the configured SMTP server. + + Args: + to (str): The recipient's email address. + subject (str): The subject of the email. + body (str): The body content of the email. + + Returns: + bool: True if the email was sent successfully, False otherwise. + """ + try: + msg = MIMEText(body) + msg["Subject"] = subject + msg["From"] = SMTP_USER + msg["To"] = to + + server = smtplib.SMTP(SMTP_SERVER, SMTP_PORT) + server.starttls() + server.login(SMTP_USER, SMTP_PASSWORD) + server.sendmail(SMTP_USER, [to], msg.as_string()) + server.quit() + return True + except Exception as e: + print(f"Error sending email: {e}") + return False diff --git a/backend/api/Notifications-service/utils/mq_listener.py b/backend/api/Notifications-service/utils/mq_listener.py new file mode 100644 index 0000000..f7ba9a8 --- /dev/null +++ b/backend/api/Notifications-service/utils/mq_listener.py @@ -0,0 +1,41 @@ +import threading +import pika + + +def callback(ch, method, properties, body): + """ + Callback function to process messages from the RabbitMQ queue. + + Args: + ch: The channel object. + method: Delivery method. + properties: Message properties. + body: The message body. + """ + print(f"Received message: {body}") + + +def start_listener(): + """ + Starts a RabbitMQ listener in a separate thread. + + The listener connects to a RabbitMQ server, declares a queue, and consumes + messages from the 'notification_queue'. Messages are processed using the + `callback` function. + """ + def run(): + connection = pika.BlockingConnection( + pika.ConnectionParameters('localhost')) + channel = connection.channel() + channel.queue_declare(queue='notification_queue') + + channel.basic_consume( + queue='notification_queue', + on_message_callback=callback, + auto_ack=True) + + print('RabbitMQ listener running...') + channel.start_consuming() + + thread = threading.Thread(target=run) + thread.start() diff --git a/backend/api/Notifications-service/utils/push_sender.py b/backend/api/Notifications-service/utils/push_sender.py new file mode 100644 index 0000000..39266ba --- /dev/null +++ b/backend/api/Notifications-service/utils/push_sender.py @@ -0,0 +1,34 @@ +import firebase_admin +from firebase_admin import messaging, credentials + + +cred = credentials.Certificate("firebase_credentials.json") +firebase_admin.initialize_app(cred) + + +def send_push_notification(user_id: str, title: str, message: str) -> bool: + """ + Sends a push notification to a specific user using Firebase Cloud Messaging + + Args: + user_id (str): The ID of the user to receive the notification. + title (str): The title of the push notification. + message (str): The message content of the push notification. + + Returns: + bool: True if the push notification was sent successfully, False otherwise. + """ + try: + message = messaging.Message( + notification=messaging.Notification( + title=title, + body=message, + ), + topic=user_id + ) + response = messaging.send(message) + print(f"Push sent: {response}") + return True + except Exception as e: + print(f"Error sending push: {e}") + return False