use crate::AppService; use crate::error::AppError; use base64::Engine; use base64::engine::general_purpose; use chrono::Utc; use models::users::{user_activity_log, user_ssh_key}; use sea_orm::*; use serde::{Deserialize, Serialize}; use session::Session; use sha2::{Digest, Sha256}; use uuid::Uuid; #[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)] pub struct AddSshKeyParams { pub title: String, pub public_key: String, } #[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)] pub struct UpdateSshKeyParams { pub title: Option, pub expires_at: Option>, } #[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)] pub struct SshKeyResponse { pub id: i64, pub user_uid: Uuid, pub title: String, pub fingerprint: String, pub key_type: String, pub key_bits: Option, pub is_verified: bool, pub last_used_at: Option>, pub expires_at: Option>, pub is_revoked: bool, pub created_at: chrono::DateTime, pub updated_at: chrono::DateTime, } #[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)] pub struct SshKeyListResponse { pub keys: Vec, pub total: usize, } #[derive(Debug)] pub struct ParsedSshKey { pub key_type: String, pub key_data: Vec, pub key_bits: Option, pub comment: Option, } impl AppService { pub async fn user_add_ssh_key( &self, context: &Session, params: AddSshKeyParams, ) -> Result { let user_uid = context.user().ok_or(AppError::Unauthorized)?; let public_key = params.public_key.trim().to_string(); let parsed = self.user_parse_ssh_public_key(&public_key)?; let fingerprint = self.user_generate_ssh_fingerprint(&parsed.key_data)?; let existing: Option = user_ssh_key::Entity::find() .filter(user_ssh_key::Column::Fingerprint.eq(&fingerprint)) .one(&self.db) .await?; if existing.is_some() { return Err(AppError::BadRequest("SSH key already exists".to_string())); } let count = user_ssh_key::Entity::find() .filter(user_ssh_key::Column::User.eq(user_uid)) .filter(user_ssh_key::Column::IsRevoked.eq(false)) .count(&self.db) .await?; if count >= 50 { return Err(AppError::BadRequest("Too many SSH keys".to_string())); } let now = Utc::now(); let ssh_key_model = user_ssh_key::ActiveModel { user: Set(user_uid), title: Set(params.title.clone()), public_key: Set(public_key), fingerprint: Set(fingerprint), key_type: Set(parsed.key_type.clone()), key_bits: Set(parsed.key_bits), is_verified: Set(false), last_used_at: Set(None), expires_at: Set(None), is_revoked: Set(false), created_at: Set(now), updated_at: Set(now), ..Default::default() }; let created_key = ssh_key_model.insert(&self.db).await?; let _ = user_activity_log::ActiveModel { user_uid: Set(Some(user_uid)), action: Set("ssh_key_add".to_string()), ip_address: Set(context.ip_address()), user_agent: Set(context.user_agent()), details: Set(serde_json::json!({ "key_id": created_key.id, "key_title": params.title, "key_type": parsed.key_type, "fingerprint": created_key.fingerprint })), created_at: Set(now), ..Default::default() } .insert(&self.db) .await; Ok(self.user_model_to_response(created_key)) } pub async fn user_list_ssh_keys( &self, context: &Session, ) -> Result { let user_uid = context.user().ok_or(AppError::Unauthorized)?; let keys: Vec = user_ssh_key::Entity::find() .filter(user_ssh_key::Column::User.eq(user_uid)) .order_by_desc(user_ssh_key::Column::CreatedAt) .all(&self.db) .await?; let total = keys.len(); let keys = keys .into_iter() .map(|k| self.user_model_to_response(k)) .collect(); Ok(SshKeyListResponse { keys, total }) } pub async fn user_get_ssh_key( &self, context: &Session, key_id: i64, ) -> Result { let user_uid = context.user().ok_or(AppError::Unauthorized)?; let key: user_ssh_key::Model = user_ssh_key::Entity::find_by_id(key_id) .filter(user_ssh_key::Column::User.eq(user_uid)) .one(&self.db) .await? .ok_or(AppError::NotFound("SSH key not found".to_string()))?; Ok(self.user_model_to_response(key)) } pub async fn user_update_ssh_key( &self, context: &Session, key_id: i64, params: UpdateSshKeyParams, ) -> Result { let user_uid = context.user().ok_or(AppError::Unauthorized)?; let key: user_ssh_key::Model = user_ssh_key::Entity::find_by_id(key_id) .filter(user_ssh_key::Column::User.eq(user_uid)) .one(&self.db) .await? .ok_or(AppError::NotFound("SSH key not found".to_string()))?; let mut active_key: user_ssh_key::ActiveModel = key.into(); let updated_title = params.title.clone(); let updated_expires_at = params.expires_at; if let Some(title) = params.title { active_key.title = Set(title); } if let Some(expires_at) = params.expires_at { active_key.expires_at = Set(Some(expires_at)); } active_key.updated_at = Set(Utc::now()); let updated_key = active_key.update(&self.db).await?; let _ = user_activity_log::ActiveModel { user_uid: Set(Some(user_uid)), action: Set("ssh_key_update".to_string()), ip_address: Set(context.ip_address()), user_agent: Set(context.user_agent()), details: Set(serde_json::json!({ "key_id": key_id, "updated_fields": { "title": updated_title, "expires_at": updated_expires_at } })), created_at: Set(Utc::now()), ..Default::default() } .insert(&self.db) .await; Ok(self.user_model_to_response(updated_key)) } pub async fn user_delete_ssh_key( &self, context: &Session, key_id: i64, ) -> Result<(), AppError> { let user_uid = context.user().ok_or(AppError::Unauthorized)?; let key: user_ssh_key::Model = user_ssh_key::Entity::find_by_id(key_id) .filter(user_ssh_key::Column::User.eq(user_uid)) .one(&self.db) .await? .ok_or(AppError::NotFound("SSH key not found".to_string()))?; let _ = user_activity_log::ActiveModel { user_uid: Set(Some(user_uid)), action: Set("ssh_key_delete".to_string()), ip_address: Set(context.ip_address()), user_agent: Set(context.user_agent()), details: Set(serde_json::json!({ "key_id": key_id, "key_title": key.title.clone(), "fingerprint": key.fingerprint.clone() })), created_at: Set(Utc::now()), ..Default::default() } .insert(&self.db) .await; user_ssh_key::Entity::delete(key.into_active_model()) .exec(&self.db) .await?; Ok(()) } pub async fn user_revoke_ssh_key( &self, context: &Session, key_id: i64, ) -> Result<(), AppError> { let user_uid = context.user().ok_or(AppError::Unauthorized)?; let key: user_ssh_key::Model = user_ssh_key::Entity::find_by_id(key_id) .filter(user_ssh_key::Column::User.eq(user_uid)) .one(&self.db) .await? .ok_or(AppError::NotFound("SSH key not found".to_string()))?; let mut active_key: user_ssh_key::ActiveModel = key.clone().into(); active_key.is_revoked = Set(true); active_key.updated_at = Set(Utc::now()); active_key.update(&self.db).await?; let _ = user_activity_log::ActiveModel { user_uid: Set(Some(user_uid)), action: Set("ssh_key_revoke".to_string()), ip_address: Set(context.ip_address()), user_agent: Set(context.user_agent()), details: Set(serde_json::json!({ "key_id": key_id, "key_title": key.title, "fingerprint": key.fingerprint })), created_at: Set(Utc::now()), ..Default::default() } .insert(&self.db) .await; Ok(()) } pub async fn user_verify_ssh_key_by_fingerprint( &self, fingerprint: &str, ) -> Result { let key: user_ssh_key::Model = user_ssh_key::Entity::find() .filter(user_ssh_key::Column::Fingerprint.eq(fingerprint)) .filter(user_ssh_key::Column::IsRevoked.eq(false)) .one(&self.db) .await? .ok_or(AppError::NotFound("SSH key not found".to_string()))?; if let Some(expires_at) = key.expires_at { let now = Utc::now(); if now > expires_at { return Err(AppError::Unauthorized); // AppError::SshKeyExpired replaced with generic Unauthorized } } Ok(key) } pub async fn user_touch_ssh_key_last_used(&self, key_id: i64) -> Result<(), AppError> { let key: user_ssh_key::Model = user_ssh_key::Entity::find_by_id(key_id) .one(&self.db) .await? .ok_or(AppError::NotFound("SSH key not found".to_string()))?; let mut active_key: user_ssh_key::ActiveModel = key.into(); active_key.last_used_at = Set(Some(Utc::now())); active_key.updated_at = Set(Utc::now()); active_key.update(&self.db).await?; Ok(()) } fn user_parse_ssh_public_key(&self, public_key: &str) -> Result { let parts: Vec<&str> = public_key.split_whitespace().collect(); if parts.len() < 2 { return Err(AppError::BadRequest("Invalid SSH key format".to_string())); } let key_type = parts[0]; let key_data_base64 = parts[1]; let comment: Option = parts.get(2).map(|s| (*s).to_string()); let normalized_key_type = match key_type { "ssh-rsa" => "rsa", "ssh-ed25519" => "ed25519", "ecdsa-sha2-nistp256" | "ecdsa-sha2-nistp384" | "ecdsa-sha2-nistp521" => "ecdsa", "ssh-dss" => "dsa", _ => return Err(AppError::BadRequest("Unsupported SSH key type".to_string())), }; let key_data = general_purpose::STANDARD .decode(key_data_base64) .map_err(|_| AppError::BadRequest("Invalid SSH key format".to_string()))?; let key_bits = if normalized_key_type == "rsa" { self.user_calculate_rsa_key_bits(&key_data) } else { None }; Ok(ParsedSshKey { key_type: normalized_key_type.to_string(), key_data, key_bits, comment, }) } fn user_generate_ssh_fingerprint(&self, key_data: &[u8]) -> Result { let mut hasher = Sha256::new(); hasher.update(key_data); let hash = hasher.finalize(); let fingerprint = format!("SHA256:{}", general_purpose::STANDARD_NO_PAD.encode(&hash)); Ok(fingerprint) } fn user_calculate_rsa_key_bits(&self, key_data: &[u8]) -> Option { if key_data.len() < 256 { Some(1024) } else if key_data.len() < 512 { Some(2048) } else if key_data.len() < 1024 { Some(4096) } else { Some(8192) } } fn user_model_to_response(&self, model: user_ssh_key::Model) -> SshKeyResponse { SshKeyResponse { id: model.id, user_uid: model.user, title: model.title, fingerprint: model.fingerprint, key_type: model.key_type, key_bits: model.key_bits, is_verified: model.is_verified, last_used_at: model.last_used_at, expires_at: model.expires_at, is_revoked: model.is_revoked, created_at: model.created_at, updated_at: model.updated_at, } } }