fix(auth): persist ToS acceptance in Rust/SQLite backend
ToS acceptance was stored in localStorage which gets cleared on app updates, causing users to re-login frequently. Now stores ToS state in SQLite (~/.local/share/qbz/legal_settings.db) which survives updates. - Add legal_settings.rs with SQLite storage for ToS acceptance - Update qobuzLegalStore.ts to use Rust invoke commands - LoginView now loads ToS from Rust before auto-login check - Migration from localStorage for existing users Closes #31
This commit is contained in:
1 parent
fe7816f6c6
commit
3926209b0d
5 files changed
+212
-8
No files matched your search
@@ -0,0 +1,117 @@
|
|||||||
|
//! Legal settings storage
|
||||||
|
//!
|
||||||
|
//! Persists user acceptance of legal terms:
|
||||||
|
//! - qobuz_tos_accepted: User has accepted Qobuz Terms of Service
|
||||||
|
|
||||||
|
use rusqlite::{Connection, params};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use log::info;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct LegalSettings {
|
||||||
|
/// User has accepted Qobuz Terms of Service
|
||||||
|
pub qobuz_tos_accepted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LegalSettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
qobuz_tos_accepted: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LegalSettingsStore {
|
||||||
|
conn: Connection,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LegalSettingsStore {
|
||||||
|
pub fn new() -> Result<Self, String> {
|
||||||
|
let data_dir = dirs::data_dir()
|
||||||
|
.ok_or("Could not determine data directory")?
|
||||||
|
.join("qbz");
|
||||||
|
|
||||||
|
std::fs::create_dir_all(&data_dir)
|
||||||
|
.map_err(|e| format!("Failed to create data directory: {}", e))?;
|
||||||
|
|
||||||
|
let db_path = data_dir.join("legal_settings.db");
|
||||||
|
let conn = Connection::open(&db_path)
|
||||||
|
.map_err(|e| format!("Failed to open legal settings database: {}", e))?;
|
||||||
|
|
||||||
|
// Create table
|
||||||
|
conn.execute_batch(
|
||||||
|
"CREATE TABLE IF NOT EXISTS legal_settings (
|
||||||
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||||
|
qobuz_tos_accepted INTEGER NOT NULL DEFAULT 0
|
||||||
|
);"
|
||||||
|
).map_err(|e| format!("Failed to create legal settings table: {}", e))?;
|
||||||
|
|
||||||
|
// Insert default row if it doesn't exist
|
||||||
|
conn.execute(
|
||||||
|
"INSERT OR IGNORE INTO legal_settings (id, qobuz_tos_accepted) VALUES (1, 0)",
|
||||||
|
[]
|
||||||
|
).map_err(|e| format!("Failed to insert default legal settings: {}", e))?;
|
||||||
|
|
||||||
|
info!("[LegalSettings] Database initialized");
|
||||||
|
|
||||||
|
Ok(Self { conn })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_settings(&self) -> Result<LegalSettings, String> {
|
||||||
|
self.conn
|
||||||
|
.query_row(
|
||||||
|
"SELECT qobuz_tos_accepted FROM legal_settings WHERE id = 1",
|
||||||
|
[],
|
||||||
|
|row| {
|
||||||
|
let qobuz_tos_accepted: i32 = row.get(0)?;
|
||||||
|
Ok(LegalSettings {
|
||||||
|
qobuz_tos_accepted: qobuz_tos_accepted != 0,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("Failed to get legal settings: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_qobuz_tos_accepted(&self, accepted: bool) -> Result<(), String> {
|
||||||
|
self.conn
|
||||||
|
.execute(
|
||||||
|
"UPDATE legal_settings SET qobuz_tos_accepted = ? WHERE id = 1",
|
||||||
|
params![accepted as i32],
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("Failed to update ToS acceptance: {}", e))?;
|
||||||
|
|
||||||
|
info!("[LegalSettings] Qobuz ToS accepted: {}", accepted);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type LegalSettingsState = Arc<Mutex<LegalSettingsStore>>;
|
||||||
|
|
||||||
|
// Tauri commands
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_legal_settings(
|
||||||
|
state: tauri::State<'_, LegalSettingsState>,
|
||||||
|
) -> Result<LegalSettings, String> {
|
||||||
|
let store = state.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||||
|
store.get_settings()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn get_qobuz_tos_accepted(
|
||||||
|
state: tauri::State<'_, LegalSettingsState>,
|
||||||
|
) -> Result<bool, String> {
|
||||||
|
let store = state.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||||
|
let settings = store.get_settings()?;
|
||||||
|
Ok(settings.qobuz_tos_accepted)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn set_qobuz_tos_accepted(
|
||||||
|
state: tauri::State<'_, LegalSettingsState>,
|
||||||
|
accepted: bool,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let store = state.lock().map_err(|e| format!("Lock error: {}", e))?;
|
||||||
|
store.set_qobuz_tos_accepted(accepted)
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ pub mod favorites_preferences;
|
|||||||
pub mod favorites_cache;
|
pub mod favorites_cache;
|
||||||
pub mod subscription_state;
|
pub mod subscription_state;
|
||||||
pub mod tray_settings;
|
pub mod tray_settings;
|
||||||
|
pub mod legal_settings;
|
||||||
|
|
||||||
pub use audio_settings::{
|
pub use audio_settings::{
|
||||||
AudioSettings,
|
AudioSettings,
|
||||||
@@ -83,3 +84,12 @@ pub use favorites_cache::{
|
|||||||
sync_cached_favorite_artists,
|
sync_cached_favorite_artists,
|
||||||
clear_favorites_cache,
|
clear_favorites_cache,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub use legal_settings::{
|
||||||
|
LegalSettings,
|
||||||
|
LegalSettingsState,
|
||||||
|
LegalSettingsStore,
|
||||||
|
get_legal_settings,
|
||||||
|
get_qobuz_tos_accepted,
|
||||||
|
set_qobuz_tos_accepted,
|
||||||
|
};
|
||||||
@@ -196,6 +196,13 @@ pub fn run() {
|
|||||||
// Initialize tray settings state
|
// Initialize tray settings state
|
||||||
let tray_settings_state = config::tray_settings::TraySettingsState::new()
|
let tray_settings_state = config::tray_settings::TraySettingsState::new()
|
||||||
.expect("Failed to initialize tray settings");
|
.expect("Failed to initialize tray settings");
|
||||||
|
// Initialize legal settings state (ToS acceptance persistence)
|
||||||
|
let legal_settings_state = config::legal_settings::LegalSettingsState::new(
|
||||||
|
std::sync::Mutex::new(
|
||||||
|
config::legal_settings::LegalSettingsStore::new()
|
||||||
|
.expect("Failed to initialize legal settings")
|
||||||
|
)
|
||||||
|
);
|
||||||
// Initialize updates state
|
// Initialize updates state
|
||||||
let updates_state = updates::UpdatesState::new()
|
let updates_state = updates::UpdatesState::new()
|
||||||
.expect("Failed to initialize updates state");
|
.expect("Failed to initialize updates state");
|
||||||
@@ -442,6 +449,7 @@ pub fn run() {
|
|||||||
.manage(favorites_prefs_state)
|
.manage(favorites_prefs_state)
|
||||||
.manage(favorites_cache_state)
|
.manage(favorites_cache_state)
|
||||||
.manage(tray_settings_state)
|
.manage(tray_settings_state)
|
||||||
|
.manage(legal_settings_state)
|
||||||
.manage(updates_state)
|
.manage(updates_state)
|
||||||
.manage(musicbrainz_state)
|
.manage(musicbrainz_state)
|
||||||
.manage(listenbrainz_state)
|
.manage(listenbrainz_state)
|
||||||
@@ -841,6 +849,10 @@ pub fn run() {
|
|||||||
config::tray_settings::set_enable_tray,
|
config::tray_settings::set_enable_tray,
|
||||||
config::tray_settings::set_minimize_to_tray,
|
config::tray_settings::set_minimize_to_tray,
|
||||||
config::tray_settings::set_close_to_tray,
|
config::tray_settings::set_close_to_tray,
|
||||||
|
// Legal settings commands
|
||||||
|
config::legal_settings::get_legal_settings,
|
||||||
|
config::legal_settings::get_qobuz_tos_accepted,
|
||||||
|
config::legal_settings::set_qobuz_tos_accepted,
|
||||||
// MusicBrainz integration commands
|
// MusicBrainz integration commands
|
||||||
commands::musicbrainz_resolve_track,
|
commands::musicbrainz_resolve_track,
|
||||||
commands::musicbrainz_resolve_artist,
|
commands::musicbrainz_resolve_artist,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import TitleBar from '../TitleBar.svelte';
|
import TitleBar from '../TitleBar.svelte';
|
||||||
import { t } from '$lib/i18n';
|
import { t } from '$lib/i18n';
|
||||||
import { setManualOffline } from '$lib/stores/offlineStore';
|
import { setManualOffline } from '$lib/stores/offlineStore';
|
||||||
import { qobuzTosAccepted } from '$lib/stores/qobuzLegalStore';
|
import { qobuzTosAccepted, loadTosAcceptance } from '$lib/stores/qobuzLegalStore';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
|
|
||||||
interface UserInfo {
|
interface UserInfo {
|
||||||
@@ -83,6 +83,10 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load ToS acceptance from Rust (persisted, survives app updates)
|
||||||
|
initStatus = 'Loading preferences...';
|
||||||
|
await loadTosAcceptance();
|
||||||
|
|
||||||
// Check for saved credentials and auto-login
|
// Check for saved credentials and auto-login
|
||||||
initStatus = 'Checking saved credentials...';
|
initStatus = 'Checking saved credentials...';
|
||||||
const hasSavedCreds = await invoke<boolean>('has_saved_credentials');
|
const hasSavedCreds = await invoke<boolean>('has_saved_credentials');
|
||||||
|
|||||||
@@ -1,18 +1,79 @@
|
|||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { writable } from 'svelte/store';
|
import { writable, get } from 'svelte/store';
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
|
||||||
const STORAGE_KEY = 'qbz-qobuz-tos-accepted';
|
// Store for ToS acceptance - persisted in Rust (survives app updates)
|
||||||
|
export const qobuzTosAccepted = writable<boolean>(false);
|
||||||
|
|
||||||
function readInitial(): boolean {
|
// Flag to track if we've loaded from Rust
|
||||||
|
let initialized = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load ToS acceptance state from Rust backend.
|
||||||
|
* Should be called early in app initialization.
|
||||||
|
*/
|
||||||
|
export async function loadTosAcceptance(): Promise<boolean> {
|
||||||
if (!browser) return false;
|
if (!browser) return false;
|
||||||
return localStorage.getItem(STORAGE_KEY) === 'true';
|
|
||||||
|
try {
|
||||||
|
const accepted = await invoke<boolean>('get_qobuz_tos_accepted');
|
||||||
|
qobuzTosAccepted.set(accepted);
|
||||||
|
initialized = true;
|
||||||
|
return accepted;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load ToS acceptance from backend:', err);
|
||||||
|
// Fallback to localStorage for migration from old versions
|
||||||
|
const localValue = localStorage.getItem('qbz-qobuz-tos-accepted') === 'true';
|
||||||
|
if (localValue) {
|
||||||
|
// Migrate to Rust storage
|
||||||
|
try {
|
||||||
|
await invoke('set_qobuz_tos_accepted', { accepted: true });
|
||||||
|
qobuzTosAccepted.set(true);
|
||||||
|
localStorage.removeItem('qbz-qobuz-tos-accepted');
|
||||||
|
console.log('Migrated ToS acceptance to Rust storage');
|
||||||
|
return true;
|
||||||
|
} catch (migrateErr) {
|
||||||
|
console.error('Failed to migrate ToS acceptance:', migrateErr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
initialized = true;
|
||||||
|
return localValue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const qobuzTosAccepted = writable<boolean>(readInitial());
|
/**
|
||||||
|
* Set ToS acceptance state in Rust backend.
|
||||||
|
*/
|
||||||
|
export async function setTosAcceptance(accepted: boolean): Promise<void> {
|
||||||
|
if (!browser) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await invoke('set_qobuz_tos_accepted', { accepted });
|
||||||
|
qobuzTosAccepted.set(accepted);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to save ToS acceptance:', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to changes and persist to Rust.
|
||||||
|
* Only persists if value actually changed and we're initialized.
|
||||||
|
*/
|
||||||
if (browser) {
|
if (browser) {
|
||||||
|
let lastValue: boolean | null = null;
|
||||||
|
|
||||||
qobuzTosAccepted.subscribe((value) => {
|
qobuzTosAccepted.subscribe((value) => {
|
||||||
localStorage.setItem(STORAGE_KEY, String(value));
|
// Skip if not initialized or value hasn't changed
|
||||||
|
if (!initialized || lastValue === value) {
|
||||||
|
lastValue = value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastValue = value;
|
||||||
|
|
||||||
|
// Persist to Rust (fire and forget, errors logged in setTosAcceptance)
|
||||||
|
invoke('set_qobuz_tos_accepted', { accepted: value }).catch((err) => {
|
||||||
|
console.error('Failed to persist ToS acceptance:', err);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Reference in new issue
Block a user