Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
### Logins
- New `allow_empty_passwords` feature flag to allow storing logins with empty passwords. This feature is intended to be enabled on desktop during the migration.

### Breach Alerts
- New component: `breach-alerts` for storing and retrieving breach alert dismissals by breach ID.

[Full Changelog](In progress)

# v150.0 (_2026-03-23_)
Expand Down
15 changes: 15 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ members = [
"components/ads-client/integration-tests",
"components/as-ohttp-client",
"components/autofill",
"components/breach-alerts",
"components/context_id",
"components/crashtest",
"components/example",
Expand Down Expand Up @@ -109,6 +110,7 @@ default-members = [
"components/ads-client",
"components/as-ohttp-client",
"components/autofill",
"components/breach-alerts",
"components/context_id",
"components/crashtest",
"components/fxa-client",
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ by the [uniffi](https://github.com/mozilla/uniffi-rs/) library.
* [ads-client](/components/ads-client) - for fetching ads via UAPI
* [autofill](components/autofill) - for storage and syncing of credit card and
address information
* [breach-alerts](components/breach-alerts) - for breach alert interaction data
* [crashtest](components/crashtest) - testing-purposes (crashing the Rust code)
* [fxa-client](components/fxa-client) - for applications that need to sign in
with FxA, access encryption keys for sync, and more.
Expand Down
20 changes: 20 additions & 0 deletions components/breach-alerts/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "breach-alerts"
edition = "2021"
version = "0.1.0"
authors = ["sync-team@mozilla.com"]
license = "MPL-2.0"

[dependencies]
uniffi = { version = "0.31" }
error-support = { path = "../support/error" }
interrupt-support = { path = "../support/interrupt" }
parking_lot = ">=0.11,<=0.12"
rusqlite = { version = "0.37.0", features = ["functions", "bundled", "unlock_notify"] }
sql-support = { path = "../support/sql" }
thiserror = "2"
url = { version = "2.1" }

[dev-dependencies]
error-support = { path = "../support/error", features = ["testing"] }
tempfile = "3"
10 changes: 10 additions & 0 deletions components/breach-alerts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# breach-alerts

Stores and retrieves breach alert dismissals by breach ID.

## API

- `get_breach_alert_dismissals(breach_ids)` — returns matching dismissals
- `set_breach_alert_dismissals(dismissals)` — upserts dismissals
- `clear_breach_alert_dismissals(breach_ids)` — deletes specific dismissals
- `clear_all_breach_alert_dismissals()` — deletes all dismissals
14 changes: 14 additions & 0 deletions components/breach-alerts/sql/create_schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- This Source Code Form is subject to the terms of the Mozilla Public
-- License, v. 2.0. If a copy of the MPL was not distributed with this
-- file, You can obtain one at http://mozilla.org/MPL/2.0/.

CREATE TABLE IF NOT EXISTS breach_alert_dismissals (
breach_id TEXT NOT NULL PRIMARY KEY,
dismissed_at INTEGER NOT NULL
);

-- This table holds key-value metadata.
CREATE TABLE IF NOT EXISTS meta (
key TEXT PRIMARY KEY,
value NOT NULL
) WITHOUT ROWID;
210 changes: 210 additions & 0 deletions components/breach-alerts/src/api.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use crate::error::*;
use rusqlite::Connection;
use sql_support::ConnExt;

#[derive(Debug, Clone, PartialEq, uniffi::Record)]
pub struct BreachAlertDismissal {
pub breach_id: String,
pub dismissed_at: i64,
}

pub fn get_breach_alert_dismissals(
conn: &Connection,
breach_ids: &[String],
) -> Result<Vec<BreachAlertDismissal>> {
if breach_ids.is_empty() {
return Ok(vec![]);
}
let sql = format!(
"SELECT breach_id, dismissed_at FROM breach_alert_dismissals WHERE breach_id IN ({})",
sql_support::repeat_sql_vars(breach_ids.len())
);
let mut stmt = conn.conn().prepare(&sql)?;
let rows = stmt.query_map(rusqlite::params_from_iter(breach_ids), |row| {
Ok(BreachAlertDismissal {
breach_id: row.get(0)?,
dismissed_at: row.get(1)?,
})
})?;
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Error::from)
}

pub fn set_breach_alert_dismissals(
conn: &Connection,
dismissals: &[BreachAlertDismissal],
) -> Result<()> {
if dismissals.is_empty() {
return Ok(());
}
let sql = format!(
"INSERT OR REPLACE INTO breach_alert_dismissals (breach_id, dismissed_at) VALUES {}",
sql_support::repeat_display(dismissals.len(), ",", |_, f| write!(f, "(?,?)"))
);
let params: Vec<&dyn rusqlite::types::ToSql> = dismissals
.iter()
.flat_map(|d| -> [&dyn rusqlite::types::ToSql; 2] { [&d.breach_id, &d.dismissed_at] })
.collect();
conn.conn().execute(&sql, params.as_slice())?;
Ok(())
}

pub fn clear_breach_alert_dismissals(conn: &Connection, breach_ids: &[String]) -> Result<()> {
if breach_ids.is_empty() {
return Ok(());
}
let sql = format!(
"DELETE FROM breach_alert_dismissals WHERE breach_id IN ({})",
sql_support::repeat_sql_vars(breach_ids.len())
);
conn.conn()
.execute(&sql, rusqlite::params_from_iter(breach_ids))?;
Ok(())
}

pub fn clear_all_breach_alert_dismissals(conn: &Connection) -> Result<()> {
conn.conn()
.execute("DELETE FROM breach_alert_dismissals", [])?;
Ok(())
}

#[cfg(test)]
mod tests {
use super::*;
use crate::db::test::new_mem_db;

#[test]
fn test_set_and_get() -> Result<()> {
let db = new_mem_db();
let conn = db.get_connection()?;

let dismissals = vec![
BreachAlertDismissal {
breach_id: "breach-1".into(),
dismissed_at: 1000,
},
BreachAlertDismissal {
breach_id: "breach-2".into(),
dismissed_at: 2000,
},
];
set_breach_alert_dismissals(conn, &dismissals)?;

let result = get_breach_alert_dismissals(
conn,
&["breach-1".into(), "breach-2".into(), "breach-3".into()],
)?;
assert_eq!(result.len(), 2);
assert!(result.contains(&BreachAlertDismissal {
breach_id: "breach-1".into(),
dismissed_at: 1000
}));
assert!(result.contains(&BreachAlertDismissal {
breach_id: "breach-2".into(),
dismissed_at: 2000
}));
Ok(())
}

#[test]
fn test_set_updates_existing() -> Result<()> {
let db = new_mem_db();
let conn = db.get_connection()?;

set_breach_alert_dismissals(
conn,
&[BreachAlertDismissal {
breach_id: "breach-1".into(),
dismissed_at: 1000,
}],
)?;
set_breach_alert_dismissals(
conn,
&[BreachAlertDismissal {
breach_id: "breach-1".into(),
dismissed_at: 2000,
}],
)?;

let result = get_breach_alert_dismissals(conn, &["breach-1".into()])?;
assert_eq!(
result,
vec![BreachAlertDismissal {
breach_id: "breach-1".into(),
dismissed_at: 2000
}]
);
Ok(())
}

#[test]
fn test_clear_specific() -> Result<()> {
let db = new_mem_db();
let conn = db.get_connection()?;

set_breach_alert_dismissals(
conn,
&[
BreachAlertDismissal {
breach_id: "breach-1".into(),
dismissed_at: 1000,
},
BreachAlertDismissal {
breach_id: "breach-2".into(),
dismissed_at: 2000,
},
],
)?;
clear_breach_alert_dismissals(conn, &["breach-1".into()])?;

let result = get_breach_alert_dismissals(conn, &["breach-1".into(), "breach-2".into()])?;
assert_eq!(
result,
vec![BreachAlertDismissal {
breach_id: "breach-2".into(),
dismissed_at: 2000
}]
);
Ok(())
}

#[test]
fn test_clear_all() -> Result<()> {
let db = new_mem_db();
let conn = db.get_connection()?;

set_breach_alert_dismissals(
conn,
&[
BreachAlertDismissal {
breach_id: "breach-1".into(),
dismissed_at: 1000,
},
BreachAlertDismissal {
breach_id: "breach-2".into(),
dismissed_at: 2000,
},
],
)?;
clear_all_breach_alert_dismissals(conn)?;

let result = get_breach_alert_dismissals(conn, &["breach-1".into(), "breach-2".into()])?;
assert!(result.is_empty());
Ok(())
}

#[test]
fn test_empty_inputs() -> Result<()> {
let db = new_mem_db();
let conn = db.get_connection()?;

assert_eq!(get_breach_alert_dismissals(conn, &[])?, vec![]);
set_breach_alert_dismissals(conn, &[])?;
clear_breach_alert_dismissals(conn, &[])?;
Ok(())
}
}
Loading
Loading