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 (访问令牌)") ) )] #[instrument(skip(state, payload))] pub async fn create_app_handler( State(state): State, AuthContext { user_id, .. }: AuthContext, Json(payload): Json, ) -> Result, 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 (访问令牌)"), ListAppsQuery ) )] #[instrument(skip(state))] pub async fn list_apps_handler( State(state): State, AuthContext { user_id, .. }: AuthContext, Query(query): Query, ) -> Result>, 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 (访问令牌)"), ("app_id" = String, Path, description = "App id") ) )] #[instrument(skip(state))] pub async fn get_app_handler( State(state): State, AuthContext { user_id, .. }: AuthContext, Path(app_id): Path, ) -> Result, 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 (访问令牌)"), ("app_id" = String, Path, description = "App id") ) )] #[instrument(skip(state, payload))] pub async fn update_app_handler( State(state): State, AuthContext { user_id, .. }: AuthContext, Path(app_id): Path, Json(payload): Json, ) -> Result, 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 (访问令牌)"), ("app_id" = String, Path, description = "App id") ) )] #[instrument(skip(state, payload))] pub async fn request_app_status_change_handler( State(state): State, AuthContext { user_id, .. }: AuthContext, Path(app_id): Path, Json(payload): Json, ) -> Result, 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 (访问令牌)"), ("status" = Option, Query, description = "pending/approved/applied/rejected"), ("page" = Option, Query, description = "页码,默认 1"), ("page_size" = Option, Query, description = "每页数量,默认 20,最大 200") ) )] #[instrument(skip(state))] pub async fn list_app_status_change_requests_handler( State(state): State, AuthContext { user_id, .. }: AuthContext, Query(params): Query>, ) -> Result>, 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::().ok()); let page_size = params.get("page_size").and_then(|v| v.parse::().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 (访问令牌)"), ("request_id" = String, Path, description = "Request id (UUID)") ) )] #[instrument(skip(state, payload))] pub async fn approve_app_status_change_handler( State(state): State, AuthContext { user_id, .. }: AuthContext, Path(request_id): Path, Json(payload): Json, ) -> Result, 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 (访问令牌)"), ("request_id" = String, Path, description = "Request id (UUID)"), ("reason" = Option, Query, description = "Reject reason") ) )] #[instrument(skip(state))] pub async fn reject_app_status_change_handler( State(state): State, AuthContext { user_id, .. }: AuthContext, Path(request_id): Path, Query(params): Query>, ) -> Result, 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 (访问令牌)"), ("X-Sensitive-Token" = Option, 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, AuthContext { user_id, .. }: AuthContext, headers: HeaderMap, Path(app_id): Path, ) -> Result, 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!({}))) }