f0a1552
ZhenYi
feat(chat): add chat module with room management, message handling, and session revocation
f0a1552
use models::users::user_ssh_keys;
use models::users::user_ssh_keys::SshKeyType;
use sea_orm::{
ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, Set,
};
use serde::{Deserialize, Serialize};
use ssh_key::{Algorithm, EcdsaCurve, HashAlg};
use time::OffsetDateTime;
use uuid::Uuid;
use super::AuthService;
use crate::errors::AuthError;
use crate::share::types::ListQuery;
#[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<SshKeyType> {
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<SshKeyDto, AuthError> {
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,
query: ListQuery,
) -> Result<export::pagination::Page<SshKeyDto>, AuthError> {
let (limit, offset) = query.page();
let total = user_ssh_keys::Entity::find()
.filter(user_ssh_keys::Column::UserId.eq(uid))
.count(self.db().read())
.await? as i64;
let rows = user_ssh_keys::Entity::find()
.filter(user_ssh_keys::Column::UserId.eq(uid))
.order_by_desc(user_ssh_keys::Column::CreatedAt)
.limit(limit)
.offset(offset)
.all(self.db().read())
.await?;
Ok(export::pagination::Page {
total,
limit,
offset,
items: 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(())
}
}