a06673b
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<key_entity::Model> {
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<DeployKeyPage> {
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<DeployKeyView> {
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<DeployKeyView> {
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}")
}