diff --git a/musicround/config.py b/musicround/config.py index 4ff612e..b972b55 100644 --- a/musicround/config.py +++ b/musicround/config.py @@ -62,7 +62,10 @@ class Config: DROPBOX_REDIRECT_URI = os.getenv("DROPBOX_REDIRECT_URI", "http://localhost:5000/users/dropbox/callback") MAIL_HOST = os.getenv("MAIL_HOST", "localhost") - MAIL_PORT = os.getenv("MAIL_PORT", 25) + try: + MAIL_PORT = int(os.getenv("MAIL_PORT", "25")) + except (ValueError, TypeError): + MAIL_PORT = 25 MAIL_USE_TLS = os.getenv("MAIL_USE_TLS", "False") == "True" MAIL_USE_SSL = os.getenv("MAIL_USE_SSL", "False") == "True" MAIL_USERNAME = os.getenv("MAIL_USERNAME", "") @@ -73,7 +76,9 @@ class Config: # Automation settings AUTOMATION_TOKEN = os.getenv("AUTOMATION_TOKEN") if not AUTOMATION_TOKEN: - raise ValueError("AUTOMATION_TOKEN environment variable must be set. Generate a secure token with: python -c 'import secrets; print(secrets.token_urlsafe(32))'") # Reverse proxy settings + raise ValueError("AUTOMATION_TOKEN environment variable must be set. Generate a secure token with: python -c 'import secrets; print(secrets.token_urlsafe(32))'") + + # Reverse proxy settings USE_HTTPS = os.getenv("USE_HTTPS", "False") == "True" # Force HTTPS URL generation PREFERRED_URL_SCHEME = os.getenv("PREFERRED_URL_SCHEME", 'https' if USE_HTTPS else 'http') diff --git a/musicround/helpers/email_helper.py b/musicround/helpers/email_helper.py index e739f70..55f34b6 100644 --- a/musicround/helpers/email_helper.py +++ b/musicround/helpers/email_helper.py @@ -82,7 +82,7 @@ def send_email(recipient, subject, body_text, attachments=None): try: current_app.logger.info(f"Attempting to send email to {recipient} via {mail_host}:{mail_port}") - with smtplib.SMTP(mail_host, mail_port) as server: + with smtplib.SMTP(mail_host, mail_port, timeout=30) as server: server.starttls() current_app.logger.debug("STARTTLS established") server.login(mail_username, mail_password) diff --git a/musicround/helpers/metadata.py b/musicround/helpers/metadata.py index b29a1c5..e7625fd 100644 --- a/musicround/helpers/metadata.py +++ b/musicround/helpers/metadata.py @@ -504,7 +504,7 @@ def get_deezer_data(isrc, app=None): if not deezer_client: # If no client in app context, make direct API call - response = requests.get(f"https://api.deezer.com/track/isrc:{isrc}") + response = requests.get(f"https://api.deezer.com/track/isrc:{isrc}", timeout=10) if response.status_code == 200: track = response.json() else: @@ -539,7 +539,7 @@ def get_deezer_data(isrc, app=None): if deezer_client: album = deezer_client.get_album(album_id) else: - album_response = requests.get(f"https://api.deezer.com/album/{album_id}") + album_response = requests.get(f"https://api.deezer.com/album/{album_id}", timeout=10) album = album_response.json() if album_response.status_code == 200 else None if album and not album.get('error'): @@ -591,7 +591,7 @@ def get_lastfm_data(artist_name, track_title, app=None): 'format': 'json' } - response = requests.get(url=url, params=params) + response = requests.get(url=url, params=params, timeout=10) if response.status_code == 200: data = response.json() @@ -753,7 +753,7 @@ def get_acrcloud_data(isrc, app=None): 'include_works': 1 # Include additional work metadata } - response = requests.get(url, headers=headers, params=params) + response = requests.get(url, headers=headers, params=params, timeout=10) if response.status_code != 200: app.logger.warning(f"ACRCloud API error: {response.status_code} - {response.text}") return result diff --git a/musicround/helpers/spotify_helper.py b/musicround/helpers/spotify_helper.py index 02db085..7d31dee 100644 --- a/musicround/helpers/spotify_helper.py +++ b/musicround/helpers/spotify_helper.py @@ -26,7 +26,7 @@ def refresh_spotify_token(refresh_token): 'client_secret': client_secret } - response = requests.post('https://accounts.spotify.com/api/token', data=data) + response = requests.post('https://accounts.spotify.com/api/token', data=data, timeout=10) if response.status_code == 200: return response.json() @@ -181,7 +181,7 @@ def get_spotify_user_info(access_token): } try: - response = requests.get('https://api.spotify.com/v1/me', headers=headers) + response = requests.get('https://api.spotify.com/v1/me', headers=headers, timeout=10) if response.status_code == 200: return response.json() diff --git a/musicround/models.py b/musicround/models.py index 363e3d6..cdf9e65 100644 --- a/musicround/models.py +++ b/musicround/models.py @@ -100,6 +100,8 @@ class User(db.Model, UserMixin): def check_password(self, password): """Check if provided password matches the hash""" + if not self.password_hash: + return False return check_password_hash(self.password_hash, password) def set_token(self): @@ -111,8 +113,8 @@ class User(db.Model, UserMixin): """Check if user has a specific role""" return any(role.name == role_name for role in self.roles) - def is_admin(self): - """Check if user is an admin""" + def is_admin_by_role(self): + """Check if user is an admin via role assignment""" return self.has_role('admin') def __repr__(self): @@ -323,7 +325,9 @@ class SystemSetting(db.Model): db.session.add(setting) else: setting.value = value - db.session.commit() @staticmethod + db.session.commit() + + @staticmethod def all_settings(): return {s.key: s.value for s in SystemSetting.query.all()} diff --git a/musicround/routes/core.py b/musicround/routes/core.py index 2620f10..021aebf 100644 --- a/musicround/routes/core.py +++ b/musicround/routes/core.py @@ -302,7 +302,10 @@ def serve_user_audio(filepath): """ Serve user custom audio files from the data directory """ - if '..' in filepath: + # Resolve the real path to prevent path traversal attacks + base_dir = os.path.realpath('/data') + requested_path = os.path.realpath(os.path.join('/data', filepath)) + if not requested_path.startswith(base_dir + os.sep) and requested_path != base_dir: abort(404) if 'custommp3/' in filepath: diff --git a/musicround/routes/db_admin.py b/musicround/routes/db_admin.py index e6ebfbf..1bccc75 100644 --- a/musicround/routes/db_admin.py +++ b/musicround/routes/db_admin.py @@ -18,7 +18,7 @@ def admin_required(view_func): if not current_user.is_authenticated: return redirect(url_for('users.login')) - if not current_user.is_admin(): + if not current_user.is_admin: flash('Admin access required.', 'danger') return redirect(url_for('core.index')) @@ -37,7 +37,7 @@ def raw_db_access(): # Base model view with authentication class AuthModelView(ModelView): def is_accessible(self): - return current_user.is_authenticated and current_user.is_admin() + return current_user.is_authenticated and current_user.is_admin def inaccessible_callback(self, name, **kwargs): if not current_user.is_authenticated: diff --git a/musicround/routes/import_routes.py b/musicround/routes/import_routes.py index f0c98ef..34d6122 100644 --- a/musicround/routes/import_routes.py +++ b/musicround/routes/import_routes.py @@ -750,7 +750,7 @@ def queue_status(): Display real-time status of the import queue for administrators """ # Check if user is an admin - if not current_user.is_admin(): + if not current_user.is_admin: flash('Admin access required for Import Queue view.', 'danger') return redirect(url_for('core.index')) diff --git a/musicround/routes/import_songs.py b/musicround/routes/import_songs.py index ffbb213..b9ba6fa 100644 --- a/musicround/routes/import_songs.py +++ b/musicround/routes/import_songs.py @@ -85,7 +85,10 @@ def import_playlist(): flash("No playlist ID provided for import.", "danger") return redirect(request.referrer or url_for('core.search')) - priority = int(request.form.get('priority', 10)) + try: + priority = int(request.form.get('priority', 10)) + except (ValueError, TypeError): + priority = 10 queue = current_app.config.get('import_queue') if not queue: flash("Import queue not initialized.", "danger") diff --git a/musicround/routes/process.py b/musicround/routes/process.py index 76d5a0d..5dcd047 100644 --- a/musicround/routes/process.py +++ b/musicround/routes/process.py @@ -1,16 +1,15 @@ from flask import Blueprint, session, redirect, url_for, jsonify, request, current_app +from flask_login import login_required import base64 process_bp = Blueprint('process', __name__, url_prefix='/process') @process_bp.route('/base64', methods=['POST']) +@login_required def base64_encode_data(): """ Return base64-encoded string from data provided in request body. """ - if 'access_token' not in session: - return redirect(url_for('users.login')) # Assuming 'users.login' is the correct endpoint - # Get binary data from request data = request.get_data() if not data: diff --git a/musicround/routes/users.py b/musicround/routes/users.py index 7b81802..6913ed9 100644 --- a/musicround/routes/users.py +++ b/musicround/routes/users.py @@ -61,7 +61,7 @@ def admin_required(f): from functools import wraps @wraps(f) def decorated_function(*args, **kwargs): - if not current_user.is_authenticated or not current_user.is_admin(): + if not current_user.is_authenticated or not current_user.is_admin: flash('Admin access required.', 'danger') return redirect(url_for('users.profile')) return f(*args, **kwargs) @@ -882,7 +882,7 @@ def use_refresh_token(): @login_required def setup(): """One-time setup route to promote the current user to admin""" - if current_user.is_admin(): + if current_user.is_admin: flash('You are already an administrator.', 'info') return redirect(url_for('users.profile')) @@ -1264,7 +1264,7 @@ def create_backup(): if automation_token == current_app.config.get('AUTOMATION_TOKEN'): # Allow the request without authentication for automation pass - elif not current_user.is_authenticated or not current_user.is_admin(): + elif not current_user.is_authenticated or not current_user.is_admin: return jsonify({"status": "error", "message": "Unauthorized"}), 401 # Get custom backup name if provided diff --git a/musicround/templates/base.html b/musicround/templates/base.html index e52c363..294ad90 100644 --- a/musicround/templates/base.html +++ b/musicround/templates/base.html @@ -145,7 +145,7 @@
  • View Rounds
  • - {% if current_user.is_admin() %} + {% if current_user.is_admin %}