Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/super-linter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
uses: super-linter/super-linter@v7
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VALIDATE_ALL_CODEBASE: true
VALIDATE_ALL_CODEBASE: false
VALIDATE_JSON_PRETTIER: false
VALIDATE_PYTHON_ISORT: false
VALIDATE_PYTHON_PYLINT: false
Expand Down
4 changes: 2 additions & 2 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
from flask import Flask, jsonify, request
from flask_cors import CORS

from config import Config, limiter
from config.settings import Config, limiter
from routes import register_routes
from utility import extract_error_message
from utility.database import extract_error_message

app = Flask(__name__)
app.config.from_object(Config)
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
14 changes: 4 additions & 10 deletions routes/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,17 @@
from flask import Blueprint, jsonify, request
from pymysql import MySQLError

from config import limiter
from config.settings import limiter
from jwt_helper import (
TokenError,
extract_token_from_header,
generate_access_token,
generate_refresh_token,
verify_token,
)
from utility import (
database_cursor,
encrypt_email,
hash_email,
hash_password,
validate_email,
validate_password,
verify_password,
)
from utility.database import database_cursor
from utility.encryption import encrypt_email, hash_email, hash_password, verify_password
from utility.validation import validate_email, validate_password

authentication_blueprint = Blueprint("authentication", __name__)

Expand Down
2 changes: 1 addition & 1 deletion routes/comment.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from flask import Blueprint, jsonify

from utility import database_cursor
from utility.database import database_cursor

comment_blueprint = Blueprint("comment", __name__)

Expand Down
2 changes: 1 addition & 1 deletion routes/ingredient.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from flask import Blueprint, jsonify

from utility import database_cursor
from utility.database import database_cursor

ingredient_blueprint = Blueprint("ingredient", __name__)

Expand Down
2 changes: 1 addition & 1 deletion routes/language.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from flask import Blueprint, jsonify

from utility import database_cursor
from utility.database import database_cursor

language_blueprint = Blueprint("language", __name__)

Expand Down
6 changes: 3 additions & 3 deletions routes/person.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
from argon2 import exceptions
from flask import Blueprint, jsonify, request

