Add Spotify integration with improved token management and user interface

- Implemented Spotify OAuth handling in spotify_client_manager.py to ensure valid access tokens for users.
- Created helper functions in spotify_helper.py for refreshing tokens and retrieving user information.
- Developed manage_spotify.html template for connecting and managing Spotify accounts, displaying connection status and token information.
- Added logging for token management processes to enhance debugging and monitoring.
- Introduced a debug client for Spotify interactions to facilitate easier testing and development.
This commit is contained in:
Christian Krakau-Louis
2025-05-27 21:40:37 +02:00
parent d2772b88fe
commit 2f55f898ed
44 changed files with 2317 additions and 1884 deletions
+54 -142
View File
@@ -4,10 +4,12 @@ Authentication routes for the Music Round application
import os
from flask import Blueprint, render_template, redirect, url_for, request, flash, current_app, session
from flask_login import login_user, current_user, logout_user, login_required
from werkzeug.security import check_password_hash
from werkzeug.security import check_password_hash, generate_password_hash
from musicround.models import User, db
from datetime import datetime
import spotipy
import requests
import secrets
from musicround.helpers.auth_helpers import oauth, find_or_create_user, update_oauth_tokens, get_spotify_user_info
# Create blueprint
auth_bp = Blueprint('auth', __name__)
@@ -26,154 +28,64 @@ def login():
@auth_bp.route('/login-with-spotify')
def login_with_spotify():
"""Start Spotify OAuth flow for login"""
# If user is already logged in, redirect to home
"""Start Spotify OAuth flow for login using Authlib."""
if current_user.is_authenticated:
return redirect(url_for('core.index'))
if not current_app.config.get('SPOTIFY_CLIENT_ID') or not current_app.config.get('SPOTIFY_CLIENT_SECRET'):
flash('Spotify login is not configured.', 'danger')
return redirect(url_for('users.login'))
# The redirect URI should point to *this* blueprint's callback
redirect_uri = url_for('auth.callback', _external=True)
# Create a new OAuth object
sp_oauth = current_app.config['sp_oauth']
# Get the authorization URL
auth_url = sp_oauth.get_authorize_url()
# Store state in session for validation
session['oauth_state'] = sp_oauth.state
# Set flag that we're using OAuth for login, not just connection
session['spotify_login_flow'] = True
return redirect(auth_url)
# Ensure 'show_dialog': 'true' is part of authorize_params in auth_helpers.py
# when registering the Spotify client.
return oauth.spotify.authorize_redirect(redirect_uri)
@auth_bp.route('/callback')
def callback():
"""Handle Spotify OAuth callback for login"""
"""Handle Spotify OAuth callback for login using Authlib."""
try:
# Verify the state parameter
if request.args.get('state') != session.get('oauth_state'):
flash("Authentication state mismatch. Please try logging in again.", "danger")
return redirect(url_for('auth.index'))
# Get the authorization code
code = request.args.get('code')
if not code:
flash("No authorization code received from Spotify.", "danger")
return redirect(url_for('auth.index'))
# Exchange the code for an access token
sp_oauth = current_app.config['sp_oauth']
token_info = sp_oauth.get_access_token(code)
if not token_info or 'access_token' not in token_info:
flash("Failed to obtain access token from Spotify.", "danger")
return redirect(url_for('auth.index'))
# Store the token in the session
session['access_token'] = token_info['access_token']
session['refresh_token'] = token_info.get('refresh_token')
session['token_expiration'] = token_info.get('expires_at')
session['token_source'] = 'user'
# Get user info from Spotify to find or create the user account
sp = spotipy.Spotify(auth=token_info['access_token'])
spotify_user_info = sp.current_user()
if not spotify_user_info or 'id' not in spotify_user_info:
flash("Could not fetch user information from Spotify.", "danger")
return redirect(url_for('auth.index'))
spotify_id = spotify_user_info['id']
email = spotify_user_info.get('email')
display_name = spotify_user_info.get('display_name', spotify_id)
# Log the Spotify login attempt
current_app.logger.info(f"Spotify login attempt: ID={spotify_id}, Email={email}, Name={display_name}")
# Look for an existing user with this Spotify ID
user = User.query.filter_by(oauth_id=spotify_id).first()
# If no user found with this Spotify ID but we have an email, try to find by email
if not user and email:
user = User.query.filter_by(email=email).first()
if user:
# Update the user's Spotify ID if they have an account with the same email
user.oauth_id = spotify_id
current_app.logger.info(f"Linked Spotify ID {spotify_id} to existing account: {user.username}")
# If we still don't have a user, create a new one
token = oauth.spotify.authorize_access_token()
current_app.logger.debug(f"Spotify token received for login: {token}")
# Fetch user info using the token
spotify_info = get_spotify_user_info(token)
if not spotify_info or not spotify_info.get('id'):
flash('Could not fetch Spotify user information. Please try again.', 'danger')
current_app.logger.error(f"Failed to get Spotify user info for login. Response: {spotify_info}")
return redirect(url_for('users.login'))
# Find or create user based on Spotify profile
# This function needs to handle new user creation if they don't exist
# or link to an existing user if email matches, etc.
user = find_or_create_user(spotify_info, 'spotify')
if not user:
if not email:
# If Spotify didn't provide an email, we can't create a new user automatically
flash("Your Spotify account does not have an email address. Please register manually.", "danger")
return redirect(url_for('users.register'))
# Generate a unique username based on Spotify display name
base_username = ''.join(c for c in display_name if c.isalnum()).lower()
if not base_username:
base_username = "spotify_user"
username = base_username
count = 1
while User.query.filter_by(username=username).first():
username = f"{base_username}{count}"
count += 1
# Create a new user
from werkzeug.security import generate_password_hash
import secrets
# Generate a random password - user can reset it later
random_password = secrets.token_urlsafe(12)
user = User(
username=username,
email=email,
password_hash=generate_password_hash(random_password),
first_name=display_name.split()[0] if ' ' in display_name else display_name,
last_name=' '.join(display_name.split()[1:]) if ' ' in display_name else '',
oauth_id=spotify_id,
created_at=datetime.now(),
last_login=datetime.now()
)
try:
db.session.add(user)
db.session.commit()
current_app.logger.info(f"Created new user from Spotify: {username} (ID: {user.id})")
flash(f"Welcome! A new account has been created for you as '{username}'.", "success")
except Exception as e:
db.session.rollback()
current_app.logger.error(f"Error creating user from Spotify: {e}")
flash("Error creating account. Please try again or register manually.", "danger")
return redirect(url_for('users.register'))
# Store the Spotify tokens in the user's account
user.spotify_token = token_info['access_token']
user.spotify_refresh_token = token_info.get('refresh_token')
if 'expires_at' in token_info:
user.spotify_token_expiry = datetime.fromtimestamp(token_info['expires_at'])
# Update last login time
user.last_login = datetime.now()
try:
db.session.commit()
except Exception as e:
db.session.rollback()
current_app.logger.error(f"Error updating user with Spotify tokens: {e}")
flash("Error updating your account with Spotify information.", "danger")
flash('Could not sign in with Spotify. If you are a new user, registration might be disabled. Please try again or contact support.', 'danger')
current_app.logger.error(f"Failed to find or create user for Spotify login: {spotify_info.get('email')}")
return redirect(url_for('users.login'))
# Log the user in
login_user(user)
# Update the Spotify client with the new token
current_app.config['sp'].set_auth(token_info['access_token'])
flash("Successfully logged in with Spotify!", "success")
return redirect(url_for('core.index'))
# Update tokens in the User model
if update_oauth_tokens(user, token, 'spotify'):
login_user(user) # Log in the user
user.last_login = datetime.now()
db.session.commit()
flash('Successfully logged in with Spotify!', 'success')
current_app.logger.info(f"User {user.username} logged in via Spotify ({spotify_info.get('name')})")
next_page = request.args.get('next') or session.pop('next_url', None)
if not next_page or not next_page.startswith('/'):
next_page = url_for('core.index')
return redirect(next_page)
else:
flash('Failed to store Spotify tokens. Please try again.', 'danger')
current_app.logger.error(f"Failed to update Spotify tokens for user {user.username} during login.")
return redirect(url_for('users.login'))
except Exception as e:
current_app.logger.error(f"Error during Spotify callback: {e}")
flash("Error during Spotify authentication. Please try again.", "danger")
return redirect(url_for('auth.index'))
current_app.logger.error(f"Error in Spotify login callback: {str(e)}")
flash(f'An error occurred during Spotify login: {str(e)}.', 'danger')
return redirect(url_for('users.login'))