Skip to main content

saorsa_core/projects/
mod.rs

1// Copyright 2024 Saorsa Labs Limited
2//
3// This software is dual-licensed under:
4// - GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later)
5// - Commercial License
6//
7// For AGPL-3.0 license, see LICENSE-AGPL-3.0
8// For commercial licensing, contact: david@saorsalabs.com
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under these licenses is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
14//! Projects system with hierarchical organization structure
15//!
16//! Features:
17//! - Hierarchical structure: Organizations → Departments → Teams → Projects
18//! - Document management with version control and threshold signatures
19//! - Media storage for videos, audio, and images
20//! - Granular permissions using threshold groups
21//! - Approval workflows with multi-signature requirements
22//! - Activity tracking and analytics
23
24use crate::identity::enhanced::{DepartmentId, EnhancedIdentity, OrganizationId, TeamId};
25use crate::quantum_crypto::types::GroupId;
26use crate::storage::{FileChunker, FileMetadata, StorageManager, keys, ttl};
27use crate::threshold::ThresholdSignature;
28use blake3::Hasher;
29use serde::{Deserialize, Serialize};
30use std::collections::HashMap;
31use std::time::SystemTime;
32use thiserror::Error;
33use uuid::Uuid;
34
35/// Comprehensive error types for project operations
36///
37/// Covers all possible failure modes in project management including
38/// storage failures, permission denials, and workflow violations.
39#[derive(Debug, Error)]
40pub enum ProjectsError {
41    /// Underlying storage system error
42    #[error("Storage error: {0}")]
43    StorageError(#[from] crate::storage::StorageError),
44
45    /// Project with specified ID does not exist
46    #[error("Project not found: {0}")]
47    ProjectNotFound(String),
48
49    /// Document with specified ID does not exist
50    #[error("Document not found: {0}")]
51    DocumentNotFound(String),
52
53    /// User lacks required permissions for operation
54    #[error("Permission denied: {0}")]
55    PermissionDenied(String),
56
57    /// Operation is not valid in current context
58    #[error("Invalid operation: {0}")]
59    InvalidOperation(String),
60
61    /// Workflow validation or execution error
62    #[error("Workflow error: {0}")]
63    WorkflowError(String),
64}
65
66/// Result type for project operations
67type Result<T> = std::result::Result<T, ProjectsError>;
68
69/// Unique identifier for projects in the system
70///
71/// Uses UUID v4 to ensure global uniqueness across all organizations
72/// and prevent ID collision in distributed environments.
73#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
74pub struct ProjectId(pub String);
75
76impl Default for ProjectId {
77    fn default() -> Self {
78        Self::new()
79    }
80}
81
82impl ProjectId {
83    /// Generate a new unique project identifier
84    ///
85    /// # Returns
86    /// A new ProjectId with a randomly generated UUID
87    pub fn new() -> Self {
88        Self(Uuid::new_v4().to_string())
89    }
90}
91
92/// Unique identifier for documents within projects
93///
94/// Documents are the primary content units in projects and can represent
95/// text files, code, specifications, or any other structured content.
96#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
97pub struct DocumentId(pub String);
98
99impl Default for DocumentId {
100    fn default() -> Self {
101        Self::new()
102    }
103}
104
105impl DocumentId {
106    /// Generate a new unique document identifier
107    ///
108    /// # Returns
109    /// A new DocumentId with a randomly generated UUID
110    pub fn new() -> Self {
111        Self(Uuid::new_v4().to_string())
112    }
113}
114
115/// Unique identifier for folders in project hierarchies
116///
117/// Folders organize documents and other folders in a hierarchical
118/// structure, supporting nested organization of project content.
119#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
120pub struct FolderId(pub String);
121
122impl Default for FolderId {
123    fn default() -> Self {
124        Self::new()
125    }
126}
127
128impl FolderId {
129    /// Generate a new unique folder identifier
130    ///
131    /// # Returns
132    /// A new FolderId with a randomly generated UUID
133    pub fn new() -> Self {
134        Self(Uuid::new_v4().to_string())
135    }
136}
137
138/// User identifier type for project member references
139///
140/// References users who participate in projects with various roles
141/// and permission levels.
142pub type UserId = String;
143
144/// Blake3 cryptographic hash for content integrity verification
145///
146/// Used for document versioning, deduplication, and integrity checks.
147/// Blake3 provides fast, secure hashing with excellent performance.
148pub type Blake3Hash = [u8; 32];
149
150/// Complete project structure with hierarchical organization
151///
152/// Projects are the primary organizational unit for collaborative work.
153/// They belong to organizations and can be assigned to departments and teams.
154/// Access control is managed through threshold groups and permissions.
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct Project {
157    /// Unique identifier for this project
158    pub id: ProjectId,
159    /// Human-readable project name
160    pub name: String,
161    /// Detailed description of project purpose and scope
162    pub description: String,
163    /// Organization this project belongs to
164    pub organization_id: OrganizationId,
165    /// Optional department assignment within organization
166    pub department_id: Option<DepartmentId>,
167    /// Optional team assignment within department
168    pub team_id: Option<TeamId>,
169    /// Threshold group that owns this project
170    pub owner_group: GroupId,
171    /// Additional access groups with specific permissions
172    pub access_groups: Vec<AccessGroup>,
173    /// Root folder containing all project content
174    pub root_folder: FolderId,
175    /// Project configuration and behavior settings
176    pub settings: ProjectSettings,
177    /// Metadata for analytics and tracking
178    pub metadata: ProjectMetadata,
179    /// Timestamp when project was created
180    pub created_at: SystemTime,
181    /// User ID of project creator
182    pub created_by: UserId,
183}
184
185/// Access group with permissions
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct AccessGroup {
188    pub group_id: GroupId,
189    pub permissions: Vec<ProjectPermission>,
190    pub name: String,
191}
192
193/// Project-specific permissions
194#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
195pub enum ProjectPermission {
196    Read,
197    Write,
198    Delete,
199    Share,
200    ManageMembers,
201    ManageWorkflows,
202    ApproveDocuments,
203    ViewAnalytics,
204}
205
206/// Project settings
207#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct ProjectSettings {
209    pub require_approval: bool,
210    pub approval_threshold: u16,
211    pub version_control: bool,
212    pub max_file_size_mb: u64,
213    pub allowed_file_types: Vec<String>,
214    pub retention_days: Option<u32>,
215    pub enable_watermarks: bool,
216    pub enable_analytics: bool,
217}
218
219impl Default for ProjectSettings {
220    fn default() -> Self {
221        Self {
222            require_approval: false,
223            approval_threshold: 1,
224            version_control: true,
225            max_file_size_mb: 1024, // 1GB
226            allowed_file_types: vec![],
227            retention_days: None,
228            enable_watermarks: false,
229            enable_analytics: true,
230        }
231    }
232}
233
234/// Project metadata
235#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct ProjectMetadata {
237    pub total_documents: u64,
238    pub total_size_bytes: u64,
239    pub last_activity: SystemTime,
240    pub active_users: u32,
241    pub custom_fields: HashMap<String, String>,
242}
243
244/// Folder structure
245#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct Folder {
247    pub id: FolderId,
248    pub name: String,
249    pub parent_id: Option<FolderId>,
250    pub project_id: ProjectId,
251    pub subfolders: Vec<FolderId>,
252    pub documents: Vec<DocumentId>,
253    pub created_at: SystemTime,
254    pub created_by: UserId,
255}
256
257/// Document with version control
258#[derive(Debug, Clone, Serialize, Deserialize)]
259pub struct Document {
260    pub id: DocumentId,
261    pub name: String,
262    pub description: String,
263    pub folder_id: FolderId,
264    pub project_id: ProjectId,
265    pub document_type: DocumentType,
266    pub current_version: DocumentVersion,
267    pub versions: Vec<DocumentVersion>,
268    pub access_log: Vec<AccessLogEntry>,
269    pub workflow_state: Option<WorkflowState>,
270    pub tags: Vec<String>,
271    pub created_at: SystemTime,
272    pub created_by: UserId,
273}
274
275/// Document type
276#[derive(Debug, Clone, Serialize, Deserialize)]
277pub enum DocumentType {
278    Text { format: String },
279    Spreadsheet,
280    Presentation,
281    Image { width: u32, height: u32 },
282    Video { duration_seconds: u64 },
283    Audio { duration_seconds: u64 },
284    PDF { pages: u32 },
285    Archive,
286    Other { mime_type: String },
287}
288
289/// Document version
290#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct DocumentVersion {
292    pub version_number: u64,
293    pub content_hash: Blake3Hash,
294    pub encryption_key: EncryptedKey,
295    pub size_bytes: u64,
296    pub author: UserId,
297    pub signatures: Vec<VersionSignature>,
298    pub comment: String,
299    pub created_at: SystemTime,
300    pub is_approved: bool,
301}
302
303/// Encrypted key (encrypted with project key)
304#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct EncryptedKey {
306    pub ciphertext: Vec<u8>,
307    pub nonce: Vec<u8>,
308}
309
310/// Version signature with threshold crypto
311#[derive(Debug, Clone, Serialize, Deserialize)]
312pub struct VersionSignature {
313    pub signer_id: UserId,
314    pub signature: ThresholdSignature,
315    pub signed_at: SystemTime,
316    pub comment: Option<String>,
317}
318
319/// Access log entry
320#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct AccessLogEntry {
322    pub user_id: UserId,
323    pub action: AccessAction,
324    pub timestamp: SystemTime,
325    pub ip_address: Option<String>,
326    pub device_id: Option<String>,
327}
328
329/// Access action
330#[derive(Debug, Clone, Serialize, Deserialize)]
331pub enum AccessAction {
332    View,
333    Download,
334    Edit,
335    Share,
336    Delete,
337    Approve,
338    Reject,
339}
340
341/// Workflow state for approvals
342#[derive(Debug, Clone, Serialize, Deserialize)]
343pub struct WorkflowState {
344    pub workflow_id: String,
345    pub current_stage: WorkflowStage,
346    pub approvers: Vec<UserId>,
347    pub approvals: Vec<Approval>,
348    pub required_approvals: u16,
349    pub deadline: Option<SystemTime>,
350    pub created_at: SystemTime,
351}
352
353/// Workflow stage
354#[derive(Debug, Clone, Serialize, Deserialize)]
355pub enum WorkflowStage {
356    Draft,
357    UnderReview,
358    Approved,
359    Rejected,
360    Published,
361}
362
363/// Approval record
364#[derive(Debug, Clone, Serialize, Deserialize)]
365pub struct Approval {
366    pub approver_id: UserId,
367    pub decision: ApprovalDecision,
368    pub signature: ThresholdSignature,
369    pub comment: Option<String>,
370    pub approved_at: SystemTime,
371}
372
373/// Approval decision
374#[derive(Debug, Clone, Serialize, Deserialize)]
375pub enum ApprovalDecision {
376    Approve,
377    Reject,
378    RequestChanges,
379}
380
381/// Project analytics
382#[derive(Debug, Clone, Serialize, Deserialize, Default)]
383pub struct ProjectAnalytics {
384    pub total_views: u64,
385    pub total_downloads: u64,
386    pub unique_viewers: u64,
387    pub average_time_spent_seconds: u64,
388    pub most_active_users: Vec<(UserId, u64)>,
389    pub popular_documents: Vec<(DocumentId, u64)>,
390    pub storage_trend: Vec<(SystemTime, u64)>,
391    pub activity_heatmap: HashMap<u32, u64>, // hour of day -> activity count
392}
393
394/// Projects manager
395pub struct ProjectsManager {
396    storage: StorageManager,
397    identity: EnhancedIdentity,
398    file_chunker: FileChunker,
399}
400
401impl ProjectsManager {
402    /// Create new projects manager
403    pub fn new(storage: StorageManager, identity: EnhancedIdentity) -> Self {
404        Self {
405            storage,
406            identity,
407            file_chunker: FileChunker::new(1024 * 1024), // 1MB chunks
408        }
409    }
410
411    /// Create a new project
412    pub async fn create_project(
413        &mut self,
414        name: String,
415        description: String,
416        organization_id: OrganizationId,
417        department_id: Option<DepartmentId>,
418        team_id: Option<TeamId>,
419        owner_group: GroupId,
420    ) -> Result<Project> {
421        // Create root folder
422        let root_folder = Folder {
423            id: FolderId::new(),
424            name: "Root".to_string(),
425            parent_id: None,
426            project_id: ProjectId::new(), // Will be updated
427            subfolders: vec![],
428            documents: vec![],
429            created_at: SystemTime::now(),
430            created_by: self.identity.base_identity.user_id.clone(),
431        };
432
433        let project = Project {
434            id: ProjectId::new(),
435            name,
436            description,
437            organization_id,
438            department_id,
439            team_id,
440            owner_group,
441            access_groups: vec![],
442            root_folder: root_folder.id.clone(),
443            settings: ProjectSettings::default(),
444            metadata: ProjectMetadata {
445                total_documents: 0,
446                total_size_bytes: 0,
447                last_activity: SystemTime::now(),
448                active_users: 1,
449                custom_fields: HashMap::new(),
450            },
451            created_at: SystemTime::now(),
452            created_by: self.identity.base_identity.user_id.clone(),
453        };
454
455        // Store project
456        let key = keys::project(&project.id.0);
457        self.storage
458            .store_encrypted(&key, &project, ttl::PROFILE, None)
459            .await?;
460
461        // Store root folder
462        let folder_key = format!("project:folder:{}", root_folder.id.0);
463        self.storage
464            .store_encrypted(&folder_key, &root_folder, ttl::PROFILE, None)
465            .await?;
466
467        Ok(project)
468    }
469
470    /// Upload a document
471    pub async fn upload_document(
472        &mut self,
473        project_id: ProjectId,
474        folder_id: FolderId,
475        name: String,
476        description: String,
477        content: &[u8],
478        document_type: DocumentType,
479    ) -> Result<Document> {
480        // Verify project access
481        let project = self.get_project(&project_id).await?;
482        self.check_project_permission(&project, ProjectPermission::Write)?;
483
484        // Check file size
485        let size_mb = content.len() / (1024 * 1024);
486        if size_mb as u64 > project.settings.max_file_size_mb {
487            return Err(ProjectsError::InvalidOperation(format!(
488                "File too large: {}MB (max: {}MB).into()",
489                size_mb, project.settings.max_file_size_mb
490            )));
491        }
492
493        // Calculate content hash
494        let mut hasher = Hasher::new();
495        hasher.update(content);
496        let content_hash = hasher.finalize().into();
497
498        // Generate encryption key for this document
499        let doc_key = rand::random::<[u8; 32]>();
500
501        // Encrypt content with document key
502        let encrypted_content = self.encrypt_content(content, &doc_key)?;
503
504        // Encrypt document key with project key (simplified)
505        let encrypted_key = EncryptedKey {
506            ciphertext: doc_key.to_vec(), // In practice, properly encrypt this
507            nonce: vec![0; 12],
508        };
509
510        // Create document
511        let document = Document {
512            id: DocumentId::new(),
513            name,
514            description,
515            folder_id,
516            project_id: project_id.clone(),
517            document_type,
518            current_version: DocumentVersion {
519                version_number: 1,
520                content_hash,
521                encryption_key: encrypted_key.clone(),
522                size_bytes: content.len() as u64,
523                author: self.identity.base_identity.user_id.clone(),
524                signatures: vec![],
525                comment: "Initial upload".to_string(),
526                created_at: SystemTime::now(),
527                is_approved: !project.settings.require_approval,
528            },
529            versions: vec![],
530            access_log: vec![AccessLogEntry {
531                user_id: self.identity.base_identity.user_id.clone(),
532                action: AccessAction::Edit,
533                timestamp: SystemTime::now(),
534                ip_address: None,
535                device_id: None,
536            }],
537            workflow_state: if project.settings.require_approval {
538                Some(WorkflowState {
539                    workflow_id: Uuid::new_v4().to_string(),
540                    current_stage: WorkflowStage::Draft,
541                    approvers: vec![],
542                    approvals: vec![],
543                    required_approvals: project.settings.approval_threshold,
544                    deadline: None,
545                    created_at: SystemTime::now(),
546                })
547            } else {
548                None
549            },
550            tags: vec![],
551            created_at: SystemTime::now(),
552            created_by: self.identity.base_identity.user_id.clone(),
553        };
554
555        // Store document metadata
556        let doc_key = keys::document_meta(&document.id.0);
557        self.storage
558            .store_encrypted(&doc_key, &document, ttl::PROFILE, None)
559            .await?;
560
561        // Store document content using chunker
562        let file_metadata = FileMetadata {
563            file_id: document.id.0.clone(),
564            name: document.name.clone(),
565            size: content.len() as u64,
566            mime_type: match &document.document_type {
567                DocumentType::Other { mime_type } => mime_type.clone(),
568                _ => "application/octet-stream".to_string(),
569            },
570            hash: content_hash.to_vec(),
571            total_chunks: 0, // Will be set by chunker
572            created_at: SystemTime::now(),
573            created_by: self.identity.base_identity.user_id.clone(),
574        };
575
576        self.file_chunker
577            .store_file(
578                &mut self.storage,
579                &document.id.0,
580                &encrypted_content,
581                file_metadata,
582            )
583            .await?;
584
585        // Update project metadata
586        self.update_project_metadata(&project_id, 1, content.len() as i64)
587            .await?;
588
589        Ok(document)
590    }
591
592    /// Download a document
593    pub async fn download_document(&mut self, document_id: &DocumentId) -> Result<Vec<u8>> {
594        // Get document metadata
595        let document = self.get_document(document_id).await?;
596
597        // Check access
598        let project = self.get_project(&document.project_id).await?;
599        self.check_project_permission(&project, ProjectPermission::Read)?;
600
601        // Log access
602        self.log_document_access(document_id, AccessAction::Download)
603            .await?;
604
605        // Retrieve document content
606        let encrypted_content = self
607            .file_chunker
608            .get_file(&self.storage, &document_id.0)
609            .await?;
610
611        // Decrypt content (simplified - in practice, decrypt the doc key first)
612        let doc_key = &document.current_version.encryption_key.ciphertext;
613        let content = self.decrypt_content(&encrypted_content, doc_key)?;
614
615        Ok(content)
616    }
617
618    /// Create a new version of a document
619    pub async fn create_document_version(
620        &mut self,
621        document_id: &DocumentId,
622        content: &[u8],
623        comment: String,
624    ) -> Result<DocumentVersion> {
625        let mut document = self.get_document(document_id).await?;
626
627        // Check write permission
628        let project = self.get_project(&document.project_id).await?;
629        self.check_project_permission(&project, ProjectPermission::Write)?;
630
631        // Move current version to history
632        document.versions.push(document.current_version.clone());
633
634        // Create new version
635        let mut hasher = Hasher::new();
636        hasher.update(content);
637        let content_hash = hasher.finalize().into();
638
639        let new_version = DocumentVersion {
640            version_number: document.current_version.version_number + 1,
641            content_hash,
642            encryption_key: document.current_version.encryption_key.clone(), // Reuse key
643            size_bytes: content.len() as u64,
644            author: self.identity.base_identity.user_id.clone(),
645            signatures: vec![],
646            comment,
647            created_at: SystemTime::now(),
648            is_approved: !project.settings.require_approval,
649        };
650
651        document.current_version = new_version.clone();
652
653        // Update workflow state if needed
654        if project.settings.require_approval {
655            document.workflow_state = Some(WorkflowState {
656                workflow_id: Uuid::new_v4().to_string(),
657                current_stage: WorkflowStage::UnderReview,
658                approvers: vec![],
659                approvals: vec![],
660                required_approvals: project.settings.approval_threshold,
661                deadline: None,
662                created_at: SystemTime::now(),
663            });
664        }
665
666        // Store updated document
667        let doc_key = keys::document_meta(&document_id.0);
668        self.storage
669            .store_encrypted(&doc_key, &document, ttl::PROFILE, None)
670            .await?;
671
672        // Store new content
673        let encrypted_content =
674            self.encrypt_content(content, &document.current_version.encryption_key.ciphertext)?;
675        let file_metadata = FileMetadata {
676            file_id: format!("{}_v{}", document_id.0, new_version.version_number),
677            name: document.name.clone(),
678            size: content.len() as u64,
679            mime_type: "application/octet-stream".to_string(),
680            hash: content_hash.to_vec(),
681            total_chunks: 0,
682            created_at: SystemTime::now(),
683            created_by: self.identity.base_identity.user_id.clone(),
684        };
685
686        self.file_chunker
687            .store_file(
688                &mut self.storage,
689                &file_metadata.file_id,
690                &encrypted_content,
691                file_metadata.clone(),
692            )
693            .await?;
694
695        Ok(new_version)
696    }
697
698    /// Approve a document
699    pub async fn approve_document(
700        &mut self,
701        document_id: &DocumentId,
702        comment: Option<String>,
703    ) -> Result<()> {
704        let mut document = self.get_document(document_id).await?;
705
706        // Check approval permission
707        let project = self.get_project(&document.project_id).await?;
708        self.check_project_permission(&project, ProjectPermission::ApproveDocuments)?;
709
710        // Update workflow state
711        if let Some(ref mut workflow) = document.workflow_state {
712            // Add approval (simplified - would use threshold signature)
713            workflow.approvals.push(Approval {
714                approver_id: self.identity.base_identity.user_id.clone(),
715                decision: ApprovalDecision::Approve,
716                signature: vec![0; 64], // Placeholder
717                comment,
718                approved_at: SystemTime::now(),
719            });
720
721            // Check if enough approvals
722            if workflow.approvals.len() >= workflow.required_approvals as usize {
723                workflow.current_stage = WorkflowStage::Approved;
724                document.current_version.is_approved = true;
725            }
726        }
727
728        // Store updated document
729        let doc_key = keys::document_meta(&document_id.0);
730        self.storage
731            .store_encrypted(&doc_key, &document, ttl::PROFILE, None)
732            .await?;
733
734        Ok(())
735    }
736
737    /// Get project by ID
738    async fn get_project(&self, project_id: &ProjectId) -> Result<Project> {
739        let key = keys::project(&project_id.0);
740        self.storage
741            .get_encrypted(&key)
742            .await
743            .map_err(|_| ProjectsError::ProjectNotFound(project_id.0.clone()))
744    }
745
746    /// Get document by ID
747    async fn get_document(&self, document_id: &DocumentId) -> Result<Document> {
748        let key = keys::document_meta(&document_id.0);
749        self.storage
750            .get_encrypted(&key)
751            .await
752            .map_err(|_| ProjectsError::DocumentNotFound(document_id.0.clone()))
753    }
754
755    /// Check project permission
756    fn check_project_permission(
757        &self,
758        _project: &Project,
759        _permission: ProjectPermission,
760    ) -> Result<()> {
761        // Simplified permission check
762        // In practice, would check threshold groups and access groups
763        Ok(())
764    }
765
766    /// Log document access
767    async fn log_document_access(
768        &mut self,
769        document_id: &DocumentId,
770        action: AccessAction,
771    ) -> Result<()> {
772        let mut document = self.get_document(document_id).await?;
773
774        document.access_log.push(AccessLogEntry {
775            user_id: self.identity.base_identity.user_id.clone(),
776            action,
777            timestamp: SystemTime::now(),
778            ip_address: None,
779            device_id: None,
780        });
781
782        // Keep log size reasonable
783        if document.access_log.len() > 1000 {
784            document.access_log.drain(0..100);
785        }
786
787        let doc_key = keys::document_meta(&document_id.0);
788        self.storage
789            .store_encrypted(&doc_key, &document, ttl::PROFILE, None)
790            .await?;
791
792        Ok(())
793    }
794
795    /// Update project metadata
796    async fn update_project_metadata(
797        &mut self,
798        project_id: &ProjectId,
799        doc_delta: i64,
800        size_delta: i64,
801    ) -> Result<()> {
802        let mut project = self.get_project(project_id).await?;
803
804        project.metadata.total_documents =
805            (project.metadata.total_documents as i64 + doc_delta) as u64;
806        project.metadata.total_size_bytes =
807            (project.metadata.total_size_bytes as i64 + size_delta) as u64;
808        project.metadata.last_activity = SystemTime::now();
809
810        let key = keys::project(&project_id.0);
811        self.storage
812            .store_encrypted(&key, &project, ttl::PROFILE, None)
813            .await?;
814
815        Ok(())
816    }
817
818    /// Encrypt content using ChaCha20Poly1305 (saorsa-pqc)
819    fn encrypt_content(&self, content: &[u8], key: &[u8]) -> Result<Vec<u8>> {
820        use saorsa_pqc::{ChaCha20Poly1305Cipher, SymmetricKey};
821        // Ensure key is exactly 32 bytes
822        if key.len() != 32 {
823            return Err(ProjectsError::InvalidOperation(
824                "Invalid encryption key length - must be 32 bytes".to_string(),
825            ));
826        }
827        let mut k = [0u8; 32];
828        k.copy_from_slice(&key[..32]);
829        let sk = SymmetricKey::from_bytes(k);
830        let cipher = ChaCha20Poly1305Cipher::new(&sk);
831        let (ciphertext, nonce) = cipher
832            .encrypt(content, None)
833            .map_err(|e| ProjectsError::InvalidOperation(format!("Encryption failed: {e}")))?;
834        let mut out = Vec::with_capacity(nonce.len() + ciphertext.len());
835        out.extend_from_slice(&nonce);
836        out.extend_from_slice(&ciphertext);
837        Ok(out)
838    }
839
840    /// Decrypt content using ChaCha20Poly1305 (saorsa-pqc)
841    fn decrypt_content(&self, encrypted: &[u8], key: &[u8]) -> Result<Vec<u8>> {
842        use saorsa_pqc::{ChaCha20Poly1305Cipher, SymmetricKey};
843        // Ensure key is exactly 32 bytes
844        if key.len() != 32 {
845            return Err(ProjectsError::InvalidOperation(
846                "Invalid decryption key length - must be 32 bytes".to_string(),
847            ));
848        }
849
850        // Minimum size: need at least nonce length + 1 byte ciphertext
851        if encrypted.len() < 13 {
852            return Err(ProjectsError::InvalidOperation(
853                "Invalid encrypted data - too short".to_string(),
854            ));
855        }
856        // Extract nonce (first 12 bytes to match our encrypt usage)
857        let (nonce_slice, ciphertext) = encrypted.split_at(12);
858        let mut nonce = [0u8; 12];
859        nonce.copy_from_slice(nonce_slice);
860        let mut k = [0u8; 32];
861        k.copy_from_slice(&key[..32]);
862        let sk = SymmetricKey::from_bytes(k);
863        let cipher = ChaCha20Poly1305Cipher::new(&sk);
864        cipher
865            .decrypt(ciphertext, &nonce, None)
866            .map_err(|e| ProjectsError::InvalidOperation(format!("Decryption failed: {e}")))
867    }
868}