normogen/backend/src/db/init.rs
goose cf6fcd4ef2 fix(backend): refresh-token jti + code cleanup (#15,#28,#29,#30)
Refresh-token rotation bug (found via Solaria smoke test):
* RefreshClaims now carries a unique random jti (UUID v4). Without it, two
  refresh tokens issued in the same second (e.g. on rotation) were byte-identical
  — breaking rotation, colliding on the tokenHash unique index, and making a
  stolen old token indistinguishable from the new one. Every refresh token is
  now unique regardless of issue time. Added a unit test asserting consecutive
  tokens differ.

Code cleanup (review items):
* #15: removed unused deps tower_governor, slog, thiserror (zero refs in src/).
* #30: deleted leftover src/main.rs.restore (a stale git-error-message backup).
* #28: removed the 9 single-line-comment stub files under src/db/ (appointment,
  family, health_data, lab_result, medication, profile, permission, share, user)
  and their mod declarations; nothing referenced them, real logic lives in
  mongodb_impl.rs/init.rs.
* #29: stripped 61 broad module-level #![allow(...)] suppressions across src/.
  Fixed the surfaced warnings instead: 4 unused 'claims' extractor bindings ->
  _claims; removed dead OpenFDAService.client/base_url fields + the never-called
  query_drug_events method + unused HashMap/reqwest imports; applied clippy's
  mechanical fixes (Ok(?) -> ?, Copy ObjectId clone, redundant closures,
  useless conversions); rewrote 'if let Ok(_) = x' -> 'if x.is_ok()'. Result:
  cargo build + clippy --all-targets are warning-free with no blanket allows.

Verified: fmt clean, build clean, clippy 0 warnings, 19 unit tests pass
(was 18; +1 jti-uniqueness test).
2026-06-27 20:57:24 -03:00

156 lines
5.9 KiB
Rust

use mongodb::{bson::doc, options::IndexOptions, Collection, IndexModel};
use anyhow::Result;
/// Creates required collections and indexes. Best-effort by design: index
/// creation failures are logged as warnings and do not abort startup, matching
/// the existing swallow-on-warning style. Safe to run repeatedly (idempotent).
pub struct DatabaseInitializer {
db: mongodb::Database,
}
impl DatabaseInitializer {
pub fn new(db: mongodb::Database) -> Self {
Self { db }
}
pub async fn initialize(&self) -> Result<()> {
println!("[MongoDB] Initializing database collections and indexes...");
// Create users collection and index
{
let collection: Collection<mongodb::bson::Document> = self.db.collection("users");
// Create email index using the builder pattern
let index = IndexModel::builder()
.keys(doc! { "email": 1 })
.options(IndexOptions::builder().unique(true).build())
.build();
match collection.create_index(index, None).await {
Ok(_) => println!("✓ Created index on users.email"),
Err(e) => println!("Warning: Failed to create index on users.email: {}", e),
}
}
// Create families collection and indexes
{
let collection: Collection<mongodb::bson::Document> = self.db.collection("families");
let index1 = IndexModel::builder().keys(doc! { "userId": 1 }).build();
let index2 = IndexModel::builder().keys(doc! { "familyId": 1 }).build();
match collection.create_index(index1, None).await {
Ok(_) => println!("✓ Created index on families.userId"),
Err(e) => println!("Warning: Failed to create index on families.userId: {}", e),
}
match collection.create_index(index2, None).await {
Ok(_) => println!("✓ Created index on families.familyId"),
Err(e) => println!(
"Warning: Failed to create index on families.familyId: {}",
e
),
}
}
// Create profiles collection and index
{
let collection: Collection<mongodb::bson::Document> = self.db.collection("profiles");
let index = IndexModel::builder().keys(doc! { "familyId": 1 }).build();
match collection.create_index(index, None).await {
Ok(_) => println!("✓ Created index on profiles.familyId"),
Err(e) => println!(
"Warning: Failed to create index on profiles.familyId: {}",
e
),
}
}
// Create health_data collection
{
let _collection: Collection<mongodb::bson::Document> =
self.db.collection("health_data");
println!("✓ Created health_data collection");
}
// Create lab_results collection
{
let _collection: Collection<mongodb::bson::Document> =
self.db.collection("lab_results");
println!("✓ Created lab_results collection");
}
// Create medications collection
{
let _collection: Collection<mongodb::bson::Document> =
self.db.collection("medications");
println!("✓ Created medications collection");
}
// Create appointments collection
{
let _collection: Collection<mongodb::bson::Document> =
self.db.collection("appointments");
println!("✓ Created appointments collection");
}
// Create shares collection and index
{
let collection: Collection<mongodb::bson::Document> = self.db.collection("shares");
let index = IndexModel::builder().keys(doc! { "familyId": 1 }).build();
match collection.create_index(index, None).await {
Ok(_) => println!("✓ Created index on shares.familyId"),
Err(e) => println!("Warning: Failed to create index on shares.familyId: {}", e),
}
}
// Create refresh_tokens collection and indexes.
// - unique index on tokenHash (the only thing we ever look tokens up by)
// - TTL index on expiresAt so expired tokens are auto-deleted by Mongo
{
let collection: Collection<mongodb::bson::Document> =
self.db.collection("refresh_tokens");
let unique_index = IndexModel::builder()
.keys(doc! { "tokenHash": 1 })
.options(IndexOptions::builder().unique(true).build())
.build();
// TTL: documents are deleted once their `expiresAt` timestamp passes.
// With expireAfterSeconds=0, Mongo removes each doc exactly when the
// datetime stored in `expiresAt` is reached.
let ttl_index = IndexModel::builder()
.keys(doc! { "expiresAt": 1 })
.options(
IndexOptions::builder()
.expire_after(std::time::Duration::from_secs(0))
.build(),
)
.build();
match collection.create_index(unique_index, None).await {
Ok(_) => println!("✓ Created unique index on refresh_tokens.tokenHash"),
Err(e) => println!(
"Warning: Failed to create index on refresh_tokens.tokenHash: {}",
e
),
}
match collection.create_index(ttl_index, None).await {
Ok(_) => println!("✓ Created TTL index on refresh_tokens.expiresAt"),
Err(e) => println!(
"Warning: Failed to create TTL index on refresh_tokens.expiresAt: {}",
e
),
}
}
println!("[MongoDB] Database initialization complete");
Ok(())
}
}