use db::conn::sea_orm::*; use db::repo::repo_deploy_keys as key_entity; use time::OffsetDateTime; use uuid::Uuid; use crate::error::{RepoError, RepoResult}; use crate::features::RepoService; use super::types::{CreateDeployKeyOption, DeployKeyPage, DeployKeyView, ListDeployKeyQuery}; fn require_admin(actor: Uuid) -> RepoResult<()> { if actor.is_nil() { return Err(RepoError::Unauthorized("authentication required".into())); } Ok(()) } async fn load_key(svc: &RepoService, repo_id: Uuid, key_id: Uuid) -> RepoResult { let db = svc.db.reader(); let key = key_entity::Entity::find_by_id(key_id) .one(&db) .await? .ok_or_else(|| RepoError::NotFound(format!("deploy key {key_id}")))?; if key.repo_id != repo_id { return Err(RepoError::NotFound(format!("deploy key {key_id}"))); } Ok(key) } pub async fn list_deploy_keys( svc: &RepoService, actor: Uuid, repo_id: Uuid, q: ListDeployKeyQuery, ) -> RepoResult { let _ = actor; let limit = q.limit.unwrap_or(svc.config.default_pagination); let offset = q.page.unwrap_or(0).saturating_mul(limit); let limit = std::cmp::min(limit, svc.config.max_pagination); let db = svc.db.reader(); let mut query = key_entity::Entity::find().filter(key_entity::Column::RepoId.eq(repo_id)); if let Some(id) = q.key_id { query = query.filter(key_entity::Column::Id.eq(id)); } if let Some(fp) = q.fingerprint { query = query.filter(key_entity::Column::Fingerprint.eq(fp)); } let total = query.clone().count(&db).await?; let items = query .limit(Some(limit)) .offset(Some(offset)) .all(&db) .await? .into_iter() .map(DeployKeyView::from) .collect(); Ok(DeployKeyPage { items, total: total as i64, }) } pub async fn get_deploy_key( svc: &RepoService, actor: Uuid, repo_id: Uuid, key_id: Uuid, ) -> RepoResult { let _ = actor; let key = load_key(svc, repo_id, key_id).await?; Ok(DeployKeyView::from(key)) } pub async fn create_deploy_key( svc: &RepoService, actor: Uuid, repo_id: Uuid, input: CreateDeployKeyOption, ) -> RepoResult { require_admin(actor)?; if input.title.trim().is_empty() { return Err(RepoError::BadRequest("title is required".into())); } if input.key.trim().is_empty() { return Err(RepoError::BadRequest("key content is required".into())); } let fingerprint = format!("sha256:{}", blake3_like_hash(&input.key)); let db = svc.db.writer(); let now = OffsetDateTime::now_utc(); let active: key_entity::ActiveModel = key_entity::ActiveModel { id: Set(Uuid::new_v4()), repo_id: Set(repo_id), title: Set(input.title), key_type: Set(db::repo::repo_deploy_keys::DeployKeyType::Ed25519), fingerprint: Set(fingerprint), content: Set(input.key), is_read_only: Set(input.read_only.unwrap_or(false)), last_used_at: Set(None), created_at: Set(now), }; let saved = active.insert(&db).await?; Ok(DeployKeyView::from(saved)) } pub async fn delete_deploy_key( svc: &RepoService, actor: Uuid, repo_id: Uuid, key_id: Uuid, ) -> RepoResult<()> { require_admin(actor)?; let _ = load_key(svc, repo_id, key_id).await?; let db = svc.db.writer(); key_entity::Entity::delete_by_id(key_id).exec(&db).await?; Ok(()) } fn blake3_like_hash(s: &str) -> String { use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; let mut h = DefaultHasher::new(); s.hash(&mut h); let v = h.finish(); format!("{v:016x}") }