Skip to content
Open
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
13 changes: 0 additions & 13 deletions alembic/env.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import logging
import os
import sys
import types
from importlib import import_module
from logging.config import fileConfig

Expand All @@ -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
Expand Down
1 change: 0 additions & 1 deletion docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
58 changes: 57 additions & 1 deletion main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@
import os
import subprocess # nosec B404 - subprocess needed for git commit hash retrieval
import threading
import time
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
Expand All @@ -17,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
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")
Expand Down Expand Up @@ -81,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()
Expand Down Expand Up @@ -111,6 +154,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:
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}")
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()
Expand Down
27 changes: 14 additions & 13 deletions modules/auth/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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


Expand All @@ -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"])
Expand All @@ -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()
Expand Down
35 changes: 18 additions & 17 deletions modules/auth/decoraters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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())
Expand Down
2 changes: 1 addition & 1 deletion modules/bot/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion modules/bot/discord_modules/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down
5 changes: 2 additions & 3 deletions modules/calendar/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions modules/calendar/clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading