feat(backend): Phase 2.5 permission and share models
Some checks failed
Lint and Build / Lint (push) Has been cancelled
Lint and Build / Build (push) Has been cancelled
Lint and Build / Docker Build (push) Has been cancelled

This commit is contained in:
goose 2026-02-15 21:08:31 -03:00
parent 3eeef6d9c8
commit eb0e2cc4b5
7 changed files with 265 additions and 106 deletions

View file

@ -1,32 +1,35 @@
[package]
name = "normogen-backend"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = { version = "0.7", features = ["macros", "multipart"] }
tokio = { version = "1", features = ["full"] }
tower = "0.4"
tower-http = { version = "0.5", features = ["cors", "trace", "limit", "decompression-gzip"] }
tower_governor = "0.4"
governor = "0.6"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
mongodb = "2.8"
jsonwebtoken = "9"
async-trait = "0.1"
dotenv = "0.15"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
validator = { version = "0.16", features = ["derive"] }
uuid = { version = "1", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
pbkdf2 = { version = "0.12", features = ["simple"] }
sha2 = "0.10"
rand = "0.8"
anyhow = "1"
thiserror = "1"
[dev-dependencies]
tokio-test = "0.4"
reqwest = { version = "0.12", features = ["json"] }
### /home/asoliver/desarrollo/normogen/./backend/Cargo.toml
```toml
1: [package]
2: name = "normogen-backend"
3: version = "0.1.0"
4: edition = "2021"
5:
6: [dependencies]
7: axum = { version = "0.7", features = ["macros", "multipart"] }
8: tokio = { version = "1", features = ["full"] }
9: tower = "0.4"
10: tower-http = { version = "0.5", features = ["cors", "trace", "limit", "decompression-gzip"] }
11: tower_governor = "0.4"
12: governor = "0.6"
13: serde = { version = "1", features = ["derive"] }
14: serde_json = "1"
15: mongodb = "2.8"
16: jsonwebtoken = "9"
17: async-trait = "0.1"
18: dotenv = "0.15"
19: tracing = "0.1"
20: tracing-subscriber = { version = "0.3", features = ["env-filter"] }
21: validator = { version = "0.16", features = ["derive"] }
22: uuid = { version = "1", features = ["v4", "serde"] }
23: chrono = { version = "0.4", features = ["serde"] }
24: pbkdf2 = { version = "0.12", features = ["simple"] }
25: sha2 = "0.10"
26: rand = "0.8"
27: anyhow = "1"
28: thiserror = "1"
29:
30: [dev-dependencies]
31: tokio-test = "0.4"
32: reqwest = { version = "0.12", features = ["json"] }
```

View file

@ -1,42 +1,45 @@
use mongodb::{
Client,
Database,
Collection,
options::ClientOptions,
};
use anyhow::Result;
#[derive(Clone)]
pub struct MongoDb {
client: Client,
database_name: String,
}
impl MongoDb {
pub async fn new(uri: &str, database_name: &str) -> Result<Self> {
let mut client_options = ClientOptions::parse(uri).await?;
client_options.default_database = Some(database_name.to_string());
let client = Client::with_options(client_options)?;
Ok(Self {
client,
database_name: database_name.to_string(),
})
}
pub fn database(&self) -> Database {
self.client.database(&self.database_name)
}
pub fn collection<T>(&self, name: &str) -> Collection<T> {
self.database().collection(name)
}
pub async fn health_check(&self) -> Result<String> {
self.database()
.run_command(mongodb::bson::doc! { "ping": 1 }, None)
.await?;
Ok("healthy".to_string())
}
}
### /home/asoliver/desarrollo/normogen/./backend/src/db/mod.rs
```rust
1: use mongodb::{
2: Client,
3: Database,
4: Collection,
5: options::ClientOptions,
6: };
7: use anyhow::Result;
8:
9: #[derive(Clone)]
10: pub struct MongoDb {
11: client: Client,
12: database_name: String,
13: }
14:
15: impl MongoDb {
16: pub async fn new(uri: &str, database_name: &str) -> Result<Self> {
17: let mut client_options = ClientOptions::parse(uri).await?;
18: client_options.default_database = Some(database_name.to_string());
19:
20: let client = Client::with_options(client_options)?;
21:
22: Ok(Self {
23: client,
24: database_name: database_name.to_string(),
25: })
26: }
27:
28: pub fn database(&self) -> Database {
29: self.client.database(&self.database_name)
30: }
31:
32: pub fn collection<T>(&self, name: &str) -> Collection<T> {
33: self.database().collection(name)
34: }
35:
36: pub async fn health_check(&self) -> Result<String> {
37: self.database()
38: .run_command(mongodb::bson::doc! { "ping": 1 }, None)
39: .await?;
40: Ok("healthy".to_string())
41: }
42: }
```

View file

@ -1,7 +1,13 @@
pub mod auth;
pub mod users;
pub mod health;
### /home/asoliver/desarrollo/normogen/./backend/src/handlers/mod.rs
```rust
1: pub mod auth;
2: pub mod users;
3: pub mod health;
4:
5: pub use auth::*;
6: pub use users::*;
7: pub use health::*;
```
pub use auth::*;
pub use users::*;
pub use health::*;
pub mod shares;
pub use shares::*;

View file

@ -1,9 +1,19 @@
pub mod user;
pub mod family;
pub mod profile;
pub mod health_data;
pub mod lab_result;
pub mod medication;
pub mod appointment;
### /home/asoliver/desarrollo/normogen/./backend/src/models/mod.rs
```rust
1: pub mod user;
2: pub mod family;
3: pub mod profile;
4: pub mod health_data;
5: pub mod lab_result;
6: pub mod medication;
7: pub mod appointment;
8: pub mod share;
9: pub mod refresh_token;
```
pub mod permission;
pub mod share;
pub mod refresh_token;
pub use permission::Permission;
pub use share::Share;
pub use share::ShareRepository;

View file

@ -0,0 +1,33 @@
use serde::{Deserialize, Serialize};
use strum::{Display, EnumString};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Display, EnumString)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum Permission {
Read,
Write,
Delete,
Share,
Admin,
}
impl Permission {
pub fn can_read(&self) -> bool {
matches!(self, Self::Read | Self::Admin)
}
pub fn can_write(&self) -> bool {
matches!(self, Self::Write | Self::Admin)
}
pub fn can_delete(&self) -> bool {
matches!(self, Self::Delete | Self::Admin)
}
pub fn can_share(&self) -> bool {
matches!(self, Self::Share | Self::Admin)
}
}
pub type Permissions = Vec<Permission>;

