feat(backend): Phase 2.5 permission and share models
This commit is contained in:
parent
3eeef6d9c8
commit
eb0e2cc4b5
7 changed files with 265 additions and 106 deletions
|
|
@ -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"] }
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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: }
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
33
backend/src/models/permission.rs
Normal file
33
backend/src/models/permission.rs
Normal 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>;
|
||||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue