gitdataai/libs/service/user/access_key.rs
2026-04-14 19:02:01 +08:00

242 lines
8.0 KiB
Rust

use crate::AppService;
use crate::error::AppError;
use base64::Engine;
use chrono::Utc;
use models::users::{user_activity_log, user_token};
use sea_orm::*;
use serde::{Deserialize, Serialize};
use session::Session;
use uuid::Uuid;
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
pub struct CreateAccessKeyParams {
pub name: String,
pub scopes: Vec<String>,
pub expires_at: Option<chrono::DateTime<Utc>>,
}
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
pub struct AccessKeyResponse {
pub id: i64,
pub name: String,
pub access_key: Option<String>,
pub scopes: Vec<String>,
pub expires_at: Option<chrono::DateTime<Utc>>,
pub is_revoked: bool,
pub created_at: chrono::DateTime<Utc>,
}
#[derive(Deserialize, Serialize, Clone, Debug, utoipa::ToSchema)]
pub struct AccessKeyListResponse {
pub access_keys: Vec<AccessKeyResponse>,
pub total: usize,
}
impl AppService {
pub async fn user_create_access_key(
&self,
context: &Session,
params: CreateAccessKeyParams,
) -> Result<AccessKeyResponse, AppError> {
let user_uid = context.user().ok_or(AppError::Unauthorized)?;
let access_key = self.user_generate_access_key();
let access_key_hash = self.user_hash_access_key(&access_key);
let access_key_model = user_token::ActiveModel {
user: Set(user_uid),
name: Set(params.name.clone()),
token_hash: Set(access_key_hash),
scopes: Set(serde_json::to_value(params.scopes.clone()).unwrap()),
expires_at: Set(params.expires_at),
is_revoked: Set(false),
created_at: Set(Utc::now()),
updated_at: Set(Utc::now()),
..Default::default()
};
let created_access_key = access_key_model.insert(&self.db).await?;
let _ = user_activity_log::ActiveModel {
user_uid: Set(Some(user_uid)),
action: Set("access_key_create".to_string()),
ip_address: Set(context.ip_address()),
user_agent: Set(context.user_agent()),
details: Set(serde_json::json!({
"access_key_name": params.name.clone(),
"access_key_id": created_access_key.id,
"scopes": params.scopes.clone()
})),
created_at: Set(Utc::now()),
..Default::default()
}
.insert(&self.db)
.await;
let scopes: Vec<String> =
serde_json::from_value(created_access_key.scopes).unwrap_or_default();
Ok(AccessKeyResponse {
id: created_access_key.id,
name: created_access_key.name,
access_key: Some(access_key),
scopes,
expires_at: created_access_key.expires_at,
is_revoked: created_access_key.is_revoked,
created_at: created_access_key.created_at,
})
}
pub async fn user_list_access_keys(
&self,
context: &Session,
) -> Result<AccessKeyListResponse, AppError> {
let user_uid = context.user().ok_or(AppError::Unauthorized)?;
let access_keys = user_token::Entity::find()
.filter(user_token::Column::User.eq(user_uid))
.order_by_desc(user_token::Column::CreatedAt)
.all(&self.db)
.await?;
let total = access_keys.len();
let access_keys = access_keys
.into_iter()
.map(|ak| {
let scopes: Vec<String> = serde_json::from_value(ak.scopes).unwrap_or_default();
AccessKeyResponse {
id: ak.id,
name: ak.name,
access_key: None,
scopes,
expires_at: ak.expires_at,
is_revoked: ak.is_revoked,
created_at: ak.created_at,
}
})
.collect();
Ok(AccessKeyListResponse { access_keys, total })
}
pub async fn user_revoke_access_key(
&self,
context: &Session,
access_key_id: i64,
) -> Result<(), AppError> {
let user_uid = context.user().ok_or(AppError::Unauthorized)?;
let access_key = user_token::Entity::find_by_id(access_key_id)
.filter(user_token::Column::User.eq(user_uid))
.one(&self.db)
.await?
.ok_or(AppError::NotFound("Access key not found".to_string()))?;
let mut active_access_key: user_token::ActiveModel = access_key.clone().into();
active_access_key.is_revoked = Set(true);
active_access_key.updated_at = Set(Utc::now());
active_access_key.update(&self.db).await?;
let _ = user_activity_log::ActiveModel {
user_uid: Set(Some(user_uid)),
action: Set("access_key_revoke".to_string()),
ip_address: Set(context.ip_address()),
user_agent: Set(context.user_agent()),
details: Set(serde_json::json!({
"access_key_name": access_key.name,
"access_key_id": access_key_id
})),
created_at: Set(Utc::now()),
..Default::default()
}
.insert(&self.db)
.await;
Ok(())
}
pub async fn user_delete_access_key(
&self,
context: &Session,
access_key_id: i64,
) -> Result<(), AppError> {
let user_uid = context.user().ok_or(AppError::Unauthorized)?;
let access_key = user_token::Entity::find_by_id(access_key_id)
.filter(user_token::Column::User.eq(user_uid))
.one(&self.db)
.await?
.ok_or(AppError::NotFound("Access key not found".to_string()))?;
let _ = user_activity_log::ActiveModel {
user_uid: Set(Some(user_uid)),
action: Set("access_key_delete".to_string()),
ip_address: Set(context.ip_address()),
user_agent: Set(context.user_agent()),
details: Set(serde_json::json!({
"access_key_name": access_key.name.clone(),
"access_key_id": access_key_id
})),
created_at: Set(Utc::now()),
..Default::default()
}
.insert(&self.db)
.await;
user_token::Entity::delete(access_key.into_active_model())
.exec(&self.db)
.await?;
Ok(())
}
pub async fn user_verify_access_key(&self, access_key: String) -> Result<Uuid, AppError> {
let access_key_hash = self.user_hash_access_key(&access_key);
let access_key_model = user_token::Entity::find()
.filter(user_token::Column::TokenHash.eq(access_key_hash))
.filter(user_token::Column::IsRevoked.eq(false))
.one(&self.db)
.await?
.ok_or(AppError::Unauthorized)?;
if let Some(expires_at) = access_key_model.expires_at {
if expires_at < Utc::now() {
return Err(AppError::Unauthorized);
}
}
Ok(access_key_model.user)
}
fn user_generate_access_key(&self) -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let chars: Vec<char> = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
.chars()
.collect();
let mut access_key = String::with_capacity(68);
access_key.push_str("gda_");
let mut state = now as u64;
for _ in 0..64 {
state = state.wrapping_mul(1103515245).wrapping_add(12345);
access_key.push(chars[(state as usize) % chars.len()]);
}
access_key
}
fn user_hash_access_key(&self, access_key: &str) -> String {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(access_key.as_bytes());
format!(
"{}",
base64::prelude::BASE64_STANDARD.encode(&hasher.finalize().to_vec())
)
}
}