360 lines
12 KiB
Rust
360 lines
12 KiB
Rust
use crate::middleware::auth::AuthContext;
|
||
use crate::models::{
|
||
App, AppStatusChangeRequest, ApproveAppStatusChangeRequest, CreateAppRequest, ListAppsQuery,
|
||
RequestAppStatusChangeRequest, UpdateAppRequest,
|
||
};
|
||
use crate::presentation::http::state::AppState;
|
||
use axum::{
|
||
Json,
|
||
extract::{Path, Query, State},
|
||
http::HeaderMap,
|
||
};
|
||
use common_telemetry::{AppError, AppResponse};
|
||
use tracing::instrument;
|
||
use uuid::Uuid;
|
||
|
||
fn require_sensitive_token(headers: &HeaderMap) -> Result<(), AppError> {
|
||
let Some(expected) = std::env::var("IAM_SENSITIVE_ACTION_TOKEN")
|
||
.ok()
|
||
.filter(|v| !v.is_empty())
|
||
else {
|
||
return Ok(());
|
||
};
|
||
let provided = headers
|
||
.get("X-Sensitive-Token")
|
||
.and_then(|v| v.to_str().ok())
|
||
.unwrap_or("")
|
||
.to_string();
|
||
if provided == expected {
|
||
Ok(())
|
||
} else {
|
||
Err(AppError::PermissionDenied(
|
||
"sensitive:token_required".into(),
|
||
))
|
||
}
|
||
}
|
||
|
||
#[utoipa::path(
|
||
post,
|
||
path = "/platform/apps",
|
||
tag = "App",
|
||
security(
|
||
("bearer_auth" = [])
|
||
),
|
||
request_body = CreateAppRequest,
|
||
responses(
|
||
(status = 201, description = "App created", body = App),
|
||
(status = 400, description = "Bad request"),
|
||
(status = 401, description = "Unauthorized"),
|
||
(status = 403, description = "Forbidden")
|
||
),
|
||
params(
|
||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)")
|
||
)
|
||
)]
|
||
#[instrument(skip(state, payload))]
|
||
pub async fn create_app_handler(
|
||
State(state): State<AppState>,
|
||
AuthContext { user_id, .. }: AuthContext,
|
||
Json(payload): Json<CreateAppRequest>,
|
||
) -> Result<AppResponse<App>, AppError> {
|
||
state
|
||
.authorization_service
|
||
.require_platform_permission(user_id, "iam:app:write")
|
||
.await?;
|
||
let app = state.app_service.create_app(payload, user_id).await?;
|
||
Ok(AppResponse::created(app))
|
||
}
|
||
|
||
#[utoipa::path(
|
||
get,
|
||
path = "/platform/apps",
|
||
tag = "App",
|
||
security(
|
||
("bearer_auth" = [])
|
||
),
|
||
responses(
|
||
(status = 200, description = "Apps list", body = [App]),
|
||
(status = 400, description = "Bad request"),
|
||
(status = 401, description = "Unauthorized"),
|
||
(status = 403, description = "Forbidden")
|
||
),
|
||
params(
|
||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||
ListAppsQuery
|
||
)
|
||
)]
|
||
#[instrument(skip(state))]
|
||
pub async fn list_apps_handler(
|
||
State(state): State<AppState>,
|
||
AuthContext { user_id, .. }: AuthContext,
|
||
Query(query): Query<ListAppsQuery>,
|
||
) -> Result<AppResponse<Vec<App>>, AppError> {
|
||
state
|
||
.authorization_service
|
||
.require_platform_permission(user_id, "iam:app:read")
|
||
.await?;
|
||
let apps = state.app_service.list_apps(query).await?;
|
||
Ok(AppResponse::ok(apps))
|
||
}
|
||
|
||
#[utoipa::path(
|
||
get,
|
||
path = "/platform/apps/{app_id}",
|
||
tag = "App",
|
||
security(
|
||
("bearer_auth" = [])
|
||
),
|
||
responses(
|
||
(status = 200, description = "App detail", body = App),
|
||
(status = 401, description = "Unauthorized"),
|
||
(status = 403, description = "Forbidden"),
|
||
(status = 404, description = "Not found")
|
||
),
|
||
params(
|
||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||
("app_id" = String, Path, description = "App id")
|
||
)
|
||
)]
|
||
#[instrument(skip(state))]
|
||
pub async fn get_app_handler(
|
||
State(state): State<AppState>,
|
||
AuthContext { user_id, .. }: AuthContext,
|
||
Path(app_id): Path<String>,
|
||
) -> Result<AppResponse<App>, AppError> {
|
||
state
|
||
.authorization_service
|
||
.require_platform_permission(user_id, "iam:app:read")
|
||
.await?;
|
||
let app = state.app_service.get_app(&app_id).await?;
|
||
Ok(AppResponse::ok(app))
|
||
}
|
||
|
||
#[utoipa::path(
|
||
patch,
|
||
path = "/platform/apps/{app_id}",
|
||
tag = "App",
|
||
security(
|
||
("bearer_auth" = [])
|
||
),
|
||
request_body = UpdateAppRequest,
|
||
responses(
|
||
(status = 200, description = "Updated", body = App),
|
||
(status = 400, description = "Bad request"),
|
||
(status = 401, description = "Unauthorized"),
|
||
(status = 403, description = "Forbidden"),
|
||
(status = 404, description = "Not found")
|
||
),
|
||
params(
|
||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||
("app_id" = String, Path, description = "App id")
|
||
)
|
||
)]
|
||
#[instrument(skip(state, payload))]
|
||
pub async fn update_app_handler(
|
||
State(state): State<AppState>,
|
||
AuthContext { user_id, .. }: AuthContext,
|
||
Path(app_id): Path<String>,
|
||
Json(payload): Json<UpdateAppRequest>,
|
||
) -> Result<AppResponse<App>, AppError> {
|
||
state
|
||
.authorization_service
|
||
.require_platform_permission(user_id, "iam:app:write")
|
||
.await?;
|
||
let app = state
|
||
.app_service
|
||
.update_app(&app_id, payload, user_id)
|
||
.await?;
|
||
Ok(AppResponse::ok(app))
|
||
}
|
||
|
||
#[utoipa::path(
|
||
post,
|
||
path = "/platform/apps/{app_id}/status-change-requests",
|
||
tag = "App",
|
||
security(
|
||
("bearer_auth" = [])
|
||
),
|
||
request_body = RequestAppStatusChangeRequest,
|
||
responses(
|
||
(status = 201, description = "Request created", body = AppStatusChangeRequest),
|
||
(status = 400, description = "Bad request"),
|
||
(status = 401, description = "Unauthorized"),
|
||
(status = 403, description = "Forbidden"),
|
||
(status = 404, description = "Not found")
|
||
),
|
||
params(
|
||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||
("app_id" = String, Path, description = "App id")
|
||
)
|
||
)]
|
||
#[instrument(skip(state, payload))]
|
||
pub async fn request_app_status_change_handler(
|
||
State(state): State<AppState>,
|
||
AuthContext { user_id, .. }: AuthContext,
|
||
Path(app_id): Path<String>,
|
||
Json(payload): Json<RequestAppStatusChangeRequest>,
|
||
) -> Result<AppResponse<AppStatusChangeRequest>, AppError> {
|
||
state
|
||
.authorization_service
|
||
.require_platform_permission(user_id, "iam:app:write")
|
||
.await?;
|
||
let req = state
|
||
.app_service
|
||
.request_status_change(&app_id, payload, user_id)
|
||
.await?;
|
||
Ok(AppResponse::created(req))
|
||
}
|
||
|
||
#[utoipa::path(
|
||
get,
|
||
path = "/platform/app-status-change-requests",
|
||
tag = "App",
|
||
security(
|
||
("bearer_auth" = [])
|
||
),
|
||
responses(
|
||
(status = 200, description = "Requests list", body = [AppStatusChangeRequest]),
|
||
(status = 400, description = "Bad request"),
|
||
(status = 401, description = "Unauthorized"),
|
||
(status = 403, description = "Forbidden")
|
||
),
|
||
params(
|
||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||
("status" = Option<String>, Query, description = "pending/approved/applied/rejected"),
|
||
("page" = Option<u32>, Query, description = "页码,默认 1"),
|
||
("page_size" = Option<u32>, Query, description = "每页数量,默认 20,最大 200")
|
||
)
|
||
)]
|
||
#[instrument(skip(state))]
|
||
pub async fn list_app_status_change_requests_handler(
|
||
State(state): State<AppState>,
|
||
AuthContext { user_id, .. }: AuthContext,
|
||
Query(params): Query<std::collections::HashMap<String, String>>,
|
||
) -> Result<AppResponse<Vec<AppStatusChangeRequest>>, AppError> {
|
||
state
|
||
.authorization_service
|
||
.require_platform_permission(user_id, "iam:app:read")
|
||
.await?;
|
||
let status = params.get("status").cloned();
|
||
let page = params.get("page").and_then(|v| v.parse::<u32>().ok());
|
||
let page_size = params.get("page_size").and_then(|v| v.parse::<u32>().ok());
|
||
let rows = state
|
||
.app_service
|
||
.list_status_change_requests(status, page, page_size)
|
||
.await?;
|
||
Ok(AppResponse::ok(rows))
|
||
}
|
||
|
||
#[utoipa::path(
|
||
post,
|
||
path = "/platform/app-status-change-requests/{request_id}/approve",
|
||
tag = "App",
|
||
security(
|
||
("bearer_auth" = [])
|
||
),
|
||
request_body = ApproveAppStatusChangeRequest,
|
||
responses(
|
||
(status = 200, description = "Approved", body = AppStatusChangeRequest),
|
||
(status = 400, description = "Bad request"),
|
||
(status = 401, description = "Unauthorized"),
|
||
(status = 403, description = "Forbidden"),
|
||
(status = 404, description = "Not found")
|
||
),
|
||
params(
|
||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||
("request_id" = String, Path, description = "Request id (UUID)")
|
||
)
|
||
)]
|
||
#[instrument(skip(state, payload))]
|
||
pub async fn approve_app_status_change_handler(
|
||
State(state): State<AppState>,
|
||
AuthContext { user_id, .. }: AuthContext,
|
||
Path(request_id): Path<Uuid>,
|
||
Json(payload): Json<ApproveAppStatusChangeRequest>,
|
||
) -> Result<AppResponse<AppStatusChangeRequest>, AppError> {
|
||
state
|
||
.authorization_service
|
||
.require_platform_permission(user_id, "iam:app:approve")
|
||
.await?;
|
||
let row = state
|
||
.app_service
|
||
.approve_status_change(request_id, payload.effective_at, user_id)
|
||
.await?;
|
||
Ok(AppResponse::ok(row))
|
||
}
|
||
|
||
#[utoipa::path(
|
||
post,
|
||
path = "/platform/app-status-change-requests/{request_id}/reject",
|
||
tag = "App",
|
||
security(
|
||
("bearer_auth" = [])
|
||
),
|
||
responses(
|
||
(status = 200, description = "Rejected", body = AppStatusChangeRequest),
|
||
(status = 400, description = "Bad request"),
|
||
(status = 401, description = "Unauthorized"),
|
||
(status = 403, description = "Forbidden"),
|
||
(status = 404, description = "Not found")
|
||
),
|
||
params(
|
||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||
("request_id" = String, Path, description = "Request id (UUID)"),
|
||
("reason" = Option<String>, Query, description = "Reject reason")
|
||
)
|
||
)]
|
||
#[instrument(skip(state))]
|
||
pub async fn reject_app_status_change_handler(
|
||
State(state): State<AppState>,
|
||
AuthContext { user_id, .. }: AuthContext,
|
||
Path(request_id): Path<Uuid>,
|
||
Query(params): Query<std::collections::HashMap<String, String>>,
|
||
) -> Result<AppResponse<AppStatusChangeRequest>, AppError> {
|
||
state
|
||
.authorization_service
|
||
.require_platform_permission(user_id, "iam:app:approve")
|
||
.await?;
|
||
let reason = params.get("reason").cloned();
|
||
let row = state
|
||
.app_service
|
||
.reject_status_change(request_id, reason, user_id)
|
||
.await?;
|
||
Ok(AppResponse::ok(row))
|
||
}
|
||
|
||
#[utoipa::path(
|
||
delete,
|
||
path = "/platform/apps/{app_id}",
|
||
tag = "App",
|
||
security(
|
||
("bearer_auth" = [])
|
||
),
|
||
responses(
|
||
(status = 200, description = "Deleted"),
|
||
(status = 401, description = "Unauthorized"),
|
||
(status = 403, description = "Forbidden"),
|
||
(status = 404, description = "Not found")
|
||
),
|
||
params(
|
||
("Authorization" = String, Header, description = "Bearer <access_token>(访问令牌)"),
|
||
("X-Sensitive-Token" = Option<String>, Header, description = "二次验证令牌(当 IAM_SENSITIVE_ACTION_TOKEN 设置时必填)"),
|
||
("app_id" = String, Path, description = "App id")
|
||
)
|
||
)]
|
||
#[instrument(skip(state, headers))]
|
||
pub async fn delete_app_handler(
|
||
State(state): State<AppState>,
|
||
AuthContext { user_id, .. }: AuthContext,
|
||
headers: HeaderMap,
|
||
Path(app_id): Path<String>,
|
||
) -> Result<AppResponse<serde_json::Value>, AppError> {
|
||
state
|
||
.authorization_service
|
||
.require_platform_permission(user_id, "iam:app:delete")
|
||
.await?;
|
||
require_sensitive_token(&headers)?;
|
||
state.app_service.delete_app(&app_id, user_id).await?;
|
||
Ok(AppResponse::ok(serde_json::json!({})))
|
||
}
|