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:
vicrodh committed 2026-01-30 14:49:39 -06:00
1 parent fe7816f6c6
commit 3926209b0d
5 files changed
+212 -8

No files matched your search

+117
View File
@@ -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)
}
+10
View File
@@ -16,6 +16,7 @@ pub mod favorites_preferences;
pub mod favorites_cache;
pub mod subscription_state;
pub mod tray_settings;
pub mod legal_settings;
pub use audio_settings::{
AudioSettings,
@@ -83,3 +84,12 @@ pub use favorites_cache::{
sync_cached_favorite_artists,
clear_favorites_cache,
};
pub use legal_settings::{
LegalSettings,
LegalSettingsState,
LegalSettingsStore,
get_legal_settings,
get_qobuz_tos_accepted,
set_qobuz_tos_accepted,
};
+12
View File
@@ -196,6 +196,13 @@ pub fn run() {
// Initialize tray settings state
let tray_settings_state = config::tray_settings::TraySettingsState::new()
.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
let updates_state = updates::UpdatesState::new()
.expect("Failed to initialize updates state");
@@ -442,6 +449,7 @@ pub fn run() {
.manage(favorites_prefs_state)
.manage(favorites_cache_state)
.manage(tray_settings_state)
.manage(legal_settings_state)
.manage(updates_state)
.manage(musicbrainz_state)
.manage(listenbrainz_state)
@@ -841,6 +849,10 @@ pub fn run() {
config::tray_settings::set_enable_tray,
config::tray_settings::set_minimize_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
commands::musicbrainz_resolve_track,
commands::musicbrainz_resolve_artist,
+5 -1
View File
@@ -3,7 +3,7 @@
import TitleBar from '../TitleBar.svelte';
import { t } from '$lib/i18n';
import { setManualOffline } from '$lib/stores/offlineStore';
import { qobuzTosAccepted } from '$lib/stores/qobuzLegalStore';
import { qobuzTosAccepted, loadTosAcceptance } from '$lib/stores/qobuzLegalStore';
import { get } from 'svelte/store';
interface UserInfo {
@@ -83,6 +83,10 @@
return;
}
// Load ToS acceptance from Rust (persisted, survives app updates)
initStatus = 'Loading preferences...';
await loadTosAcceptance();
// Check for saved credentials and auto-login
initStatus = 'Checking saved credentials...';
const hasSavedCreds = await invoke<boolean>('has_saved_credentials');
+68 -7
View File
@@ -1,18 +1,79 @@
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;
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) {
let lastValue: boolean | null = null;
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);
});
});
}