from utility import (
database_cursor,
from utility.database import database_cursor
from utility.encryption import (
decrypt_email,
encrypt_email,
hash_email,
hash_password,
mask_email,
validate_password,
verify_password,
)
from utility.validation import validate_password

person_blueprint = Blueprint("person", __name__)

Expand Down
4 changes: 2 additions & 2 deletions routes/picture.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

from flask import Blueprint, jsonify, request, send_from_directory

from config import Config
from config.settings import Config
from jwt_helper import token_required
from utility import database_cursor
from utility.database import database_cursor

picture_blueprint = Blueprint("picture", __name__)

Expand Down
4 changes: 2 additions & 2 deletions routes/recipe.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from flask import Blueprint, jsonify, request

from config import DEFAULT_PAGE_SIZE
from utility import database_cursor
from config.settings import DEFAULT_PAGE_SIZE
from utility.database import database_cursor

recipe_blueprint = Blueprint("recipe", __name__)

Expand Down
2 changes: 1 addition & 1 deletion routes/recipe_engagement.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from flask import Blueprint, jsonify, request

from utility import database_cursor
from utility.database import database_cursor

recipe_engagement_blueprint = Blueprint("recipe_engagement", __name__)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from utility import decrypt_email, encrypt_email
from utility.encryption import decrypt_email, encrypt_email


def test_decrypt_email_type(sample_email):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from utility import encrypt_email
from utility.encryption import encrypt_email


def test_encrypt_email(sample_email):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from utility import hash_email
from utility.encryption import hash_email


def test_hash_email_type(sample_email):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import pytest

from utility import mask_email
from utility.encryption import mask_email


@pytest.mark.parametrize(
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from argon2 import PasswordHasher

from utility import hash_password
from utility.encryption import hash_password

ph = PasswordHasher()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from argon2 import PasswordHasher
from argon2.exceptions import VerificationError, VerifyMismatchError

from utility import hash_password, verify_password
from utility.encryption import hash_password, verify_password

ph = PasswordHasher()

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from utility import validate_email
from utility.validation import validate_email


def test_valid_emails():
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from argon2 import PasswordHasher

from utility import validate_password
from utility.validation import validate_password

ph = PasswordHasher()

Expand Down
Empty file added utility/__init__.py
Empty file.
29 changes: 29 additions & 0 deletions utility/database.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from contextlib import contextmanager

from config.database import get_db_connection

__all__ = ["database_cursor", "extract_error_message"]


@contextmanager
def database_cursor():
db = get_db_connection()
cursor = db.cursor()
try:
yield cursor
db.commit()
except Exception as e:
db.rollback()
raise e
finally:
cursor.close()
db.close()


def extract_error_message(message):
"""Extracts a user-friendly error message from a database error message."""
try:
cleaner_message = message.split(", ")[1].strip("()'")
return cleaner_message if "SQL" not in cleaner_message else "Database error"
except IndexError:
return "An unknown error occurred"
54 changes: 8 additions & 46 deletions utility.py → utility/encryption.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,27 @@
import hashlib
import os
import re
from contextlib import contextmanager
from re import match

from argon2 import PasswordHasher
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from dotenv import load_dotenv

from db import get_db_connection
__all__ = [
"decrypt_email",
"encrypt_email",
"hash_email",
"hash_password",
"mask_email",
"verify_password",
]

load_dotenv()
ph = PasswordHasher()
AES_KEY = bytes.fromhex(os.getenv("AES_SECRET_KEY", os.urandom(32).hex()))
PEPPER = os.getenv("PEPPER", "SuperSecretPepper").encode("utf-8")


@contextmanager
def database_cursor():
db = get_db_connection()
cursor = db.cursor()
try:
yield cursor
db.commit()
except Exception as e:
db.rollback()
raise e
finally:
cursor.close()
db.close()


def decrypt_email(encrypted_email: str) -> str:
"""Decrypts an AES-256 encrypted email."""
encrypted_data = base64.b64decode(encrypted_email)
Expand All @@ -59,15 +49,6 @@ def encrypt_email(email: str) -> str:
return base64.b64encode(iv + ciphertext).decode()


def extract_error_message(message):
"""Extracts a user-friendly error message from a database error message."""
try:
cleaner_message = message.split(", ")[1].strip("()'")
return cleaner_message if "SQL" not in cleaner_message else "Database error"
except IndexError:
return "An unknown error occurred"


def hash_email(email: str) -> str:
"""Generate a SHA-256 hash of the email (used for fast lookup)."""
return hashlib.sha256(email.encode()).hexdigest()
Expand Down Expand Up @@ -97,25 +78,6 @@ def mask_part(part: str) -> str:
return masked_local + "@" + masked_domain


def validate_email(email: str) -> bool:
"""Validates an email address using a regex pattern."""
pattern = r"^[a-zA-Z0-9._+-]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.[a-zA-Z]{2,}$"
return bool(re.match(pattern, email))


def validate_password(password) -> bool:
"""
Validates a password based on the following criteria:
- At least 12 characters long.
- Contains at least one uppercase letter (A-Z).
- Contains at least one lowercase letter (a-z).
- Contains at least one digit (0-9).
- Contains at least one special character (any non-alphanumeric character).
"""
pattern = r"^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[^A-Za-z0-9]).{12,}$"
return bool(match(pattern, password))


def verify_password(password, stored_password):
peppered_password = password.encode("utf-8") + PEPPER
return ph.verify(stored_password, peppered_password)
22 changes: 22 additions & 0 deletions utility/validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from re import match

__all__ = ["validate_email", "validate_password"]


def validate_email(email: str) -> bool:
"""Validates an email address using a regex pattern."""
pattern = r"^[a-zA-Z0-9._+-]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.[a-zA-Z]{2,}$"
return bool(match(pattern, email))


def validate_password(password) -> bool:
"""
Validates a password based on the following criteria:
- At least 12 characters long.
- Contains at least one uppercase letter (A-Z).
- Contains at least one lowercase letter (a-z).
- Contains at least one digit (0-9).
- Contains at least one special character (any non-alphanumeric character).
"""
pattern = r"^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[^A-Za-z0-9]).{12,}$"
return bool(match(pattern, password))