Merge branch 'testing' of DigiFi/FirstCore into master

This commit is contained in:
2025-04-15 16:13:50 +00:00
committed by Gogs
11 changed files with 225 additions and 17 deletions
+13 -3
View File
@@ -1,7 +1,9 @@
from flask import Blueprint, request, jsonify, send_from_directory from flask import Blueprint, request, jsonify, send_from_directory
from app.api.services import ( from app.api.services import (
AuthorizationService, AuthorizationService,
TransactionService, LoanService, TransactionService,
LoanService,
AuthService
) )
from app.utils.logger import logger from app.utils.logger import logger
from app.api.middlewares import enforce_json, require_auth from app.api.middlewares import enforce_json, require_auth
@@ -36,9 +38,17 @@ def serve_paths(filename):
return send_from_directory(swagger_dir, 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 # Get All Transactions Endpoint
@api.route("/transactions", methods=["GET"]) @api.route("/transactions", methods=["GET"])
# @jwt_required() @jwt_required()
def get_transactions(): def get_transactions():
# Extract query parameters for filtering # Extract query parameters for filtering
filters = { filters = {
@@ -56,7 +66,7 @@ def get_transactions():
# Get All Loans Endpoint # Get All Loans Endpoint
@api.route("/loans", methods=["GET"]) @api.route("/loans", methods=["GET"])
# @jwt_required() @jwt_required()
def get_loans(): def get_loans():
# Extract query parameters for filtering # Extract query parameters for filtering
filters = { filters = {
+1
View File
@@ -3,3 +3,4 @@ from app.api.services.customer_consent import CustomerConsentService
from app.api.services.authorization import AuthorizationService from app.api.services.authorization import AuthorizationService
from app.api.services.transaction import TransactionService from app.api.services.transaction import TransactionService
from app.api.services.loan import LoanService from app.api.services.loan import LoanService
from app.api.services.auth_service import AuthService
+58
View File
@@ -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
+3
View File
@@ -30,6 +30,9 @@ class Config:
JWT_REFRESH_TOKEN_EXPIRES = os.getenv( JWT_REFRESH_TOKEN_EXPIRES = os.getenv(
"JWT_REFRESH_TOKEN_EXPIRES", timedelta(days=30) "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_BROKER = 'dev-events.simbrellang.net:9085'
KAFKA_PAYMENT_TOPIC = 'PROCESS_PAYMENT' KAFKA_PAYMENT_TOPIC = 'PROCESS_PAYMENT'
+2 -1
View File
@@ -2,5 +2,6 @@ from .customer import Customer
from .account import Account from .account import Account
from .loan import Loan from .loan import Loan
from .transaction import Transaction from .transaction import Transaction
from .user import User
__all__ = ['Customer', 'Account', 'Loan', 'Transaction'] __all__ = ['Customer', 'Account', 'Loan', 'Transaction', User]
+57
View File
@@ -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'<User {self.username}>'
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
}
+9
View File
@@ -58,6 +58,9 @@
} }
], ],
"paths": { "paths": {
"/login": {
"$ref": "../swagger/paths/Login.json"
},
"/Authorize": { "/Authorize": {
"$ref": "../swagger/paths/Authorize.json" "$ref": "../swagger/paths/Authorize.json"
}, },
@@ -88,6 +91,12 @@
"AuthorizeRefreshRequest": { "AuthorizeRefreshRequest": {
"$ref": "../swagger/schemas/AuthorizeRefreshRequest.json" "$ref": "../swagger/schemas/AuthorizeRefreshRequest.json"
}, },
"LoginRequest": {
"$ref": "../swagger/schemas/LoginRequest.json"
},
"LoginResponse": {
"$ref": "../swagger/schemas/LoginResponse.json"
},
"LoansResponse": { "LoansResponse": {
"$ref": "../swagger/schemas/LoansResponse.json" "$ref": "../swagger/schemas/LoansResponse.json"
}, },
+42
View File
@@ -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"
}
}
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"type": "object",
"properties": {
"username": {
"type": "string",
"example": "digifiuser"
},
"password": {
"type": "string",
"example": "digifipass"
}
},
"required": ["username", "password"]
}
+13
View File
@@ -0,0 +1,13 @@
{
"type": "object",
"properties": {
"jwt_token": {
"type": "string",
"example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkaWdpZml1c2VyIiwibmFtZSI6ImJhY2tvZmZpY2UgdXNlciIsImlhdCI6MTUxNjIzOTAyMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
},
"name": {
"type": "string",
"example": "backoffice user"
}
}
}
+13 -13
View File
@@ -20,7 +20,7 @@
<stringProp name="HTTPSampler.implementation"></stringProp> <stringProp name="HTTPSampler.implementation"></stringProp>
</ConfigTestElement> </ConfigTestElement>
<hashTree/> <hashTree/>
<Arguments guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true"> <Arguments guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables">
<collectionProp name="Arguments.arguments"> <collectionProp name="Arguments.arguments">
<elementProp name="customer_id" elementType="Argument"> <elementProp name="customer_id" elementType="Argument">
<stringProp name="Argument.name">customer_id</stringProp> <stringProp name="Argument.name">customer_id</stringProp>
@@ -60,18 +60,18 @@
</collectionProp> </collectionProp>
</Arguments> </Arguments>
<hashTree/> <hashTree/>
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Loan Endpoint Test" enabled="true"> <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Loan Endpoint Test">
<intProp name="ThreadGroup.num_threads">10</intProp> <intProp name="ThreadGroup.num_threads">10</intProp>
<intProp name="ThreadGroup.ramp_time">5</intProp> <intProp name="ThreadGroup.ramp_time">1</intProp>
<boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp> <boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>
<stringProp name="ThreadGroup.on_sample_error">continue</stringProp> <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
<elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller"> <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller">
<stringProp name="LoopController.loops">10</stringProp> <stringProp name="LoopController.loops">1</stringProp>
<boolProp name="LoopController.continue_forever">false</boolProp> <boolProp name="LoopController.continue_forever">false</boolProp>
</elementProp> </elementProp>
</ThreadGroup> </ThreadGroup>
<hashTree> <hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get All Loans"> <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get All Loans" enabled="true">
<stringProp name="HTTPSampler.path">/loans</stringProp> <stringProp name="HTTPSampler.path">/loans</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp> <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<stringProp name="HTTPSampler.method">GET</stringProp> <stringProp name="HTTPSampler.method">GET</stringProp>
@@ -102,7 +102,7 @@
</JSONPathAssertion> </JSONPathAssertion>
<hashTree/> <hashTree/>
</hashTree> </hashTree>
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Loans with Filters"> <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Get Loans with Filters" enabled="true">
<stringProp name="HTTPSampler.path">/loans</stringProp> <stringProp name="HTTPSampler.path">/loans</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp> <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<stringProp name="HTTPSampler.method">GET</stringProp> <stringProp name="HTTPSampler.method">GET</stringProp>
@@ -128,7 +128,7 @@
</elementProp> </elementProp>
</HTTPSamplerProxy> </HTTPSamplerProxy>
<hashTree> <hashTree>
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Response Code Assertion"> <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Response Code Assertion" enabled="true">
<collectionProp name="Asserion.test_strings"> <collectionProp name="Asserion.test_strings">
<stringProp name="49586">200</stringProp> <stringProp name="49586">200</stringProp>
</collectionProp> </collectionProp>
@@ -138,7 +138,7 @@
<intProp name="Assertion.test_type">8</intProp> <intProp name="Assertion.test_type">8</intProp>
</ResponseAssertion> </ResponseAssertion>
<hashTree/> <hashTree/>
<JSONPathAssertion guiclass="JSONPathAssertionGui" testclass="JSONPathAssertion" testname="JSON Path Assertion"> <JSONPathAssertion guiclass="JSONPathAssertionGui" testclass="JSONPathAssertion" testname="JSON Path Assertion" enabled="true">
<stringProp name="JSON_PATH">$.loans</stringProp> <stringProp name="JSON_PATH">$.loans</stringProp>
<stringProp name="EXPECTED_VALUE"></stringProp> <stringProp name="EXPECTED_VALUE"></stringProp>
<boolProp name="JSONVALIDATION">false</boolProp> <boolProp name="JSONVALIDATION">false</boolProp>
@@ -148,7 +148,7 @@
</JSONPathAssertion> </JSONPathAssertion>
<hashTree/> <hashTree/>
</hashTree> </hashTree>
<ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="View Results Tree"> <ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="View Results Tree" enabled="true">
<boolProp name="ResultCollector.error_logging">false</boolProp> <boolProp name="ResultCollector.error_logging">false</boolProp>
<objProp> <objProp>
<name>saveConfig</name> <name>saveConfig</name>
@@ -185,7 +185,7 @@
<stringProp name="filename"></stringProp> <stringProp name="filename"></stringProp>
</ResultCollector> </ResultCollector>
<hashTree/> <hashTree/>
<ResultCollector guiclass="SummaryReport" testclass="ResultCollector" testname="Summary Report"> <ResultCollector guiclass="SummaryReport" testclass="ResultCollector" testname="Summary Report" enabled="true">
<boolProp name="ResultCollector.error_logging">false</boolProp> <boolProp name="ResultCollector.error_logging">false</boolProp>
<objProp> <objProp>
<name>saveConfig</name> <name>saveConfig</name>
@@ -298,7 +298,7 @@
</elementProp> </elementProp>
</HTTPSamplerProxy> </HTTPSamplerProxy>
<hashTree> <hashTree>
<ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Response Code Assertion" enabled="true"> <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Response Code Assertion">
<collectionProp name="Asserion.test_strings"> <collectionProp name="Asserion.test_strings">
<stringProp name="49586">200</stringProp> <stringProp name="49586">200</stringProp>
</collectionProp> </collectionProp>
@@ -308,7 +308,7 @@
<intProp name="Assertion.test_type">8</intProp> <intProp name="Assertion.test_type">8</intProp>
</ResponseAssertion> </ResponseAssertion>
<hashTree/> <hashTree/>
<JSONPathAssertion guiclass="JSONPathAssertionGui" testclass="JSONPathAssertion" testname="JSON Path Assertion" enabled="true"> <JSONPathAssertion guiclass="JSONPathAssertionGui" testclass="JSONPathAssertion" testname="JSON Path Assertion">
<stringProp name="JSON_PATH">$.transactions</stringProp> <stringProp name="JSON_PATH">$.transactions</stringProp>
<stringProp name="EXPECTED_VALUE"></stringProp> <stringProp name="EXPECTED_VALUE"></stringProp>
<boolProp name="JSONVALIDATION">false</boolProp> <boolProp name="JSONVALIDATION">false</boolProp>
@@ -355,7 +355,7 @@
<stringProp name="filename"></stringProp> <stringProp name="filename"></stringProp>
</ResultCollector> </ResultCollector>
<hashTree/> <hashTree/>
<ResultCollector guiclass="SummaryReport" testclass="ResultCollector" testname="Summary Report" enabled="true"> <ResultCollector guiclass="SummaryReport" testclass="ResultCollector" testname="Summary Report">
<boolProp name="ResultCollector.error_logging">false</boolProp> <boolProp name="ResultCollector.error_logging">false</boolProp>
<objProp> <objProp>
<name>saveConfig</name> <name>saveConfig</name>