diff --git a/app/api/routes/routes.py b/app/api/routes/routes.py index e5b2e1a..8298052 100644 --- a/app/api/routes/routes.py +++ b/app/api/routes/routes.py @@ -1,7 +1,9 @@ from flask import Blueprint, request, jsonify, send_from_directory from app.api.services import ( AuthorizationService, - TransactionService, LoanService, + TransactionService, + LoanService, + AuthService ) from app.utils.logger import logger from app.api.middlewares import enforce_json, require_auth @@ -36,9 +38,17 @@ def serve_paths(filename): return send_from_directory(swagger_dir, filename) +# Login endpoint +@api.route("/login", methods=["POST"]) +def login(): + data = request.get_json() + response = AuthService.login(data) + return response + + # Get All Transactions Endpoint @api.route("/transactions", methods=["GET"]) -# @jwt_required() +@jwt_required() def get_transactions(): # Extract query parameters for filtering filters = { @@ -56,7 +66,7 @@ def get_transactions(): # Get All Loans Endpoint @api.route("/loans", methods=["GET"]) -# @jwt_required() +@jwt_required() def get_loans(): # Extract query parameters for filtering filters = { diff --git a/app/api/services/__init__.py b/app/api/services/__init__.py index 35aa334..70d8957 100644 --- a/app/api/services/__init__.py +++ b/app/api/services/__init__.py @@ -3,3 +3,4 @@ from app.api.services.customer_consent import CustomerConsentService from app.api.services.authorization import AuthorizationService from app.api.services.transaction import TransactionService from app.api.services.loan import LoanService +from app.api.services.auth_service import AuthService diff --git a/app/api/services/auth_service.py b/app/api/services/auth_service.py new file mode 100644 index 0000000..33faf54 --- /dev/null +++ b/app/api/services/auth_service.py @@ -0,0 +1,58 @@ +from flask import jsonify +from app.utils.logger import logger +from app.api.services.base_service import BaseService +from app.models.user import User +from flask_jwt_extended import create_access_token +from datetime import timedelta + + +class AuthService(BaseService): + @staticmethod + def login(data): + """ + Process the login request. + + Args: + data (dict): Login credentials including username and password. + + Returns: + dict: A standardized response with JWT token and user information. + """ + try: + # Extract credentials + username = data.get('username') + password = data.get('password') + + # Validate input + if not username or not password: + return jsonify({ + "message": "Username and password are required" + }), 400 + + # Get user by username + user = User.get_user_by_username(username) + + # Check if user exists and password is correct + if not user or not user.check_password(password): + return jsonify({ + "message": "Invalid username or password" + }), 401 + + # Create JWT token with 15 minute expiration + access_token = create_access_token( + identity=user.username, + expires_delta=timedelta(minutes=15), + additional_claims={"name": user.name} + ) + + # Return token and user information + return { + "jwt_token": access_token, + "name": user.name + } + + except Exception as e: + logger.error(f"An error occurred during login: {str(e)}", exc_info=True) + return jsonify({ + "message": "Internal Server Error" + }), 500 \ No newline at end of file diff --git a/app/config.py b/app/config.py index b23548a..b0d43e4 100644 --- a/app/config.py +++ b/app/config.py @@ -30,6 +30,9 @@ class Config: JWT_REFRESH_TOKEN_EXPIRES = os.getenv( "JWT_REFRESH_TOKEN_EXPIRES", timedelta(days=30) ) + JWT_TOKEN_LOCATION = ["headers"] + JWT_HEADER_NAME = "Authorization" + JWT_HEADER_TYPE = "Bearer" KAFKA_BROKER = 'dev-events.simbrellang.net:9085' KAFKA_PAYMENT_TOPIC = 'PROCESS_PAYMENT' diff --git a/app/models/__init__.py b/app/models/__init__.py index 8fd1277..9fbb27f 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -2,5 +2,6 @@ from .customer import Customer from .account import Account from .loan import Loan from .transaction import Transaction +from .user import User -__all__ = ['Customer', 'Account', 'Loan', 'Transaction'] \ No newline at end of file +__all__ = ['Customer', 'Account', 'Loan', 'Transaction', User] \ No newline at end of file diff --git a/app/models/user.py b/app/models/user.py new file mode 100644 index 0000000..ccc61ed --- /dev/null +++ b/app/models/user.py @@ -0,0 +1,57 @@ +from datetime import datetime, timezone +from app.extensions import db +from werkzeug.security import generate_password_hash, check_password_hash + + +class User(db.Model): + __tablename__ = 'users' + + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + username = db.Column(db.String(50), unique=True, nullable=False) + password_hash = db.Column(db.String(255), nullable=False) + name = db.Column(db.String(100), nullable=False) + created_at = db.Column(db.DateTime, default=datetime.now(timezone.utc)) + updated_at = db.Column(db.DateTime, default=datetime.now(timezone.utc), onupdate=datetime.now(timezone.utc)) + + def __repr__(self): + return f'' + + def set_password(self, password): + self.password_hash = generate_password_hash(password) + + def check_password(self, password): + return check_password_hash(self.password_hash, password) + + @classmethod + def create_user(cls, username, password, name): + # Check if user already exists + if cls.query.filter_by(username=username).first(): + raise ValueError("Username already exists") + + user = cls( + username=username, + name=name + ) + user.set_password(password) + + try: + db.session.add(user) + db.session.commit() + except Exception as e: + db.session.rollback() + raise ValueError(f"Error creating user: {str(e)}") + + return user + + @classmethod + def get_user_by_username(cls, username): + return cls.query.filter_by(username=username).first() + + def to_dict(self): + return { + 'id': self.id, + 'username': self.username, + 'name': self.name, + 'created_at': self.created_at.isoformat() if self.created_at else None, + 'updated_at': self.updated_at.isoformat() if self.updated_at else None + } \ No newline at end of file diff --git a/app/swagger/digifi_swagger.json b/app/swagger/digifi_swagger.json index 139fb9c..c973c48 100644 --- a/app/swagger/digifi_swagger.json +++ b/app/swagger/digifi_swagger.json @@ -58,6 +58,9 @@ } ], "paths": { + "/login": { + "$ref": "../swagger/paths/Login.json" + }, "/Authorize": { "$ref": "../swagger/paths/Authorize.json" }, @@ -88,6 +91,12 @@ "AuthorizeRefreshRequest": { "$ref": "../swagger/schemas/AuthorizeRefreshRequest.json" }, + "LoginRequest": { + "$ref": "../swagger/schemas/LoginRequest.json" + }, + "LoginResponse": { + "$ref": "../swagger/schemas/LoginResponse.json" + }, "LoansResponse": { "$ref": "../swagger/schemas/LoansResponse.json" }, diff --git a/app/swagger/paths/Login.json b/app/swagger/paths/Login.json new file mode 100644 index 0000000..290a6b7 --- /dev/null +++ b/app/swagger/paths/Login.json @@ -0,0 +1,42 @@ +{ + "post": { + "tags": [ + "Authentication" + ], + "summary": "User login", + "description": "Authenticate a user and return a JWT token", + "operationId": "login", + "requestBody": { + "description": "User credentials", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/LoginRequest.json" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "../schemas/LoginResponse.json" + } + } + } + }, + "400": { + "description": "Invalid request" + }, + "401": { + "description": "Invalid username or password" + }, + "500": { + "description": "Internal server error" + } + } + } +} \ No newline at end of file diff --git a/app/swagger/schemas/LoginRequest.json b/app/swagger/schemas/LoginRequest.json new file mode 100644 index 0000000..8200994 --- /dev/null +++ b/app/swagger/schemas/LoginRequest.json @@ -0,0 +1,14 @@ +{ + "type": "object", + "properties": { + "username": { + "type": "string", + "example": "digifiuser" + }, + "password": { + "type": "string", + "example": "digifipass" + } + }, + "required": ["username", "password"] +} \ No newline at end of file diff --git a/app/swagger/schemas/LoginResponse.json b/app/swagger/schemas/LoginResponse.json new file mode 100644 index 0000000..a5e2b38 --- /dev/null +++ b/app/swagger/schemas/LoginResponse.json @@ -0,0 +1,13 @@ +{ + "type": "object", + "properties": { + "jwt_token": { + "type": "string", + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkaWdpZml1c2VyIiwibmFtZSI6ImJhY2tvZmZpY2UgdXNlciIsImlhdCI6MTUxNjIzOTAyMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" + }, + "name": { + "type": "string", + "example": "backoffice user" + } + } +} \ No newline at end of file diff --git a/jmeter/digifi_first_core_test_plan.jmx b/jmeter/digifi_first_core_test_plan.jmx index 3f44273..c234bb0 100644 --- a/jmeter/digifi_first_core_test_plan.jmx +++ b/jmeter/digifi_first_core_test_plan.jmx @@ -20,7 +20,7 @@ - + customer_id @@ -60,18 +60,18 @@ - + 10 - 5 + 1 true continue - 10 + 1 false - + /loans true GET @@ -102,7 +102,7 @@ - + /loans true GET @@ -128,7 +128,7 @@ - + 200 @@ -138,7 +138,7 @@ 8 - + $.loans false @@ -148,7 +148,7 @@ - + false saveConfig @@ -185,7 +185,7 @@ - + false saveConfig @@ -298,7 +298,7 @@ - + 200 @@ -308,7 +308,7 @@ 8 - + $.transactions false @@ -355,7 +355,7 @@ - + false saveConfig