use futures::stream::StreamExt; use mongodb::{ bson::{doc, oid::ObjectId, DateTime}, Collection, }; use serde::{Deserialize, Serialize}; /// A profile (a "subject of care" — a person or pet whose health data is /// tracked). One account owns zero or more profiles. All health data /// (medications, appointments, health stats) is scoped to a profile and /// encrypted under that profile's DEK. /// /// Zero-knowledge layers (both opaque to the server): /// - `name` / `wrapped_profile_dek` are AES-256-GCM ciphertext blobs the /// server stores verbatim and cannot read. /// - `name` is the profile's display name encrypted under the profile DEK. /// - `wrapped_profile_dek` is the profile DEK encrypted under the owner's /// account DEK. The client unwraps it at login/unlock (after unwrapping /// the account DEK) to get the profile DEK. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Profile { #[serde(rename = "_id", skip_serializing_if = "Option::is_none")] pub id: Option, #[serde(rename = "profileId")] pub profile_id: String, /// Owner account id (Mongo ObjectId hex). Replaces the 1:1 `user_id` /// semantics from the pre-A2 single-profile model. #[serde(rename = "ownerAccountId")] pub owner_account_id: String, /// Kept populated = owner_account_id for backward compat with existing /// medication/appointment filters that key on `userId`. Same value; do /// not rely on it for ownership — use `owner_account_id`. #[serde(rename = "userId")] pub user_id: String, #[serde(rename = "familyId")] pub family_id: Option, /// Display name — opaque, encrypted under the profile DEK. #[serde(rename = "name")] pub name: String, #[serde(rename = "nameIv")] pub name_iv: String, #[serde(rename = "nameAuthTag")] pub name_auth_tag: String, /// "human" | "pet" — what kind of subject this profile tracks. #[serde(rename = "kind", default = "default_kind")] pub kind: String, /// Freeform relationship to the owner: "self", "child", "spouse", /// "parent", "pet", ... #[serde(rename = "relationship", default)] pub relationship: String, #[serde(rename = "role", default = "default_role")] pub role: String, #[serde(rename = "permissions", default)] pub permissions: Vec, /// Profile DEK wrapped under the owner's account DEK (opaque ciphertext). #[serde(rename = "wrappedProfileDek")] pub wrapped_profile_dek: String, #[serde(rename = "wrappedProfileDekIv")] pub wrapped_profile_dek_iv: String, #[serde(rename = "createdAt")] pub created_at: DateTime, #[serde(rename = "updatedAt")] pub updated_at: DateTime, } fn default_kind() -> String { "human".to_string() } fn default_role() -> String { "patient".to_string() } pub struct ProfileRepository { collection: Collection, } impl ProfileRepository { pub fn new(collection: Collection) -> Self { Self { collection } } pub async fn create(&self, profile: &Profile) -> mongodb::error::Result<()> { self.collection.insert_one(profile, None).await?; Ok(()) } /// Look up a profile by its application-level profile id. pub async fn find_by_profile_id( &self, profile_id: &str, ) -> mongodb::error::Result> { self.collection .find_one(doc! { "profileId": profile_id }, None) .await } /// Look up a profile by id AND owner — used for ownership-scoped access. /// Returns None if the profile doesn't exist or doesn't belong to `owner`. pub async fn find_by_profile_id_owned( &self, profile_id: &str, owner: &str, ) -> mongodb::error::Result> { self.collection .find_one( doc! { "profileId": profile_id, "ownerAccountId": owner }, None, ) .await } /// All profiles owned by an account. pub async fn find_all_by_owner(&self, owner: &str) -> mongodb::error::Result> { let mut cursor = self .collection .find(doc! { "ownerAccountId": owner }, None) .await?; let mut out = Vec::new(); while let Some(p) = cursor.next().await { out.push(p?); } Ok(out) } /// Replace a profile's mutable fields (display name blob, kind, /// relationship), keyed by (profile_id, owner). Returns the updated doc /// or None if the profile doesn't exist / isn't owned by `owner`. pub async fn update_profile( &self, profile_id: &str, owner: &str, name_data: &str, name_iv: &str, name_auth_tag: &str, kind: &str, relationship: &str, ) -> mongodb::error::Result> { self.collection .find_one_and_update( doc! { "profileId": profile_id, "ownerAccountId": owner }, doc! { "$set": { "name": name_data, "nameIv": name_iv, "nameAuthTag": name_auth_tag, "kind": kind, "relationship": relationship, "updatedAt": DateTime::now() }}, None, ) .await } /// Delete a profile keyed by (profile_id, owner). Returns true if a doc /// was deleted, false if it didn't exist or wasn't owned by `owner`. pub async fn delete_profile( &self, profile_id: &str, owner: &str, ) -> mongodb::error::Result { let res = self .collection .delete_one( doc! { "profileId": profile_id, "ownerAccountId": owner }, None, ) .await?; Ok(res.deleted_count > 0) } }