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
17
PHASE-2-5-STATUS.md
Normal file
17
PHASE-2-5-STATUS.md
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Phase 2.5: Access Control
|
||||||
|
|
||||||
|
## Status: Core Models Complete
|
||||||
|
|
||||||
|
### Implemented
|
||||||
|
- Permission system (Read, Write, Delete, Share, Admin)
|
||||||
|
- Share model with repository
|
||||||
|
- Database integration
|
||||||
|
|
||||||
|
### Files Created
|
||||||
|
- backend/src/models/permission.rs
|
||||||
|
- backend/src/models/share.rs
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
- Create share handlers
|
||||||
|
- Add routes to main.rs
|
||||||
|
- Test the API
|
||||||
|
|
@ -1,32 +1,35 @@
|
||||||
[package]
|
### /home/asoliver/desarrollo/normogen/./backend/Cargo.toml
|
||||||
name = "normogen-backend"
|
```toml
|
||||||
version = "0.1.0"
|
1: [package]
|
||||||
edition = "2021"
|
2: name = "normogen-backend"
|
||||||
|
3: version = "0.1.0"
|
||||||
[dependencies]
|
4: edition = "2021"
|
||||||
axum = { version = "0.7", features = ["macros", "multipart"] }
|
5:
|
||||||
tokio = { version = "1", features = ["full"] }
|
6: [dependencies]
|
||||||
tower = "0.4"
|
7: axum = { version = "0.7", features = ["macros", "multipart"] }
|
||||||
tower-http = { version = "0.5", features = ["cors", "trace", "limit", "decompression-gzip"] }
|
8: tokio = { version = "1", features = ["full"] }
|
||||||
tower_governor = "0.4"
|
9: tower = "0.4"
|
||||||
governor = "0.6"
|
10: tower-http = { version = "0.5", features = ["cors", "trace", "limit", "decompression-gzip"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
11: tower_governor = "0.4"
|
||||||
serde_json = "1"
|
12: governor = "0.6"
|
||||||
mongodb = "2.8"
|
13: serde = { version = "1", features = ["derive"] }
|
||||||
jsonwebtoken = "9"
|
14: serde_json = "1"
|
||||||
async-trait = "0.1"
|
15: mongodb = "2.8"
|
||||||
dotenv = "0.15"
|
16: jsonwebtoken = "9"
|
||||||
tracing = "0.1"
|
17: async-trait = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
18: dotenv = "0.15"
|
||||||
validator = { version = "0.16", features = ["derive"] }
|
19: tracing = "0.1"
|
||||||
uuid = { version = "1", features = ["v4", "serde"] }
|
20: tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
21: validator = { version = "0.16", features = ["derive"] }
|
||||||
pbkdf2 = { version = "0.12", features = ["simple"] }
|
22: uuid = { version = "1", features = ["v4", "serde"] }
|
||||||
sha2 = "0.10"
|
23: chrono = { version = "0.4", features = ["serde"] }
|
||||||
rand = "0.8"
|
24: pbkdf2 = { version = "0.12", features = ["simple"] }
|
||||||
anyhow = "1"
|
25: sha2 = "0.10"
|
||||||
thiserror = "1"
|
26: rand = "0.8"
|
||||||
|
27: anyhow = "1"
|
||||||
[dev-dependencies]
|
28: thiserror = "1"
|
||||||
tokio-test = "0.4"
|
29:
|
||||||
reqwest = { version = "0.12", features = ["json"] }
|
30: [dev-dependencies]
|
||||||
|
31: tokio-test = "0.4"
|
||||||
|
32: reqwest = { version = "0.12", features = ["json"] }
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,45 @@
|
||||||
use mongodb::{
|
### /home/asoliver/desarrollo/normogen/./backend/src/db/mod.rs
|
||||||
Client,
|
```rust
|
||||||
Database,
|
1: use mongodb::{
|
||||||
Collection,
|
2: Client,
|
||||||
options::ClientOptions,
|
3: Database,
|
||||||
};
|
4: Collection,
|
||||||
use anyhow::Result;
|
5: options::ClientOptions,
|
||||||
|
6: };
|
||||||
#[derive(Clone)]
|
7: use anyhow::Result;
|
||||||
pub struct MongoDb {
|
8:
|
||||||
client: Client,
|
9: #[derive(Clone)]
|
||||||
database_name: String,
|
10: pub struct MongoDb {
|
||||||
}
|
11: client: Client,
|
||||||
|
12: database_name: String,
|
||||||
impl MongoDb {
|
13: }
|
||||||
pub async fn new(uri: &str, database_name: &str) -> Result<Self> {
|
14:
|
||||||
let mut client_options = ClientOptions::parse(uri).await?;
|
15: impl MongoDb {
|
||||||
client_options.default_database = Some(database_name.to_string());
|
16: pub async fn new(uri: &str, database_name: &str) -> Result<Self> {
|
||||||
|
17: let mut client_options = ClientOptions::parse(uri).await?;
|
||||||
let client = Client::with_options(client_options)?;
|
18: client_options.default_database = Some(database_name.to_string());
|
||||||
|
19:
|
||||||
Ok(Self {
|
20: let client = Client::with_options(client_options)?;
|
||||||
client,
|
21:
|
||||||
database_name: database_name.to_string(),
|
22: Ok(Self {
|
||||||
})
|
23: client,
|
||||||
}
|
24: database_name: database_name.to_string(),
|
||||||
|
25: })
|
||||||
pub fn database(&self) -> Database {
|
26: }
|
||||||
self.client.database(&self.database_name)
|
27:
|
||||||
}
|
28: pub fn database(&self) -> Database {
|
||||||
|
29: self.client.database(&self.database_name)
|
||||||
pub fn collection<T>(&self, name: &str) -> Collection<T> {
|
30: }
|
||||||
self.database().collection(name)
|
31:
|
||||||
}
|
32: pub fn collection<T>(&self, name: &str) -> Collection<T> {
|
||||||
|
33: self.database().collection(name)
|
||||||
pub async fn health_check(&self) -> Result<String> {
|
34: }
|
||||||
self.database()
|
35:
|
||||||
.run_command(mongodb::bson::doc! { "ping": 1 }, None)
|
36: pub async fn health_check(&self) -> Result<String> {
|
||||||
.await?;
|
37: self.database()
|
||||||
Ok("healthy".to_string())
|
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;
|
### /home/asoliver/desarrollo/normogen/./backend/src/handlers/mod.rs
|
||||||
pub mod users;
|
```rust
|
||||||
pub mod health;
|
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 mod shares;
|
||||||
pub use users::*;
|
pub use shares::*;
|
||||||
pub use health::*;
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,19 @@
|
||||||
pub mod user;
|
### /home/asoliver/desarrollo/normogen/./backend/src/models/mod.rs
|
||||||
pub mod family;
|
```rust
|
||||||
pub mod profile;
|
1: pub mod user;
|
||||||
pub mod health_data;
|
2: pub mod family;
|
||||||
pub mod lab_result;
|
3: pub mod profile;
|
||||||
pub mod medication;
|
4: pub mod health_data;
|
||||||
pub mod appointment;
|
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 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 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 {
|
pub struct Share {
|
||||||
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
|
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
|
||||||
pub id: Option<ObjectId>,
|
pub id: Option<ObjectId>,
|
||||||
#[serde(rename = "shareId")]
|
pub owner_id: ObjectId,
|
||||||
pub share_id: String,
|
pub target_user_id: ObjectId,
|
||||||
#[serde(rename = "userId")]
|
pub resource_type: String,
|
||||||
pub user_id: String,
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
#[serde(rename = "encryptedDataKey")]
|
pub resource_id: Option<ObjectId>,
|
||||||
pub encrypted_data_key: String,
|
pub permissions: Vec<Permission>,
|
||||||
#[serde(rename = "dataKeyIv")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub data_key_iv: String,
|
pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
#[serde(rename = "dataKeyAuthTag")]
|
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||||
pub data_key_auth_tag: String,
|
pub active: bool,
|
||||||
#[serde(rename = "expiresAt")]
|
}
|
||||||
pub expires_at: DateTime,
|
|
||||||
#[serde(rename = "createdAt")]
|
impl Share {
|
||||||
pub created_at: DateTime,
|
pub fn new(
|
||||||
#[serde(rename = "accessCount")]
|
owner_id: ObjectId,
|
||||||
pub access_count: i32,
|
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