use models::users::user_ssh_keys::SshKeyType; use models::users::user_ssh_keys; use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, QueryOrder, Set}; use serde::{Deserialize, Serialize}; use ssh_key::{Algorithm, EcdsaCurve, HashAlg}; use time::OffsetDateTime; use uuid::Uuid; use super::AuthService; use crate::errors::AuthError; #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct SshKeyDto { pub id: Uuid, pub name: String, pub key_type: SshKeyType, pub fingerprint: String, pub created_at: OffsetDateTime, } #[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)] pub struct AddSshKeyCmd { pub name: String, pub content: String, } fn key_type_of(algorithm: &Algorithm) -> Option { match algorithm { Algorithm::Rsa { .. } => Some(SshKeyType::SshRsa), Algorithm::Ed25519 => Some(SshKeyType::SshEd25519), Algorithm::Ecdsa { curve } if *curve == EcdsaCurve::NistP256 => { Some(SshKeyType::EcdsaSha2Nistp256) } _ => None, } } impl AuthService { pub async fn add_ssh_key( &self, uid: Uuid, cmd: AddSshKeyCmd, ) -> Result { let name = cmd.name.trim().to_string(); if name.is_empty() || name.len() > 64 { return Err(AuthError::BadRequest( "name must be between 1 and 64 characters".to_string(), )); } let parsed = ssh_key::PublicKey::from_openssh(cmd.content.trim()) .map_err(|_| AuthError::BadRequest("invalid ssh public key".to_string()))?; let Some(key_type) = key_type_of(&parsed.algorithm()) else { return Err(AuthError::BadRequest( "unsupported ssh key type".to_string(), )); }; let fingerprint = parsed.fingerprint(HashAlg::Sha256).to_string(); let db = self.db(); let taken = user_ssh_keys::Entity::find() .filter(user_ssh_keys::Column::Fingerprint.eq(&fingerprint)) .one(db.write()) .await?; if taken.is_some() { return Err(AuthError::Conflict( "ssh key fingerprint already registered".to_string(), )); } let now = OffsetDateTime::now_utc(); let row = user_ssh_keys::ActiveModel { id: Set(Uuid::now_v7()), user_id: Set(uid), key_type: Set(key_type), fingerprint: Set(fingerprint.clone()), name: Set(name.clone()), content: Set(cmd.content.trim().to_string()), created_at: Set(now), updated_at: Set(now), last_used_at: Set(None), }; let inserted = row.insert(db.write()).await?; Ok(SshKeyDto { id: inserted.id, name, key_type: inserted.key_type, fingerprint, created_at: inserted.created_at, }) } pub async fn list_ssh_keys(&self, uid: Uuid) -> Result, AuthError> { let rows = user_ssh_keys::Entity::find() .filter(user_ssh_keys::Column::UserId.eq(uid)) .order_by_desc(user_ssh_keys::Column::CreatedAt) .all(self.db().read()) .await?; Ok(rows .into_iter() .map(|row| SshKeyDto { id: row.id, name: row.name, key_type: row.key_type, fingerprint: row.fingerprint, created_at: row.created_at, }) .collect()) } pub async fn delete_ssh_key(&self, uid: Uuid, key_id: Uuid) -> Result<(), AuthError> { let db = self.db(); let result = user_ssh_keys::Entity::delete_many() .filter(user_ssh_keys::Column::Id.eq(key_id)) .filter(user_ssh_keys::Column::UserId.eq(uid)) .exec(db.write()) .await?; if result.rows_affected == 0 { return Err(AuthError::NotFound); } Ok(()) } }