Complete per-profile service credentials feature
Adds Tidal per-profile OAuth with token storage on profile row. Auth initiation stores profile_id in PKCE state, callback detects it and stores encrypted tokens per-profile instead of globally. Personal settings modal now shows Spotify, Tidal, Server Library, and ListenBrainz sections for non-admin profiles. Admin sees only ListenBrainz (unchanged). Server library selection wired into playlist sync via _apply_profile_library. Full per-profile support: Spotify (credentials + OAuth + playlists), Tidal (OAuth + token storage), server library (Plex/Jellyfin/Navidrome), ListenBrainz (existing). All backwards compatible — upgrading users see zero change.
This commit is contained in:
parent
be27c36da2
commit
53477768cb
2 changed files with 60 additions and 4 deletions
|
|
@ -5958,6 +5958,11 @@ def auth_tidal():
|
|||
|
||||
print(f"🔐 Stored PKCE - verifier: {temp_tidal_client.code_verifier[:20]}... challenge: {temp_tidal_client.code_challenge[:20]}...")
|
||||
|
||||
# Store profile_id for per-profile auth
|
||||
profile_id = request.args.get('profile_id', '')
|
||||
with tidal_oauth_lock:
|
||||
tidal_oauth_state["profile_id"] = profile_id if profile_id and profile_id != '1' else None
|
||||
|
||||
# Create OAuth URL
|
||||
import urllib.parse
|
||||
params = {
|
||||
|
|
@ -6216,17 +6221,42 @@ def tidal_callback():
|
|||
# Create a temporary client for the token exchange
|
||||
temp_tidal_client = TidalClient()
|
||||
|
||||
# Restore PKCE values and redirect_uri from the auth request
|
||||
# Restore PKCE values, redirect_uri, and profile_id from the auth request
|
||||
profile_id_for_tidal = None
|
||||
with tidal_oauth_lock:
|
||||
temp_tidal_client.code_verifier = tidal_oauth_state["code_verifier"]
|
||||
temp_tidal_client.code_challenge = tidal_oauth_state["code_challenge"]
|
||||
if "redirect_uri" in tidal_oauth_state:
|
||||
temp_tidal_client.redirect_uri = tidal_oauth_state["redirect_uri"]
|
||||
profile_id_for_tidal = tidal_oauth_state.get("profile_id")
|
||||
|
||||
success = temp_tidal_client.fetch_token_from_code(auth_code)
|
||||
|
||||
|
||||
if success:
|
||||
# Re-initialize the main global tidal_client instance with the new token
|
||||
# Per-profile: store tokens on profile, don't touch global client
|
||||
if profile_id_for_tidal:
|
||||
try:
|
||||
profile_id_int = int(profile_id_for_tidal)
|
||||
db = get_database()
|
||||
# Store Tidal tokens on the profile
|
||||
from config.settings import config_manager as _cm
|
||||
enc_access = _cm._encrypt_value(temp_tidal_client.access_token) if temp_tidal_client.access_token else None
|
||||
enc_refresh = _cm._encrypt_value(temp_tidal_client.refresh_token) if temp_tidal_client.refresh_token else None
|
||||
with db._get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
UPDATE profiles
|
||||
SET tidal_access_token = ?, tidal_refresh_token = ?,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
""", (enc_access, enc_refresh, profile_id_int))
|
||||
conn.commit()
|
||||
add_activity_item("✅", "Tidal Auth Complete", f"Profile {profile_id_int} authenticated with Tidal", "Now")
|
||||
return "<h1>✅ Tidal Authentication Successful!</h1><p>Your personal Tidal account is now connected. You can close this window.</p>"
|
||||
except Exception as profile_err:
|
||||
print(f"⚠️ Per-profile Tidal auth failed, falling back to global: {profile_err}")
|
||||
|
||||
# Global: Re-initialize the main global tidal_client instance with the new token
|
||||
tidal_client = TidalClient()
|
||||
if tidal_enrichment_worker:
|
||||
tidal_enrichment_worker.client = tidal_client
|
||||
|
|
|
|||
|
|
@ -1253,9 +1253,10 @@ async function openPersonalSettings() {
|
|||
|
||||
// Build sections
|
||||
body.innerHTML = '';
|
||||
// Spotify + server library per-profile only shown for non-admin profiles
|
||||
// Spotify + Tidal + server library per-profile only shown for non-admin profiles
|
||||
if (currentProfile && !currentProfile.is_admin) {
|
||||
renderPersonalSettingsSpotify(body, spotifyData);
|
||||
renderPersonalSettingsTidal(body);
|
||||
try {
|
||||
const libRes = await fetch('/api/profiles/me/server-library');
|
||||
const libData = await libRes.json();
|
||||
|
|
@ -1376,6 +1377,31 @@ async function authenticatePersonalSpotify() {
|
|||
window.open('/auth/spotify?profile_id=' + (currentProfile?.id || ''), '_blank');
|
||||
}
|
||||
|
||||
function renderPersonalSettingsTidal(body) {
|
||||
const section = document.createElement('div');
|
||||
section.id = 'ps-tidal-section';
|
||||
section.innerHTML = `
|
||||
<div class="ps-section">
|
||||
<div class="ps-section-header">
|
||||
<h4 class="ps-section-title">Tidal</h4>
|
||||
</div>
|
||||
<div class="ps-help-text" style="margin-bottom:12px;">
|
||||
Connect your own Tidal account to see your playlists. Uses the admin's Tidal app credentials.
|
||||
</div>
|
||||
<div class="ps-actions">
|
||||
<button class="ps-btn ps-btn-primary" onclick="authenticatePersonalTidal()">🔐 Authenticate Tidal</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
const existing = document.getElementById('ps-tidal-section');
|
||||
if (existing) existing.replaceWith(section);
|
||||
else body.appendChild(section);
|
||||
}
|
||||
|
||||
function authenticatePersonalTidal() {
|
||||
window.open('/auth/tidal?profile_id=' + (currentProfile?.id || ''), '_blank');
|
||||
}
|
||||
|
||||
function renderPersonalSettingsServerLibrary(body, data) {
|
||||
const plexLib = data.plex_library_id || '';
|
||||
const jellyfinUser = data.jellyfin_user_id || '';
|
||||
|
|
|
|||
Loading…
Reference in a new issue