From 2744ff74e2f58ae16e9a86bdd60211c3b5ce0b0d Mon Sep 17 00:00:00 2001 From: Ben Juntilla Date: Wed, 11 Feb 2026 16:20:05 -0700 Subject: [PATCH 1/3] refactor: split shared.py god module into focused utility modules - Extract Flask app + CORS config into modules/utils/app.py with ExtendedFlask subclass - Add config singleton to modules/utils/config.py - Add db_connect singleton to modules/utils/db.py - Add token_manager singleton to modules/utils/TokenManager.py - Extract Notion client into modules/utils/notion.py - Move token cleanup scheduler into main.py initialize_app() - Reduce shared.py to thin re-export shim for backward compatibility - Remove shared.pyi (no longer needed with ExtendedFlask subclass) - Fix circular import in TokenManager (import from modules.utils.db instead of shared) Amp-Thread-ID: https://ampcode.com/threads/T-019c4efd-7298-7219-a8fd-4b87c3e8bee1 Co-authored-by: Amp --- main.py | 16 +- modules/utils/TokenManager.py | 711 +++++++++++++++++----------------- modules/utils/app.py | 41 ++ modules/utils/config.py | 187 ++++----- modules/utils/db.py | 5 + modules/utils/notion.py | 5 + shared.py | 105 +---- shared.pyi | 26 -- 8 files changed, 535 insertions(+), 561 deletions(-) create mode 100644 modules/utils/app.py create mode 100644 modules/utils/notion.py delete mode 100644 shared.pyi diff --git a/main.py b/main.py index 751e5e20..c9acf754 100644 --- a/main.py +++ b/main.py @@ -2,6 +2,7 @@ import os import subprocess # nosec B404 - subprocess needed for git commit hash retrieval import threading +import time from datetime import UTC, datetime import discord @@ -17,7 +18,7 @@ from modules.storefront.api import storefront_blueprint from modules.superadmin.api import superadmin_blueprint from modules.users.api import users_blueprint -from shared import app, config, create_auth_bot, logger +from shared import app, config, create_auth_bot, logger, tokenManager # Set a secret key for session management app.secret_key = os.environ.get("FLASK_SECRET_KEY", "dev-secret-key") @@ -111,6 +112,19 @@ def run_auth_bot_in_thread(): # --- App Initialization --- def initialize_app(): + # Start token cleanup scheduler in background thread + def run_cleanup_scheduler(): + while True: + try: + tokenManager.cleanup_expired_refresh_tokens() + logger.info("Cleaned up expired refresh tokens") + except Exception as e: + logger.error(f"Error cleaning up expired tokens: {e}") + time.sleep(3600) + + cleanup_thread = threading.Thread(target=run_cleanup_scheduler, daemon=True) + cleanup_thread.start() + auth_thread = threading.Thread(target=run_auth_bot_in_thread, name="AuthBotThread") auth_thread.daemon = True auth_thread.start() diff --git a/modules/utils/TokenManager.py b/modules/utils/TokenManager.py index 5082b32b..acf220a1 100644 --- a/modules/utils/TokenManager.py +++ b/modules/utils/TokenManager.py @@ -1,354 +1,357 @@ -import datetime -import hashlib -import os -import secrets - -import jwt -from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric import rsa - -from modules.utils.logging_config import get_logger - -logger = get_logger(__name__) - - -class TokenManager: - def __init__(self, algorithm="RS256", keys_path="./data") -> None: - self.algorithm = algorithm - self.keys_path = keys_path - self.private_key_file = os.path.join(keys_path, "jwt_private.pem") - self.public_key_file = os.path.join(keys_path, "jwt_public.pem") - self.private_key, self.public_key = self.load_or_generate_keys() - self.blacklist = set() - self._db_connect = None - - def _get_db_connect(self): - """Lazily import db_connect to avoid circular imports""" - if self._db_connect is None: - from shared import db_connect - - self._db_connect = db_connect - return self._db_connect - - def _get_db_session(self): - """Get a database session""" - db_connect = self._get_db_connect() - return db_connect.SessionLocal() - - @staticmethod - def _hash_token(token): - """Hash a refresh token using SHA-256 for secure storage""" - return hashlib.sha256(token.encode("utf-8")).hexdigest() - - def load_or_generate_keys(self): - """Load keys from disk if they exist, otherwise generate and save new ones""" - # Check if both key files exist - if os.path.exists(self.private_key_file) and os.path.exists(self.public_key_file): - try: - # Load existing keys - with open(self.private_key_file, encoding="utf-8") as f: - private_key = f.read() - with open(self.public_key_file, encoding="utf-8") as f: - public_key = f.read() - logger.info(f"Loaded existing RSA keys from {self.keys_path}") - return private_key, public_key - except Exception as e: - logger.error(f"Error loading keys: {e}. Generating new keys...") - - # Generate new keys if loading failed or files don't exist - private_key, public_key = self.generate_keys() - - # Save keys to disk - try: - os.makedirs(self.keys_path, exist_ok=True) - with open(self.private_key_file, "w") as f: - f.write(private_key) - with open(self.public_key_file, "w") as f: - f.write(public_key) - # Set restrictive permissions on private key - os.chmod(self.private_key_file, 0o600) - logger.info(f"Generated and saved new RSA keys to {self.keys_path}") - except Exception as e: - logger.warning(f"Could not save keys to disk: {e}") - - return private_key, public_key - - def generate_keys(self): - """Generate a new RSA key pair""" - # Generate a private RSA key - private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) - - # Generate the corresponding public key - public_key = private_key.public_key() - - # Serialize private key to PEM format - private_pem = private_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.PKCS8, - encryption_algorithm=serialization.NoEncryption(), - ) - - # Serialize public key to PEM format - public_pem = public_key.public_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PublicFormat.SubjectPublicKeyInfo, - ) - - return private_pem.decode("utf-8"), public_pem.decode("utf-8") - - def generate_token_pair(self, username, discord_id=None, access_exp_minutes=30, refresh_exp_days=7): - """ - Generate both access token and refresh token. - - Args: - username (str): The user's display name - discord_id (str): The user's Discord ID (recommended for security) - access_exp_minutes (int): Access token expiration time in minutes - refresh_exp_days (int): Refresh token expiration time in days - - Returns: - tuple: (access_token, refresh_token) - """ - # Generate access token (short-lived) - access_token = self.generate_token(username, discord_id, access_exp_minutes) - - # Generate refresh token (long-lived, stored securely) - refresh_token = self.generate_refresh_token(username, discord_id, refresh_exp_days) - - return access_token, refresh_token - - def generate_token(self, username, discord_id=None, exp_minutes=60): - """ - Generate a JWT token with username and optional discord_id. - - Args: - username (str): The user's display name - discord_id (str): The user's Discord ID (recommended for security) - exp_minutes (int): Token expiration time in minutes - - Returns: - str: JWT token - """ - payload = { - "exp": datetime.datetime.now(datetime.UTC) + datetime.timedelta(minutes=exp_minutes), - "username": username, - "type": "access", # Token type for security - } - - # Add discord_id to payload if provided (more secure) - if discord_id: - payload["discord_id"] = str(discord_id) - - return jwt.encode(payload, self.private_key, algorithm=self.algorithm) - - def generate_refresh_token(self, username, discord_id=None, exp_days=7): - """ - Generate a refresh token and store its hash in the database. - - Args: - username (str): The user's display name - discord_id (str): The user's Discord ID - exp_days (int): Refresh token expiration time in days - - Returns: - str: Refresh token (raw, returned only once) - """ - from modules.auth.models import RefreshToken - - # Generate a cryptographically secure random token - raw_token = secrets.token_urlsafe(32) - token_hash = self._hash_token(raw_token) - expires_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) + datetime.timedelta(days=exp_days) - - # Store hashed refresh token in database - db = self._get_db_session() - try: - db_token = RefreshToken( - token=token_hash, - username=username, - discord_id=str(discord_id) if discord_id else None, - expires_at=expires_at, - ) - db.add(db_token) - db.commit() - except Exception as e: - db.rollback() - logger.error(f"Error storing refresh token: {e}") - raise - finally: - db.close() - - return raw_token - - def refresh_access_token(self, refresh_token): - """ - Generate a new access token using a valid refresh token. - - Args: - refresh_token (str): The raw refresh token - - Returns: - str: New access token, or None if refresh token is invalid - """ - from modules.auth.models import RefreshToken - - token_hash = self._hash_token(refresh_token) - db = self._get_db_session() - try: - db_token = db.query(RefreshToken).filter(RefreshToken.token == token_hash).first() - - if not db_token: - return None - - # Check if refresh token is expired (both datetimes are UTC, compare as naive) - expires_at = db_token.expires_at - if expires_at.tzinfo is not None: - expires_at = expires_at.replace(tzinfo=None) - if datetime.datetime.now(datetime.UTC).replace(tzinfo=None) > expires_at: - db.delete(db_token) - db.commit() - return None - - # Generate new access token - new_access_token = self.generate_token( - username=db_token.username, - discord_id=db_token.discord_id, - exp_minutes=30, - ) - - return new_access_token - except Exception as e: - logger.error(f"Error refreshing access token: {e}") - return None - finally: - db.close() - - def revoke_refresh_token(self, refresh_token): - """ - Revoke a refresh token. - - Args: - refresh_token (str): The raw refresh token to revoke - - Returns: - bool: True if token was revoked, False if not found - """ - from modules.auth.models import RefreshToken - - token_hash = self._hash_token(refresh_token) - db = self._get_db_session() - try: - db_token = db.query(RefreshToken).filter(RefreshToken.token == token_hash).first() - if db_token: - db.delete(db_token) - db.commit() - return True - return False - except Exception as e: - db.rollback() - logger.error(f"Error revoking refresh token: {e}") - return False - finally: - db.close() - - def cleanup_expired_refresh_tokens(self): - """ - Remove expired refresh tokens from the database. - """ - from modules.auth.models import RefreshToken - - db = self._get_db_session() - try: - current_time = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) - deleted = db.query(RefreshToken).filter(RefreshToken.expires_at < current_time).delete() - db.commit() - if deleted: - logger.info(f"Cleaned up {deleted} expired refresh tokens") - except Exception as e: - db.rollback() - logger.error(f"Error cleaning up expired refresh tokens: {e}") - finally: - db.close() - - def retrieve_username(self, token): - try: - payload = jwt.decode(token, self.public_key, algorithms=[self.algorithm]) - return payload.get("username") - except jwt.ExpiredSignatureError: - try: - payload = jwt.decode( - token, - self.public_key, - algorithms=[self.algorithm], - options={"verify_exp": False}, - ) - return payload.get("username") - except jwt.DecodeError: - return None - - def retrieve_discord_id(self, token): - """ - Retrieve discord_id from JWT token. - - Args: - token (str): JWT token - - Returns: - str: Discord ID if present in token, None otherwise - """ - try: - payload = jwt.decode(token, self.public_key, algorithms=[self.algorithm]) - return payload.get("discord_id") - except jwt.ExpiredSignatureError: - try: - payload = jwt.decode( - token, - self.public_key, - algorithms=[self.algorithm], - options={"verify_exp": False}, - ) - return payload.get("discord_id") - except jwt.DecodeError: - return None - - def decode_token(self, token): - return jwt.decode(token, self.public_key, algorithms=[self.algorithm]) - - def get_username_from_expiration(self, token): - try: - payload = jwt.decode(token, self.public_key, algorithms=[self.algorithm]) - return payload["username"] - except jwt.InvalidTokenError: - return None - - def is_token_valid(self, token): - if token in self.blacklist: - return False - try: - self.decode_token(token) - return True - except jwt.InvalidTokenError: - return False - - def is_token_expired(self, token): - try: - self.decode_token(token) - return False - except jwt.ExpiredSignatureError: - return True - - def refresh_token(self, token): - username = self.retrieve_username(token) - discord_id = self.retrieve_discord_id(token) - return self.generate_token(username, discord_id) - - def generate_app_token(self, name, app_name): - payload = { - "exp": datetime.datetime.now(datetime.UTC) + datetime.timedelta(days=120), - "name": name, - "app_name": app_name, - } - return jwt.encode(payload, self.private_key, algorithm=self.algorithm) - - def delete_token(self, token): - self.blacklist.add(token) +import datetime +import hashlib +import os +import secrets + +import jwt +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa + +from modules.utils.logging_config import get_logger + +logger = get_logger(__name__) + + +class TokenManager: + def __init__(self, algorithm="RS256", keys_path="./data") -> None: + self.algorithm = algorithm + self.keys_path = keys_path + self.private_key_file = os.path.join(keys_path, "jwt_private.pem") + self.public_key_file = os.path.join(keys_path, "jwt_public.pem") + self.private_key, self.public_key = self.load_or_generate_keys() + self.blacklist = set() + self._db_connect = None + + def _get_db_connect(self): + """Lazily import db_connect to avoid circular imports""" + if self._db_connect is None: + from modules.utils.db import db_connect + + self._db_connect = db_connect + return self._db_connect + + def _get_db_session(self): + """Get a database session""" + db_connect = self._get_db_connect() + return db_connect.SessionLocal() + + @staticmethod + def _hash_token(token): + """Hash a refresh token using SHA-256 for secure storage""" + return hashlib.sha256(token.encode("utf-8")).hexdigest() + + def load_or_generate_keys(self): + """Load keys from disk if they exist, otherwise generate and save new ones""" + # Check if both key files exist + if os.path.exists(self.private_key_file) and os.path.exists(self.public_key_file): + try: + # Load existing keys + with open(self.private_key_file, encoding="utf-8") as f: + private_key = f.read() + with open(self.public_key_file, encoding="utf-8") as f: + public_key = f.read() + logger.info(f"Loaded existing RSA keys from {self.keys_path}") + return private_key, public_key + except Exception as e: + logger.error(f"Error loading keys: {e}. Generating new keys...") + + # Generate new keys if loading failed or files don't exist + private_key, public_key = self.generate_keys() + + # Save keys to disk + try: + os.makedirs(self.keys_path, exist_ok=True) + with open(self.private_key_file, "w") as f: + f.write(private_key) + with open(self.public_key_file, "w") as f: + f.write(public_key) + # Set restrictive permissions on private key + os.chmod(self.private_key_file, 0o600) + logger.info(f"Generated and saved new RSA keys to {self.keys_path}") + except Exception as e: + logger.warning(f"Could not save keys to disk: {e}") + + return private_key, public_key + + def generate_keys(self): + """Generate a new RSA key pair""" + # Generate a private RSA key + private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + + # Generate the corresponding public key + public_key = private_key.public_key() + + # Serialize private key to PEM format + private_pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), + ) + + # Serialize public key to PEM format + public_pem = public_key.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + + return private_pem.decode("utf-8"), public_pem.decode("utf-8") + + def generate_token_pair(self, username, discord_id=None, access_exp_minutes=30, refresh_exp_days=7): + """ + Generate both access token and refresh token. + + Args: + username (str): The user's display name + discord_id (str): The user's Discord ID (recommended for security) + access_exp_minutes (int): Access token expiration time in minutes + refresh_exp_days (int): Refresh token expiration time in days + + Returns: + tuple: (access_token, refresh_token) + """ + # Generate access token (short-lived) + access_token = self.generate_token(username, discord_id, access_exp_minutes) + + # Generate refresh token (long-lived, stored securely) + refresh_token = self.generate_refresh_token(username, discord_id, refresh_exp_days) + + return access_token, refresh_token + + def generate_token(self, username, discord_id=None, exp_minutes=60): + """ + Generate a JWT token with username and optional discord_id. + + Args: + username (str): The user's display name + discord_id (str): The user's Discord ID (recommended for security) + exp_minutes (int): Token expiration time in minutes + + Returns: + str: JWT token + """ + payload = { + "exp": datetime.datetime.now(datetime.UTC) + datetime.timedelta(minutes=exp_minutes), + "username": username, + "type": "access", # Token type for security + } + + # Add discord_id to payload if provided (more secure) + if discord_id: + payload["discord_id"] = str(discord_id) + + return jwt.encode(payload, self.private_key, algorithm=self.algorithm) + + def generate_refresh_token(self, username, discord_id=None, exp_days=7): + """ + Generate a refresh token and store its hash in the database. + + Args: + username (str): The user's display name + discord_id (str): The user's Discord ID + exp_days (int): Refresh token expiration time in days + + Returns: + str: Refresh token (raw, returned only once) + """ + from modules.auth.models import RefreshToken + + # Generate a cryptographically secure random token + raw_token = secrets.token_urlsafe(32) + token_hash = self._hash_token(raw_token) + expires_at = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) + datetime.timedelta(days=exp_days) + + # Store hashed refresh token in database + db = self._get_db_session() + try: + db_token = RefreshToken( + token=token_hash, + username=username, + discord_id=str(discord_id) if discord_id else None, + expires_at=expires_at, + ) + db.add(db_token) + db.commit() + except Exception as e: + db.rollback() + logger.error(f"Error storing refresh token: {e}") + raise + finally: + db.close() + + return raw_token + + def refresh_access_token(self, refresh_token): + """ + Generate a new access token using a valid refresh token. + + Args: + refresh_token (str): The raw refresh token + + Returns: + str: New access token, or None if refresh token is invalid + """ + from modules.auth.models import RefreshToken + + token_hash = self._hash_token(refresh_token) + db = self._get_db_session() + try: + db_token = db.query(RefreshToken).filter(RefreshToken.token == token_hash).first() + + if not db_token: + return None + + # Check if refresh token is expired (both datetimes are UTC, compare as naive) + expires_at = db_token.expires_at + if expires_at.tzinfo is not None: + expires_at = expires_at.replace(tzinfo=None) + if datetime.datetime.now(datetime.UTC).replace(tzinfo=None) > expires_at: + db.delete(db_token) + db.commit() + return None + + # Generate new access token + new_access_token = self.generate_token( + username=db_token.username, + discord_id=db_token.discord_id, + exp_minutes=30, + ) + + return new_access_token + except Exception as e: + logger.error(f"Error refreshing access token: {e}") + return None + finally: + db.close() + + def revoke_refresh_token(self, refresh_token): + """ + Revoke a refresh token. + + Args: + refresh_token (str): The raw refresh token to revoke + + Returns: + bool: True if token was revoked, False if not found + """ + from modules.auth.models import RefreshToken + + token_hash = self._hash_token(refresh_token) + db = self._get_db_session() + try: + db_token = db.query(RefreshToken).filter(RefreshToken.token == token_hash).first() + if db_token: + db.delete(db_token) + db.commit() + return True + return False + except Exception as e: + db.rollback() + logger.error(f"Error revoking refresh token: {e}") + return False + finally: + db.close() + + def cleanup_expired_refresh_tokens(self): + """ + Remove expired refresh tokens from the database. + """ + from modules.auth.models import RefreshToken + + db = self._get_db_session() + try: + current_time = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) + deleted = db.query(RefreshToken).filter(RefreshToken.expires_at < current_time).delete() + db.commit() + if deleted: + logger.info(f"Cleaned up {deleted} expired refresh tokens") + except Exception as e: + db.rollback() + logger.error(f"Error cleaning up expired refresh tokens: {e}") + finally: + db.close() + + def retrieve_username(self, token): + try: + payload = jwt.decode(token, self.public_key, algorithms=[self.algorithm]) + return payload.get("username") + except jwt.ExpiredSignatureError: + try: + payload = jwt.decode( + token, + self.public_key, + algorithms=[self.algorithm], + options={"verify_exp": False}, + ) + return payload.get("username") + except jwt.DecodeError: + return None + + def retrieve_discord_id(self, token): + """ + Retrieve discord_id from JWT token. + + Args: + token (str): JWT token + + Returns: + str: Discord ID if present in token, None otherwise + """ + try: + payload = jwt.decode(token, self.public_key, algorithms=[self.algorithm]) + return payload.get("discord_id") + except jwt.ExpiredSignatureError: + try: + payload = jwt.decode( + token, + self.public_key, + algorithms=[self.algorithm], + options={"verify_exp": False}, + ) + return payload.get("discord_id") + except jwt.DecodeError: + return None + + def decode_token(self, token): + return jwt.decode(token, self.public_key, algorithms=[self.algorithm]) + + def get_username_from_expiration(self, token): + try: + payload = jwt.decode(token, self.public_key, algorithms=[self.algorithm]) + return payload["username"] + except jwt.InvalidTokenError: + return None + + def is_token_valid(self, token): + if token in self.blacklist: + return False + try: + self.decode_token(token) + return True + except jwt.InvalidTokenError: + return False + + def is_token_expired(self, token): + try: + self.decode_token(token) + return False + except jwt.ExpiredSignatureError: + return True + + def refresh_token(self, token): + username = self.retrieve_username(token) + discord_id = self.retrieve_discord_id(token) + return self.generate_token(username, discord_id) + + def generate_app_token(self, name, app_name): + payload = { + "exp": datetime.datetime.now(datetime.UTC) + datetime.timedelta(days=120), + "name": name, + "app_name": app_name, + } + return jwt.encode(payload, self.private_key, algorithm=self.algorithm) + + def delete_token(self, token): + self.blacklist.add(token) + + +token_manager = TokenManager() diff --git a/modules/utils/app.py b/modules/utils/app.py new file mode 100644 index 00000000..638539ac --- /dev/null +++ b/modules/utils/app.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +import os +from typing import TYPE_CHECKING + +from flask import Flask +from flask_cors import CORS + +if TYPE_CHECKING: + from modules.bot.discord_modules.bot import BotFork + from modules.calendar.service import MultiOrgCalendarService + + +class ExtendedFlask(Flask): + auth_bot: BotFork | None + multi_org_calendar_service: MultiOrgCalendarService + + +app = ExtendedFlask( + "SoDA internal API", + static_folder=os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "web", "build")), + template_folder=os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "web", "build")), +) +CORS( + app, + resources={ + r"/*": { + "origins": [ + "http://localhost:3000", + "http://127.0.0.1:3000", + "http://localhost:5173", + "http://127.0.0.1:5173", + "https://thesoda.io", + "https://admin.thesoda.io", + ], + "methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"], + "allow_headers": ["Content-Type", "Authorization", "X-Organization-ID", "X-Organization-Prefix"], + "supports_credentials": True, + } + }, +) diff --git a/modules/utils/config.py b/modules/utils/config.py index 8fc06dd7..3157ffcc 100644 --- a/modules/utils/config.py +++ b/modules/utils/config.py @@ -1,92 +1,95 @@ -import json -import os - -from dotenv import load_dotenv - -from modules.utils.logging_config import get_logger - -logger = get_logger(__name__) - - -class Config: - """Centralized configuration management for the application""" - - def __init__(self) -> None: - load_dotenv() - try: - # Core Application Config - self.SECRET_KEY = os.environ.get("SECRET_KEY", "test-secret-key") - self.CLIENT_ID = os.environ.get("CLIENT_ID", "test-client-id") - self.CLIENT_SECRET = os.environ.get("CLIENT_SECRET", "test-client-secret") - self.REDIRECT_URI = os.environ.get("REDIRECT_URI", "http://localhost:5000/callback") - self.CLIENT_URL = os.environ.get("CLIENT_URL", "http://localhost:3000") - self.TNAY_API_URL = os.environ.get("TNAY_API_URL", "") - self.OPEN_ROUTER_CLAUDE_API_KEY = os.environ.get("OPEN_ROUTER_CLAUDE_API_KEY", "") - self.DISCORD_OFFICER_WEBHOOK_URL = os.environ.get("DISCORD_OFFICER_WEBHOOK_URL", "") - self.DISCORD_POST_WEBHOOK_URL = os.environ.get("DISCORD_POST_WEBHOOK_URL", "") - self.ONEUP_PASSWORD = os.environ.get("ONEUP_PASSWORD", "") - self.ONEUP_EMAIL = os.environ.get("ONEUP_EMAIL", "") - self.PROD = os.environ.get("PROD", "false").lower() == "true" - - # Service Tokens - self.BOT_TOKEN = os.environ.get("BOT_TOKEN") - self.AVERY_BOT_TOKEN = os.environ.get("AVERY_BOT_TOKEN") - self.AUTH_BOT_TOKEN = os.environ.get("AUTH_BOT_TOKEN") - - # Auth - self.CLERK_SECRET_KEY = os.environ.get("CLERK_SECRET_KEY", "test-clerk-secret") - self.CLERK_AUTHORIZED_PARTIES = os.environ.get( - "CLERK_AUTHORIZED_PARTIES", "http://localhost:3000,http://localhost:5173" - ) - - # Database Configuration - self.DB_TYPE = os.environ.get("DB_TYPE", "sqlite") - self.DB_URI = os.environ.get("DB_URI", "sqlite:///test.db") - self.DB_NAME = os.environ.get("DB_NAME", "test") - self.DB_USER = os.environ.get("DB_USER", "test") - self.DB_PASSWORD = os.environ.get("DB_PASSWORD", "test") - self.DB_HOST = os.environ.get("DB_HOST", "localhost") - self.DB_PORT = os.environ.get("DB_PORT", "5432") - - # Google Calendar Integration - try: - with open("google-secret.json") as file: - self.GOOGLE_SERVICE_ACCOUNT = json.load(file) - logger.info("Google service account credentials loaded successfully") - except FileNotFoundError: - logger.warning("google-secret.json not found. Google Calendar features will be disabled.") - self.GOOGLE_SERVICE_ACCOUNT = None - except Exception as e: - logger.warning(f"Error loading Google credentials: {e}. Google Calendar features will be disabled.") - self.GOOGLE_SERVICE_ACCOUNT = None - - self.NOTION_API_KEY = os.environ.get("NOTION_API_KEY", "") - self.NOTION_DATABASE_ID = os.environ.get("NOTION_DATABASE_ID", "") - self.NOTION_TOKEN = os.environ.get("NOTION_TOKEN") - self.GOOGLE_CALENDAR_ID = os.environ.get("GOOGLE_CALENDAR_ID", "") - self.GOOGLE_USER_EMAIL = os.environ.get("GOOGLE_USER_EMAIL", "") - self.SERVER_PORT = int(os.environ.get("SERVER_PORT", "5000")) - self.SERVER_DEBUG = os.environ.get("SERVER_DEBUG", "false").lower() == "true" - self.TIMEZONE = os.environ.get("TIMEZONE", "America/Phoenix") - - # Monitoring Configuration (Optional) - self.SENTRY_DSN = os.environ.get("SENTRY_DSN") - self.SYS_ADMIN = os.environ.get("ADMIN_USER_ID") - - # AI Service Keys - self.GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY") - - # Superadmin config - self.SUPERADMIN_USER_ID = os.environ.get("SYS_ADMIN") - - except json.JSONDecodeError as e: - raise RuntimeError(f"Configuration error: {str(e)}") from e - - @property - def google_calendar_config(self) -> dict: - """Get Google Calendar configuration as a dictionary""" - return { - "service_account": self.GOOGLE_SERVICE_ACCOUNT, - "calendar_id": self.GOOGLE_CALENDAR_ID, - "user_email": self.GOOGLE_USER_EMAIL, - } +import json +import os + +from dotenv import load_dotenv + +from modules.utils.logging_config import get_logger + +logger = get_logger(__name__) + + +class Config: + """Centralized configuration management for the application""" + + def __init__(self) -> None: + load_dotenv() + try: + # Core Application Config + self.SECRET_KEY = os.environ.get("SECRET_KEY", "test-secret-key") + self.CLIENT_ID = os.environ.get("CLIENT_ID", "test-client-id") + self.CLIENT_SECRET = os.environ.get("CLIENT_SECRET", "test-client-secret") + self.REDIRECT_URI = os.environ.get("REDIRECT_URI", "http://localhost:5000/callback") + self.CLIENT_URL = os.environ.get("CLIENT_URL", "http://localhost:3000") + self.TNAY_API_URL = os.environ.get("TNAY_API_URL", "") + self.OPEN_ROUTER_CLAUDE_API_KEY = os.environ.get("OPEN_ROUTER_CLAUDE_API_KEY", "") + self.DISCORD_OFFICER_WEBHOOK_URL = os.environ.get("DISCORD_OFFICER_WEBHOOK_URL", "") + self.DISCORD_POST_WEBHOOK_URL = os.environ.get("DISCORD_POST_WEBHOOK_URL", "") + self.ONEUP_PASSWORD = os.environ.get("ONEUP_PASSWORD", "") + self.ONEUP_EMAIL = os.environ.get("ONEUP_EMAIL", "") + self.PROD = os.environ.get("PROD", "false").lower() == "true" + + # Service Tokens + self.BOT_TOKEN = os.environ.get("BOT_TOKEN") + self.AVERY_BOT_TOKEN = os.environ.get("AVERY_BOT_TOKEN") + self.AUTH_BOT_TOKEN = os.environ.get("AUTH_BOT_TOKEN") + + # Auth + self.CLERK_SECRET_KEY = os.environ.get("CLERK_SECRET_KEY", "test-clerk-secret") + self.CLERK_AUTHORIZED_PARTIES = os.environ.get( + "CLERK_AUTHORIZED_PARTIES", "http://localhost:3000,http://localhost:5173" + ) + + # Database Configuration + self.DB_TYPE = os.environ.get("DB_TYPE", "sqlite") + self.DB_URI = os.environ.get("DB_URI", "sqlite:///test.db") + self.DB_NAME = os.environ.get("DB_NAME", "test") + self.DB_USER = os.environ.get("DB_USER", "test") + self.DB_PASSWORD = os.environ.get("DB_PASSWORD", "test") + self.DB_HOST = os.environ.get("DB_HOST", "localhost") + self.DB_PORT = os.environ.get("DB_PORT", "5432") + + # Google Calendar Integration + try: + with open("google-secret.json") as file: + self.GOOGLE_SERVICE_ACCOUNT = json.load(file) + logger.info("Google service account credentials loaded successfully") + except FileNotFoundError: + logger.warning("google-secret.json not found. Google Calendar features will be disabled.") + self.GOOGLE_SERVICE_ACCOUNT = None + except Exception as e: + logger.warning(f"Error loading Google credentials: {e}. Google Calendar features will be disabled.") + self.GOOGLE_SERVICE_ACCOUNT = None + + self.NOTION_API_KEY = os.environ.get("NOTION_API_KEY", "") + self.NOTION_DATABASE_ID = os.environ.get("NOTION_DATABASE_ID", "") + self.NOTION_TOKEN = os.environ.get("NOTION_TOKEN") + self.GOOGLE_CALENDAR_ID = os.environ.get("GOOGLE_CALENDAR_ID", "") + self.GOOGLE_USER_EMAIL = os.environ.get("GOOGLE_USER_EMAIL", "") + self.SERVER_PORT = int(os.environ.get("SERVER_PORT", "5000")) + self.SERVER_DEBUG = os.environ.get("SERVER_DEBUG", "false").lower() == "true" + self.TIMEZONE = os.environ.get("TIMEZONE", "America/Phoenix") + + # Monitoring Configuration (Optional) + self.SENTRY_DSN = os.environ.get("SENTRY_DSN") + self.SYS_ADMIN = os.environ.get("ADMIN_USER_ID") + + # AI Service Keys + self.GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY") + + # Superadmin config + self.SUPERADMIN_USER_ID = os.environ.get("SYS_ADMIN") + + except json.JSONDecodeError as e: + raise RuntimeError(f"Configuration error: {str(e)}") from e + + @property + def google_calendar_config(self) -> dict: + """Get Google Calendar configuration as a dictionary""" + return { + "service_account": self.GOOGLE_SERVICE_ACCOUNT, + "calendar_id": self.GOOGLE_CALENDAR_ID, + "user_email": self.GOOGLE_USER_EMAIL, + } + + +config = Config() diff --git a/modules/utils/db.py b/modules/utils/db.py index db51034f..c8784b95 100644 --- a/modules/utils/db.py +++ b/modules/utils/db.py @@ -190,3 +190,8 @@ def delete_storefront_product(self, db, product_id, organization_id): logger.error(f"Error deleting storefront product: {str(e)}") db.rollback() return False + + +import modules.auth.models # noqa: F401, E402 + +db_connect = DBConnect("sqlite:///./data/user.db") diff --git a/modules/utils/notion.py b/modules/utils/notion.py new file mode 100644 index 00000000..b165756d --- /dev/null +++ b/modules/utils/notion.py @@ -0,0 +1,5 @@ +from notion_client import Client + +from modules.utils.config import config + +notion = Client(auth=config.NOTION_API_KEY) diff --git a/shared.py b/shared.py index 15e6439d..c209edea 100644 --- a/shared.py +++ b/shared.py @@ -1,106 +1,31 @@ -import asyncio -import os -import threading -import time - -import discord import sentry_sdk -from flask import Flask -from flask_cors import CORS -from notion_client import Client from sentry_sdk.integrations.flask import FlaskIntegration -# Import custom BotFork class from modules.bot.discord_modules.bot import BotFork -from modules.utils.base import Base -from modules.utils.config import Config -from modules.utils.db import DBConnect +from modules.utils.app import app +from modules.utils.config import config +from modules.utils.db import db_connect from modules.utils.logging_config import logger -from modules.utils.TokenManager import TokenManager - -# Initialize Flask app -app = Flask( - "SoDA internal API", - static_folder=os.path.join(os.path.dirname(os.path.dirname(__file__)), "web/build"), - template_folder=os.path.join(os.path.dirname(os.path.dirname(__file__)), "web/build"), -) -CORS( - app, - resources={ - r"/*": { - "origins": [ - "http://localhost:3000", - "http://127.0.0.1:3000", - "http://localhost:5173", - "http://127.0.0.1:5173", - "https://thesoda.io", - "https://admin.thesoda.io", - ], - "methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"], - "allow_headers": ["Content-Type", "Authorization", "X-Organization-ID", "X-Organization-Prefix"], - "supports_credentials": True, - } - }, -) - -# Initialize configuration -config = Config() +from modules.utils.notion import notion +from modules.utils.TokenManager import token_manager as tokenManager -# Initialize Sentry if config.SENTRY_DSN: sentry_sdk.init( dsn=config.SENTRY_DSN, integrations=[FlaskIntegration()], traces_sample_rate=1.0, profiles_sample_rate=1.0, - # Enable logs to be sent to Sentry enable_logs=True, ) logger.info("Sentry initialized with logging enabled.") else: logger.warning("SENTRY_DSN not found in environment. Sentry not initialized.") -# Initialize database connections -db_connect = DBConnect("sqlite:///./data/user.db") - -# Initialize TokenManager -tokenManager = TokenManager() - - -# Import models so their tables are registered with Base.metadata before create_all -import modules.auth.models # noqa: F401, E402 - -# Ensure all tables are created after all models are imported -Base.metadata.create_all(bind=db_connect.engine) - - -# Periodic cleanup of expired refresh tokens -def cleanup_expired_tokens(): - """Clean up expired refresh tokens periodically""" - try: - tokenManager.cleanup_expired_refresh_tokens() - logger.info("Cleaned up expired refresh tokens") - except Exception as e: - logger.error(f"Error cleaning up expired tokens: {e}") - - -# Schedule cleanup every hour - - -def run_cleanup_scheduler(): - """Run the cleanup scheduler in a separate thread""" - while True: - cleanup_expired_tokens() - time.sleep(3600) - -# Start cleanup scheduler in background thread -cleanup_thread = threading.Thread(target=run_cleanup_scheduler, daemon=True) -cleanup_thread.start() - - -def create_auth_bot(loop: asyncio.AbstractEventLoop) -> BotFork: +def create_auth_bot(loop): """Create and configure the auth bot (BotFork) instance with a specific event loop.""" + import discord + logger.info("Creating auth bot instance (BotFork)...") intents = discord.Intents.default() intents.members = True @@ -119,8 +44,12 @@ def create_auth_bot(loop: asyncio.AbstractEventLoop) -> BotFork: return auth_bot_instance -# Initialize Notion client -notion = Client(auth=config.NOTION_API_KEY) - -# Initialize bot instance -bot = create_auth_bot(asyncio.get_event_loop()) +__all__ = [ + "app", + "config", + "db_connect", + "logger", + "notion", + "tokenManager", + "create_auth_bot", +] diff --git a/shared.pyi b/shared.pyi deleted file mode 100644 index d02d65d9..00000000 --- a/shared.pyi +++ /dev/null @@ -1,26 +0,0 @@ -"""Type stubs for shared module.""" - -import asyncio - -from flask import Flask -from notion_client import Client as NotionClient - -from modules.bot.discord_modules.bot import BotFork -from modules.calendar.service import MultiOrgCalendarService -from modules.utils.config import Config -from modules.utils.db import DBConnect -from modules.utils.logging_config import logger as logger -from modules.utils.TokenManager import TokenManager - -# Extended Flask app with custom attributes -class ExtendedFlask(Flask): - auth_bot: BotFork | None - multi_org_calendar_service: MultiOrgCalendarService - -app: ExtendedFlask -config: Config -db_connect: DBConnect -tokenManager: TokenManager -notion: NotionClient - -def create_auth_bot(loop: asyncio.AbstractEventLoop) -> BotFork: ... From bff0ac4533e9600412da1fc4924bf9b505dd10fc Mon Sep 17 00:00:00 2001 From: Ben Juntilla Date: Wed, 11 Feb 2026 16:26:33 -0700 Subject: [PATCH 2/3] refactor: remove shared.py, import directly from utility modules - Move Sentry initialization and create_auth_bot() into main.py - Update all imports to use modules.utils.{app,config,db,TokenManager,logging_config,notion} - Rename tokenManager to token_manager everywhere (PEP 8) Amp-Thread-ID: https://ampcode.com/threads/T-019c4f02-ee9c-74d4-9e35-174c10061404 Co-authored-by: Amp --- main.py | 46 +++++++++++++++++++++++-- modules/auth/api.py | 27 ++++++++------- modules/auth/decoraters.py | 35 ++++++++++--------- modules/bot/api.py | 2 +- modules/bot/discord_modules/bot.py | 2 +- modules/calendar/api.py | 5 ++- modules/calendar/clients.py | 6 ++-- modules/calendar/service.py | 6 ++-- modules/calendar/utils.py | 3 +- modules/organizations/README.md | 2 +- modules/organizations/api.py | 2 +- modules/points/api.py | 7 ++-- modules/public/api.py | 2 +- modules/superadmin/api.py | 6 ++-- modules/users/api.py | 2 +- modules/utils/clerk_auth.py | 2 +- shared.py | 55 ------------------------------ 17 files changed, 101 insertions(+), 109 deletions(-) delete mode 100644 shared.py diff --git a/main.py b/main.py index c9acf754..02604a65 100644 --- a/main.py +++ b/main.py @@ -6,10 +6,13 @@ from datetime import UTC, datetime import discord +import sentry_sdk from flask import jsonify # Import current_app +from sentry_sdk.integrations.flask import FlaskIntegration from modules.auth.api import auth_blueprint from modules.bot.api import game_blueprint +from modules.bot.discord_modules.bot import BotFork from modules.calendar.api import calendar_blueprint from modules.calendar.service import MultiOrgCalendarService from modules.organizations.api import organizations_blueprint @@ -18,7 +21,23 @@ from modules.storefront.api import storefront_blueprint from modules.superadmin.api import superadmin_blueprint from modules.users.api import users_blueprint -from shared import app, config, create_auth_bot, logger, tokenManager +from modules.utils.app import app +from modules.utils.config import config +from modules.utils.logging_config import logger +from modules.utils.TokenManager import token_manager + +# Initialize Sentry if configured +if config.SENTRY_DSN: + sentry_sdk.init( + dsn=config.SENTRY_DSN, + integrations=[FlaskIntegration()], + traces_sample_rate=1.0, + profiles_sample_rate=1.0, + enable_logs=True, + ) + logger.info("Sentry initialized with logging enabled.") +else: + logger.warning("SENTRY_DSN not found in environment. Sentry not initialized.") # Set a secret key for session management app.secret_key = os.environ.get("FLASK_SECRET_KEY", "dev-secret-key") @@ -82,6 +101,29 @@ def health(): # Static file serving for the frontend is configured elsewhere (no Flask route defined here). +# --- Bot Setup --- +def create_auth_bot(loop): + """Create and configure the auth bot (BotFork) instance with a specific event loop.""" + import discord + + logger.info("Creating auth bot instance (BotFork)...") + intents = discord.Intents.default() + intents.members = True + intents.guilds = True + + auth_bot_instance = BotFork(intents=intents, loop=loop) + try: + from modules.bot.discord_modules.cogs.GameCog import GameCog + from modules.bot.discord_modules.cogs.HelperCog import HelperCog + + auth_bot_instance.add_cog(HelperCog(auth_bot_instance)) + auth_bot_instance.add_cog(GameCog(auth_bot_instance)) + logger.info("Auth bot cogs (HelperCog, GameCog) registered with BotFork instance.") + except Exception as e: + logger.error(f"Error registering auth bot cogs: {e}", exc_info=True) + return auth_bot_instance + + # --- Bot Thread Functions --- def run_auth_bot_in_thread(): loop = asyncio.new_event_loop() @@ -116,7 +158,7 @@ def initialize_app(): def run_cleanup_scheduler(): while True: try: - tokenManager.cleanup_expired_refresh_tokens() + token_manager.cleanup_expired_refresh_tokens() logger.info("Cleaned up expired refresh tokens") except Exception as e: logger.error(f"Error cleaning up expired tokens: {e}") diff --git a/modules/auth/api.py b/modules/auth/api.py index 4043697b..896f78b3 100644 --- a/modules/auth/api.py +++ b/modules/auth/api.py @@ -2,8 +2,9 @@ from flask import Blueprint, current_app, jsonify, redirect, request, session from modules.auth.decoraters import auth_required, error_handler +from modules.utils.config import config from modules.utils.logging_config import logger -from shared import config, tokenManager +from modules.utils.TokenManager import token_manager auth_blueprint = Blueprint("auth", __name__, template_folder=None, static_folder=None) CLIENT_ID = config.CLIENT_ID @@ -29,7 +30,7 @@ def validToken(): if not auth_header: return jsonify({"status": "error", "valid": False, "message": "No authorization header"}), 401 token = auth_header.split(" ")[1] - if tokenManager.is_token_valid(token): + if token_manager.is_token_valid(token): return jsonify({"status": "success", "valid": True, "expired": False}), 200 else: return jsonify({"status": "error", "valid": False}), 401 @@ -78,7 +79,7 @@ def callback(): if officer_guilds: # If user is officer in at least one organization name = auth_bot.get_name(user_id) # type: ignore[attr-defined] # Generate token pair with both access and refresh tokens - access_token, refresh_token = tokenManager.generate_token_pair( + access_token, refresh_token = token_manager.generate_token_pair( username=name, discord_id=user_id, access_exp_minutes=30, refresh_exp_days=7 ) # Store user info in session with officer guilds @@ -114,7 +115,7 @@ def refresh_token(): refresh_token = data["refresh_token"] # Generate new access token - new_access_token = tokenManager.refresh_access_token(refresh_token) + new_access_token = token_manager.refresh_access_token(refresh_token) if new_access_token: return jsonify( @@ -145,12 +146,12 @@ def revoke_token(): refresh_token = data["refresh_token"] # Revoke the refresh token - if tokenManager.revoke_refresh_token(refresh_token): + if token_manager.revoke_refresh_token(refresh_token): # Also blacklist the current access token auth_header = request.headers.get("Authorization") if auth_header: current_token = auth_header.split(" ")[1] - tokenManager.delete_token(current_token) + token_manager.delete_token(current_token) return jsonify({"message": "Token revoked successfully"}), 200 else: @@ -166,8 +167,8 @@ def valid_token(): if not auth_header: return jsonify({"status": "error", "valid": False, "message": "No authorization header"}), 401 token = auth_header.split(" ")[1] - if tokenManager.is_token_valid(token): - if tokenManager.is_token_expired(token): + if token_manager.is_token_valid(token): + if token_manager.is_token_expired(token): logger.info("Token is valid but expired.") return jsonify({"status": "success", "valid": True, "expired": True}), 200 else: @@ -190,12 +191,12 @@ def get_app_token(): if not appname: return jsonify({"error": "appname query parameter is required"}), 400 - username = tokenManager.retrieve_username(token) + username = token_manager.retrieve_username(token) if not username: return jsonify({"error": "Invalid user token"}), 401 logger.info(f"Generating app token for user {username}, app: {appname}") - app_token_value = tokenManager.generate_app_token(username, appname) + app_token_value = token_manager.generate_app_token(username, appname) return jsonify({"app_token": app_token_value}), 200 @@ -207,7 +208,7 @@ def get_name(): return jsonify({"error": "No authorization header"}), 401 autorisation = auth_header.split(" ")[1] - return jsonify({"name": tokenManager.retrieve_username(autorisation)}), 200 + return jsonify({"name": token_manager.retrieve_username(autorisation)}), 200 @auth_blueprint.route("/logout", methods=["POST"]) @@ -219,12 +220,12 @@ def logout(): data = request.get_json() if data and "refresh_token" in data: # Revoke refresh token - tokenManager.revoke_refresh_token(data["refresh_token"]) + token_manager.revoke_refresh_token(data["refresh_token"]) # Also blacklist current access token if provided if "Authorization" in request.headers: token = request.headers["Authorization"].split(" ")[1] - tokenManager.delete_token(token) + token_manager.delete_token(token) # Clear session session.clear() diff --git a/modules/auth/decoraters.py b/modules/auth/decoraters.py index 277d1657..273bffbc 100644 --- a/modules/auth/decoraters.py +++ b/modules/auth/decoraters.py @@ -4,7 +4,8 @@ from flask import current_app, jsonify, request, session -from shared import config, tokenManager +from modules.utils.config import config +from modules.utils.TokenManager import token_manager logger = logging.getLogger(__name__) @@ -49,17 +50,17 @@ def wrapper(*args, **kwargs): # Check session cookie first if session.get("token"): try: - if not tokenManager.is_token_valid(session["token"]): + if not token_manager.is_token_valid(session["token"]): session.pop("token", None) return jsonify({"message": "Session token is invalid!"}), 401 - elif tokenManager.is_token_expired(session["token"]): + elif token_manager.is_token_expired(session["token"]): session.pop("token", None) return jsonify({"message": "Session token has expired!"}), 401 # Discord OAuth session authentication successful logger.debug("Dual auth: Discord OAuth session authentication successful") # Set clerk_user_email from session if available for compatibility - username = tokenManager.retrieve_username(session["token"]) + username = token_manager.retrieve_username(session["token"]) if username: request.clerk_user_email = username # type: ignore[attr-defined] return f(*args, **kwargs) @@ -72,17 +73,17 @@ def wrapper(*args, **kwargs): return jsonify({"message": "Authentication required!"}), 401 try: - if not tokenManager.is_token_valid(token): + if not token_manager.is_token_valid(token): logger.debug("Dual auth: Discord OAuth token is invalid") return jsonify({"message": "Token is invalid!"}), 401 - elif tokenManager.is_token_expired(token): + elif token_manager.is_token_expired(token): logger.debug("Dual auth: Discord OAuth token is expired") return jsonify({"message": "Token is expired!"}), 403 # Discord OAuth token authentication successful logger.debug("Dual auth: Discord OAuth token authentication successful") # Set clerk_user_email from token for compatibility - username = tokenManager.retrieve_username(token) + username = token_manager.retrieve_username(token) if username: request.clerk_user_email = username # type: ignore[attr-defined] return f(*args, **kwargs) @@ -104,10 +105,10 @@ def wrapper(*args, **kwargs): # Check session cookie first if session.get("token"): try: - if not tokenManager.is_token_valid(session["token"]): + if not token_manager.is_token_valid(session["token"]): session.pop("token", None) return jsonify({"message": "Session token is invalid!"}), 401 - elif tokenManager.is_token_expired(session["token"]): + elif token_manager.is_token_expired(session["token"]): session.pop("token", None) return jsonify({"message": "Session token has expired!"}), 401 return f(*args, **kwargs) @@ -124,10 +125,10 @@ def wrapper(*args, **kwargs): return jsonify({"message": "Authentication required!"}), 401 try: - if not tokenManager.is_token_valid(token): + if not token_manager.is_token_valid(token): logger.debug("Token is invalid") return jsonify({"message": "Token is invalid!"}), 401 - elif tokenManager.is_token_expired(token): + elif token_manager.is_token_expired(token): logger.debug("Token is expired") return jsonify({"message": "Token is expired!"}), 403 return f(*args, **kwargs) @@ -158,10 +159,10 @@ def wrapper(*args, **kwargs): logger.debug("Found session token") try: logger.debug("Validating session token...") - if not tokenManager.is_token_valid(token): + if not token_manager.is_token_valid(token): logger.debug("Session token is invalid!") return jsonify({"message": "Token is invalid!"}), 401 - elif tokenManager.is_token_expired(token): + elif token_manager.is_token_expired(token): logger.debug("Session token is expired!") return jsonify({"message": "Token is expired!"}), 403 @@ -198,16 +199,16 @@ def wrapper(*args, **kwargs): try: logger.debug("Validating API token...") - if not tokenManager.is_token_valid(token): + if not token_manager.is_token_valid(token): logger.debug("API token is invalid!") return jsonify({"message": "Token is invalid!"}), 401 - elif tokenManager.is_token_expired(token): + elif token_manager.is_token_expired(token): logger.debug("API token is expired!") return jsonify({"message": "Token is expired!"}), 403 logger.debug("API token is valid, decoding...") # For API calls, we need to verify superadmin status from the token - token_data = tokenManager.decode_token(token) + token_data = token_manager.decode_token(token) if not token_data: logger.debug("Failed to decode token data!") return jsonify({"message": "Invalid token data!"}), 401 @@ -342,7 +343,7 @@ def wrapper(*args, **kwargs): # Get organization from database try: from modules.organizations.models import Organization - from shared import db_connect + from modules.utils.db import db_connect logger.debug("Getting database connection...") db = next(db_connect.get_db()) diff --git a/modules/bot/api.py b/modules/bot/api.py index 24fae333..22fd66ff 100644 --- a/modules/bot/api.py +++ b/modules/bot/api.py @@ -3,8 +3,8 @@ from flask import Blueprint, current_app, jsonify, request +from modules.utils.db import db_connect as db from modules.utils.logging_config import get_logger -from shared import db_connect as db # Get module logger logger = get_logger("bot.api") diff --git a/modules/bot/discord_modules/bot.py b/modules/bot/discord_modules/bot.py index fb4339d4..3d027f00 100644 --- a/modules/bot/discord_modules/bot.py +++ b/modules/bot/discord_modules/bot.py @@ -110,7 +110,7 @@ def check_officer(self, user_id, superadmin_user_id) -> list[int]: Returns a list of guild IDs where the user has the officer role. """ from modules.organizations.models import Organization - from shared import db_connect + from modules.utils.db import db_connect logger.debug(f"check_officer called for user_id: {user_id}, superadmin_user_id: {superadmin_user_id}") guild_ids_with_officer_role = [] diff --git a/modules/calendar/api.py b/modules/calendar/api.py index 30f94f5f..e620c08b 100644 --- a/modules/calendar/api.py +++ b/modules/calendar/api.py @@ -4,9 +4,8 @@ from modules.auth.decoraters import auth_required from modules.organizations.models import Organization - -# Assuming shared resources are correctly set up -from shared import db_connect, logger # Remove calendar_service import +from modules.utils.db import db_connect +from modules.utils.logging_config import logger # Import the new service and error handler from .errors import APIErrorHandler diff --git a/modules/calendar/clients.py b/modules/calendar/clients.py index 76ee826d..ec7568c1 100644 --- a/modules/calendar/clients.py +++ b/modules/calendar/clients.py @@ -8,9 +8,9 @@ from notion_client.helpers import collect_paginated_api from sentry_sdk import capture_exception, set_context, start_transaction -# Assuming shared resources are correctly set up -from shared import config, logger -from shared import notion as notion_shared_client +from modules.utils.config import config +from modules.utils.logging_config import logger +from modules.utils.notion import notion as notion_shared_client # Import custom modules from .errors import APIErrorHandler diff --git a/modules/calendar/service.py b/modules/calendar/service.py index 771c85f1..47904ef7 100644 --- a/modules/calendar/service.py +++ b/modules/calendar/service.py @@ -7,9 +7,9 @@ # Import organization models from modules.organizations.models import Organization - -# Assuming shared resources are correctly set up -from shared import config, db_connect, logger +from modules.utils.config import config +from modules.utils.db import db_connect +from modules.utils.logging_config import logger # Import custom modules from .clients import GoogleCalendarClient, NotionCalendarClient diff --git a/modules/calendar/utils.py b/modules/calendar/utils.py index 36dfa8ae..76873050 100644 --- a/modules/calendar/utils.py +++ b/modules/calendar/utils.py @@ -7,7 +7,8 @@ import pytz from sentry_sdk import capture_exception, set_context -from shared import config, logger # Assuming logger and config are available in shared +from modules.utils.config import config +from modules.utils.logging_config import logger # If logger is not in shared, initialize it here: # logger = logging.getLogger(__name__) diff --git a/modules/organizations/README.md b/modules/organizations/README.md index 5df61574..220fa1e7 100644 --- a/modules/organizations/README.md +++ b/modules/organizations/README.md @@ -81,7 +81,7 @@ organizations/ ```python from modules.organizations.models import Organization from modules.organizations.config import OrganizationSettings -from shared import db_connect +from modules.utils.db import db_connect # Create a new organization with db_connect.get_session() as session: diff --git a/modules/organizations/api.py b/modules/organizations/api.py index 23fd46bd..e81919ed 100644 --- a/modules/organizations/api.py +++ b/modules/organizations/api.py @@ -5,7 +5,7 @@ from modules.auth.decoraters import auth_required from modules.organizations.models import Organization -from shared import db_connect +from modules.utils.db import db_connect organizations_blueprint = Blueprint("organizations", __name__) diff --git a/modules/points/api.py b/modules/points/api.py index e7c09d68..412d4d90 100644 --- a/modules/points/api.py +++ b/modules/points/api.py @@ -9,8 +9,9 @@ from modules.auth.decoraters import auth_required from modules.points.models import Points, User +from modules.utils.db import db_connect from modules.utils.logging_config import logger -from shared import db_connect, tokenManager +from modules.utils.TokenManager import token_manager points_blueprint = Blueprint("points", __name__, template_folder=None, static_folder=None) @@ -727,9 +728,9 @@ def get_org_leaderboard(org_prefix): if token: try: # Check if the token is valid and not expired - if tokenManager.is_token_valid(token) and not tokenManager.is_token_expired(token): + if token_manager.is_token_valid(token) and not token_manager.is_token_expired(token): show_email = True # If valid, set to show email - elif tokenManager.is_token_expired(token): + elif token_manager.is_token_expired(token): return jsonify({"message": "Token is expired!"}), 403 # Expired token except Exception as e: return jsonify({"message": str(e)}), 401 # Token is invalid or some error occurred diff --git a/modules/public/api.py b/modules/public/api.py index 69a97aa2..b6566b8d 100644 --- a/modules/public/api.py +++ b/modules/public/api.py @@ -6,7 +6,7 @@ from modules.auth.decoraters import error_handler from modules.points.models import Points, User -from shared import db_connect +from modules.utils.db import db_connect # Update the blueprint to include the static folder public_blueprint = Blueprint( diff --git a/modules/superadmin/api.py b/modules/superadmin/api.py index 67cb91c6..9b2d8722 100644 --- a/modules/superadmin/api.py +++ b/modules/superadmin/api.py @@ -3,8 +3,10 @@ from modules.auth.decoraters import superadmin_required from modules.organizations.config import OrganizationSettings from modules.organizations.models import Organization +from modules.utils.config import config +from modules.utils.db import db_connect from modules.utils.logging_config import get_logger -from shared import config, db_connect, tokenManager +from modules.utils.TokenManager import token_manager logger = get_logger(__name__) @@ -31,7 +33,7 @@ def check_superadmin(): # Decode the token to get user information logger.debug("Decoding token...") - token_data = tokenManager.decode_token(token) + token_data = token_manager.decode_token(token) if not token_data: logger.error("Failed to decode token") return jsonify({"error": "Invalid token"}), 401 diff --git a/modules/users/api.py b/modules/users/api.py index 1b3bea46..a1bc6aa6 100644 --- a/modules/users/api.py +++ b/modules/users/api.py @@ -5,7 +5,7 @@ from modules.auth.decoraters import auth_required, error_handler from modules.points.models import Points, User -from shared import db_connect +from modules.utils.db import db_connect logger = logging.getLogger(__name__) diff --git a/modules/utils/clerk_auth.py b/modules/utils/clerk_auth.py index 4d176ff5..782ae227 100644 --- a/modules/utils/clerk_auth.py +++ b/modules/utils/clerk_auth.py @@ -5,8 +5,8 @@ from clerk_backend_api.security.types import AuthenticateRequestOptions from flask import jsonify, request +from modules.utils.config import config from modules.utils.logging_config import get_logger -from shared import config logger = get_logger("utils.clerk_auth") diff --git a/shared.py b/shared.py deleted file mode 100644 index c209edea..00000000 --- a/shared.py +++ /dev/null @@ -1,55 +0,0 @@ -import sentry_sdk -from sentry_sdk.integrations.flask import FlaskIntegration - -from modules.bot.discord_modules.bot import BotFork -from modules.utils.app import app -from modules.utils.config import config -from modules.utils.db import db_connect -from modules.utils.logging_config import logger -from modules.utils.notion import notion -from modules.utils.TokenManager import token_manager as tokenManager - -if config.SENTRY_DSN: - sentry_sdk.init( - dsn=config.SENTRY_DSN, - integrations=[FlaskIntegration()], - traces_sample_rate=1.0, - profiles_sample_rate=1.0, - enable_logs=True, - ) - logger.info("Sentry initialized with logging enabled.") -else: - logger.warning("SENTRY_DSN not found in environment. Sentry not initialized.") - - -def create_auth_bot(loop): - """Create and configure the auth bot (BotFork) instance with a specific event loop.""" - import discord - - logger.info("Creating auth bot instance (BotFork)...") - intents = discord.Intents.default() - intents.members = True - intents.guilds = True - - auth_bot_instance = BotFork(intents=intents, loop=loop) - try: - from modules.bot.discord_modules.cogs.GameCog import GameCog - from modules.bot.discord_modules.cogs.HelperCog import HelperCog - - auth_bot_instance.add_cog(HelperCog(auth_bot_instance)) - auth_bot_instance.add_cog(GameCog(auth_bot_instance)) - logger.info("Auth bot cogs (HelperCog, GameCog) registered with BotFork instance.") - except Exception as e: - logger.error(f"Error registering auth bot cogs: {e}", exc_info=True) - return auth_bot_instance - - -__all__ = [ - "app", - "config", - "db_connect", - "logger", - "notion", - "tokenManager", - "create_auth_bot", -] From 5bf25ce46bf8ac933bc552285960b3db0ea881cf Mon Sep 17 00:00:00 2001 From: Ben Juntilla Date: Wed, 11 Feb 2026 16:28:37 -0700 Subject: [PATCH 3/3] Remove leftover shared.py references from alembic env and dev compose Amp-Thread-ID: https://ampcode.com/threads/T-019c4f08-01fd-773b-b695-28d6a1bf88cf Co-authored-by: Amp --- alembic/env.py | 13 ------------- docker-compose.dev.yml | 1 - 2 files changed, 14 deletions(-) diff --git a/alembic/env.py b/alembic/env.py index 07edbbc8..8ff80f6f 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -1,7 +1,5 @@ -import logging import os import sys -import types from importlib import import_module from logging.config import fileConfig @@ -22,17 +20,6 @@ # Ensure the project root is on sys.path so module imports work. sys.path.insert(0, os.path.realpath(os.path.join(os.path.dirname(__file__), ".."))) -# Stub out ``shared`` before importing model modules. The calendar models -# import ``shared`` transitively (calendar.models -> calendar.utils -> shared), -# which would initialise Config, the database, Discord bots, background -# threads, etc. None of that is needed for Alembic – we only need the -# SQLAlchemy table metadata. -if "shared" not in sys.modules: - _stub = types.ModuleType("shared") - _stub.config = types.SimpleNamespace() # type: ignore[attr-defined] - _stub.logger = logging.getLogger("alembic.stub") # type: ignore[attr-defined] - sys.modules["shared"] = _stub - # Import the declarative Base and all model modules so that # Base.metadata is fully populated for autogenerate support. from modules.utils.base import Base # noqa: E402 diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 51d89faf..94804961 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -5,6 +5,5 @@ services: - ./data:/app/data - ./modules:/app/modules - ./main.py:/app/main.py - - ./shared.py:/app/shared.py environment: - IS_PROD=false \ No newline at end of file