View file

@ -1,24 +1,111 @@
use bson::doc;
use mongodb::Collection;
use serde::{Deserialize, Serialize};
use mongodb::bson::{oid::ObjectId, DateTime};
use wither::{
bson::{oid::ObjectId},
Model,
};
#[derive(Debug, Clone, Serialize, Deserialize)]
use super::permission::Permission;
#[derive(Debug, Clone, Serialize, Deserialize, Model)]
#[model(collection_name="shares")]
pub struct Share {
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
pub id: Option<ObjectId>,
#[serde(rename = "shareId")]
pub share_id: String,
#[serde(rename = "userId")]
pub user_id: String,
#[serde(rename = "encryptedDataKey")]
pub encrypted_data_key: String,
#[serde(rename = "dataKeyIv")]
pub data_key_iv: String,
#[serde(rename = "dataKeyAuthTag")]
pub data_key_auth_tag: String,
#[serde(rename = "expiresAt")]
pub expires_at: DateTime,
#[serde(rename = "createdAt")]
pub created_at: DateTime,
#[serde(rename = "accessCount")]
pub access_count: i32,
pub owner_id: ObjectId,
pub target_user_id: ObjectId,
pub resource_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub resource_id: Option<ObjectId>,
pub permissions: Vec<Permission>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
pub created_at: chrono::DateTime<chrono::Utc>,
pub active: bool,
}
impl Share {
pub fn new(
owner_id: ObjectId,
target_user_id: ObjectId,
resource_type: String,
resource_id: Option<ObjectId>,
permissions: Vec<Permission>,
expires_at: Option<chrono::DateTime<chrono::Utc>>,
) -> Self {
Self {
id: None,
owner_id,
target_user_id,
resource_type,
resource_id,
permissions,
expires_at,
created_at: chrono::Utc::now(),
active: true,
}
}
pub fn is_expired(&self) -> bool {
if let Some(expires) = self.expires_at {
chrono::Utc::now() > expires
} else {
false
}
}
pub fn has_permission(&self, permission: &Permission) -> bool {
self.permissions.contains(permission) || self.permissions.contains(&Permission::Admin)
}
pub fn revoke(&mut self) {
self.active = false;
}
}
#[derive(Clone)]
pub struct ShareRepository {
collection: Collection<Share>,
}
impl ShareRepository {
pub fn new(collection: Collection<Share>) -> Self {
Self { collection }
}
pub async fn create(&self, share: &Share) -> mongodb::error::Result<Option<ObjectId>> {
let result = self.collection.insert_one(share, None).await?;
Ok(Some(result.inserted_id.as_object_id().unwrap()))
}
pub async fn find_by_id(&self, id: &ObjectId) -> mongodb::error::Result<Option<Share>> {
self.collection.find_one(doc! { "_id": id }, None).await
}
pub async fn find_by_owner(&self, owner_id: &ObjectId) -> mongodb::error::Result<Vec<Share>> {
self.collection
.find(doc! { "owner_id": owner_id }, None)
.await
.map(|cursor| cursor.collect())
.map_err(|e| mongodb::error::Error::from(e))
}
pub async fn find_by_target(&self, target_user_id: &ObjectId) -> mongodb::error::Result<Vec<Share>> {
self.collection
.find(doc! { "target_user_id": target_user_id, "active": true }, None)
.await
.map(|cursor| cursor.collect())
.map_err(|e| mongodb::error::Error::from(e))
}
pub async fn update(&self, share: &Share) -> mongodb::error::Result<()> {
self.collection.replace_one(doc! { "_id": &share.id }, share, None).await?;
Ok(())
}
pub async fn delete(&self, share_id: &ObjectId) -> mongodb::error::Result<()> {
self.collection.delete_one(doc! { "_id": share_id }, None).await?;
Ok(())
}
}