Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions lib/features/account/screens/account_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:mostro/core/app_routes.dart';
import 'package:mostro/core/app_theme.dart';
import 'package:mostro/core/services/identity_service.dart';
import 'package:mostro/features/account/providers/backup_reminder_provider.dart';
import 'package:mostro/features/trades/providers/trades_providers.dart';
import 'package:mostro/features/account/providers/privacy_mode_provider.dart';
import 'package:mostro/features/account/widgets/backup_trigger_sheet.dart';
import 'package:mostro/l10n/app_localizations.dart';
Expand Down Expand Up @@ -386,6 +387,12 @@ class _AccountScreenState extends ConsumerState<AccountScreen> {
// where the user is left without a valid identity.
await IdentityService.regenerate();
ref.read(sessionProvider.notifier).clearSession();
// The new identity starts with an empty trade DB (the Rust
// side clears trades, messages and sessions on regenerate,
// issue #273); drop the cached list so My Trades reflects
// the clean slate immediately instead of showing the
// previous identity's orders until the next refresh.
ref.invalidate(rawTradesProvider);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
await ref
.read(backupReminderProvider.notifier)
.showBackupReminder();
Expand Down
20 changes: 20 additions & 0 deletions rust/src/api/identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,26 @@ pub async fn delete_identity() -> Result<()> {
if let Err(e) = db.clear_trade_keys().await {
log::warn!("[identity] failed to clear trade key mappings: {e}");
}
// Messages before trades: messages.trade_id is an FK onto trades(id).
// Both belong to the deleted identity — their trade keys were just
// cleared, so the rows are dead state a new identity must not inherit
// (privacy: a fresh user must not see the previous one's My Trades or
// chats). See issue #273.
if let Err(e) = db.clear_messages().await {
log::warn!("[identity] failed to clear messages: {e}");
}
if let Err(e) = db.clear_trades().await {
log::warn!("[identity] failed to clear trades: {e}");
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

// Empty the in-memory sessions too: they key decryption for the deleted
// identity's trades and must not carry into the new one.
let dropped = crate::mostro::session::session_manager().clear_all().await;
if dropped > 0 {
log::debug!(
"[identity] cleared {dropped} in-memory session(s) on identity deletion"
);
}

// Last, so the cleanup warnings above are dropped too: buffered lines name
Expand Down
8 changes: 8 additions & 0 deletions rust/src/db/indexeddb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,14 @@ impl Storage for IndexedDbStorage {
Ok(()) // IndexedDB not yet implemented (#233)
}

async fn clear_trades(&self) -> Result<()> {
Ok(()) // IndexedDB not yet implemented (#233)
}

async fn clear_messages(&self) -> Result<()> {
Ok(()) // IndexedDB not yet implemented (#233)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// ── Settings KV — fully implemented (chat cursor + preferences, #246) ───

async fn get_setting(&self, key: &str) -> Result<Option<String>> {
Expand Down
10 changes: 10 additions & 0 deletions rust/src/db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,16 @@ pub trait Storage: Send + Sync {
/// order→index mappings belong to the removed identity's derivation tree.
async fn clear_trade_keys(&self) -> Result<()>;

/// Delete ALL trade rows. Used on identity deletion: the recovered trade
/// history belongs to the removed identity, and its trade keys are cleared
/// alongside, so the rows are dead state a new identity must not inherit.
async fn clear_trades(&self) -> Result<()>;

/// Delete ALL chat message rows. Used on identity deletion: the peer and
/// admin conversations belong to the removed identity and must not leak
/// into the fresh one.
async fn clear_messages(&self) -> Result<()>;

// ── Settings / Mostro node ────────────────────────────────────────────────

/// Read a value from the generic key-value settings store, or `None` when
Expand Down
65 changes: 65 additions & 0 deletions rust/src/db/sqlite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,20 @@ impl Storage for SqliteStorage {
Ok(())
}

async fn clear_trades(&self) -> Result<()> {
sqlx::query("DELETE FROM trades")
.execute(&self.pool)
.await?;
Ok(())
}

async fn clear_messages(&self) -> Result<()> {
sqlx::query("DELETE FROM messages")
.execute(&self.pool)
.await?;
Ok(())
}

async fn get_setting(&self, key: &str) -> Result<Option<String>> {
let row: Option<(String,)> =
sqlx::query_as("SELECT value FROM settings WHERE key = ?")
Expand Down Expand Up @@ -1064,4 +1078,55 @@ mod tests {
drop(storage);
let _ = std::fs::remove_file(&path);
}

#[tokio::test]
async fn clear_trades_and_messages_empty_both_tables() {
// Identity deletion must wipe the previous user's trade history and
// chats (privacy, issue #273), not just the keys. Insert one trade and
// one message, clear both tables, and confirm nothing survives.
let path = temp_db_path();
let storage = SqliteStorage::open(path.to_str().unwrap()).await.unwrap();
sqlx::query("INSERT INTO trades VALUES ('t1', '{}', 'Active', 1, NULL)")
.execute(&storage.pool)
.await
.unwrap();
sqlx::query(
"INSERT INTO messages (id, trade_id, data, is_read, created_at) \
VALUES ('m1', 't1', '{}', 0, 1)",
)
.execute(&storage.pool)
.await
.unwrap();

// Precondition: one row in each table.
let trades: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM trades")
.fetch_one(&storage.pool)
.await
.unwrap();
assert_eq!(trades.0, 1);
assert!(storage.message_exists("m1").await.unwrap());

storage.clear_messages().await.unwrap();
storage.clear_trades().await.unwrap();

// Both tables are empty afterwards.
let trades: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM trades")
.fetch_one(&storage.pool)
.await
.unwrap();
let messages: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM messages")
.fetch_one(&storage.pool)
.await
.unwrap();
assert_eq!(trades.0, 0);
assert_eq!(messages.0, 0);
assert!(!storage.message_exists("m1").await.unwrap());

// Clearing again on empty tables is a no-op, not an error.
storage.clear_messages().await.unwrap();
storage.clear_trades().await.unwrap();

drop(storage);
let _ = std::fs::remove_file(&path);
}
}
10 changes: 10 additions & 0 deletions rust/src/mostro/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,16 @@ impl SessionManager {
});
before - sessions.len()
}

/// Drop every in-memory session. Used on identity deletion: the sessions
/// belong to the removed identity's trades, and a new identity must not
/// inherit them. Returns the number of sessions dropped.
pub async fn clear_all(&self) -> usize {
let mut sessions = self.sessions.write().await;
let dropped = sessions.len();
sessions.clear();
dropped
}
}

// ── Global singleton ────────────────────────────────────────────────────────
Expand Down
Loading