729 lines
20 KiB
Rust
729 lines
20 KiB
Rust
//! REST HTTP API handlers for IAM.
|
|
|
|
use axum::{
|
|
extract::{Path, Query, State},
|
|
http::{HeaderMap, StatusCode},
|
|
routing::{get, post},
|
|
Json, Router,
|
|
};
|
|
use iam_client::client::{IamClient, IamClientConfig};
|
|
use iam_types::{Organization, Principal, PrincipalKind, PrincipalRef, Project, Scope};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// REST API state
|
|
#[derive(Clone)]
|
|
pub struct RestApiState {
|
|
pub server_addr: String,
|
|
pub tls_enabled: bool,
|
|
pub admin_token: Option<String>,
|
|
}
|
|
|
|
/// Standard REST error response
|
|
#[derive(Debug, Serialize)]
|
|
pub struct ErrorResponse {
|
|
pub error: ErrorDetail,
|
|
pub meta: ResponseMeta,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct ErrorDetail {
|
|
pub code: String,
|
|
pub message: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub details: Option<serde_json::Value>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct ResponseMeta {
|
|
pub request_id: String,
|
|
pub timestamp: String,
|
|
}
|
|
|
|
impl ResponseMeta {
|
|
fn new() -> Self {
|
|
Self {
|
|
request_id: uuid::Uuid::new_v4().to_string(),
|
|
timestamp: chrono::Utc::now().to_rfc3339(),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn iam_client_config(state: &RestApiState) -> IamClientConfig {
|
|
let mut config = IamClientConfig::new(&state.server_addr);
|
|
if let Some(token) = state.admin_token.as_deref() {
|
|
config = config.with_admin_token(token);
|
|
}
|
|
if !state.tls_enabled {
|
|
config = config.without_tls();
|
|
}
|
|
config
|
|
}
|
|
|
|
/// Standard REST success response
|
|
#[derive(Debug, Serialize)]
|
|
pub struct SuccessResponse<T> {
|
|
pub data: T,
|
|
pub meta: ResponseMeta,
|
|
}
|
|
|
|
impl<T> SuccessResponse<T> {
|
|
fn new(data: T) -> Self {
|
|
Self {
|
|
data,
|
|
meta: ResponseMeta::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct TokenRequest {
|
|
pub username: String,
|
|
pub password: String,
|
|
#[serde(default = "default_ttl")]
|
|
pub ttl_seconds: u64,
|
|
}
|
|
|
|
fn default_ttl() -> u64 {
|
|
3600
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct TokenResponse {
|
|
pub token: String,
|
|
pub expires_at: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct VerifyRequest {
|
|
pub token: String,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct VerifyResponse {
|
|
pub valid: bool,
|
|
pub principal_id: Option<String>,
|
|
pub principal_name: Option<String>,
|
|
pub roles: Option<Vec<String>>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct CreateUserRequest {
|
|
pub id: String,
|
|
pub name: String,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct UserResponse {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub kind: String,
|
|
pub org_id: Option<String>,
|
|
pub project_id: Option<String>,
|
|
pub enabled: bool,
|
|
}
|
|
|
|
impl From<Principal> for UserResponse {
|
|
fn from(p: Principal) -> Self {
|
|
Self {
|
|
id: p.id,
|
|
name: p.name,
|
|
kind: format!("{:?}", p.kind),
|
|
org_id: p.org_id,
|
|
project_id: p.project_id,
|
|
enabled: p.enabled,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct UsersResponse {
|
|
pub users: Vec<UserResponse>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct CreateOrganizationRequest {
|
|
pub id: String,
|
|
pub name: String,
|
|
#[serde(default)]
|
|
pub description: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct UpdateOrganizationRequest {
|
|
pub name: Option<String>,
|
|
pub description: Option<String>,
|
|
pub enabled: Option<bool>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct OrganizationResponse {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub description: String,
|
|
pub enabled: bool,
|
|
}
|
|
|
|
impl From<Organization> for OrganizationResponse {
|
|
fn from(org: Organization) -> Self {
|
|
Self {
|
|
id: org.id,
|
|
name: org.name,
|
|
description: org.description,
|
|
enabled: org.enabled,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct OrganizationsResponse {
|
|
pub organizations: Vec<OrganizationResponse>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct CreateProjectRequest {
|
|
pub id: String,
|
|
pub org_id: String,
|
|
pub name: String,
|
|
#[serde(default)]
|
|
pub description: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct UpdateProjectRequest {
|
|
pub name: Option<String>,
|
|
pub description: Option<String>,
|
|
pub enabled: Option<bool>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct ProjectsQuery {
|
|
pub org_id: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct ProjectResponse {
|
|
pub id: String,
|
|
pub org_id: String,
|
|
pub name: String,
|
|
pub description: String,
|
|
pub enabled: bool,
|
|
}
|
|
|
|
impl From<Project> for ProjectResponse {
|
|
fn from(project: Project) -> Self {
|
|
Self {
|
|
id: project.id,
|
|
org_id: project.org_id,
|
|
name: project.name,
|
|
description: project.description,
|
|
enabled: project.enabled,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct ProjectsResponse {
|
|
pub projects: Vec<ProjectResponse>,
|
|
}
|
|
|
|
pub fn build_router(state: RestApiState) -> Router {
|
|
Router::new()
|
|
.route("/api/v1/auth/token", post(issue_token))
|
|
.route("/api/v1/auth/verify", post(verify_token))
|
|
.route("/api/v1/users", get(list_users).post(create_user))
|
|
.route("/api/v1/users/{id}", get(get_user))
|
|
.route(
|
|
"/api/v1/orgs",
|
|
get(list_organizations).post(create_organization),
|
|
)
|
|
.route(
|
|
"/api/v1/orgs/{org_id}",
|
|
get(get_organization)
|
|
.patch(update_organization)
|
|
.delete(delete_organization),
|
|
)
|
|
.route("/api/v1/projects", get(list_projects).post(create_project))
|
|
.route(
|
|
"/api/v1/orgs/{org_id}/projects/{project_id}",
|
|
get(get_project)
|
|
.patch(update_project)
|
|
.delete(delete_project),
|
|
)
|
|
.route("/health", get(health_check))
|
|
.with_state(state)
|
|
}
|
|
|
|
async fn health_check() -> (StatusCode, Json<SuccessResponse<serde_json::Value>>) {
|
|
(
|
|
StatusCode::OK,
|
|
Json(SuccessResponse::new(
|
|
serde_json::json!({ "status": "healthy" }),
|
|
)),
|
|
)
|
|
}
|
|
|
|
fn require_admin(
|
|
state: &RestApiState,
|
|
headers: &HeaderMap,
|
|
) -> Result<(), (StatusCode, Json<ErrorResponse>)> {
|
|
let Some(token) = state.admin_token.as_deref() else {
|
|
return Ok(());
|
|
};
|
|
|
|
if let Some(value) = headers.get("x-iam-admin-token") {
|
|
if let Ok(raw) = value.to_str() {
|
|
if raw.trim() == token {
|
|
return Ok(());
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(value) = headers.get("authorization") {
|
|
if let Ok(raw) = value.to_str() {
|
|
let raw = raw.trim();
|
|
if let Some(rest) = raw
|
|
.strip_prefix("Bearer ")
|
|
.or_else(|| raw.strip_prefix("bearer "))
|
|
{
|
|
if rest.trim() == token {
|
|
return Ok(());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Err(error_response(
|
|
StatusCode::UNAUTHORIZED,
|
|
"ADMIN_TOKEN_REQUIRED",
|
|
"missing or invalid IAM admin token",
|
|
))
|
|
}
|
|
|
|
async fn connect_client(
|
|
state: &RestApiState,
|
|
) -> Result<IamClient, (StatusCode, Json<ErrorResponse>)> {
|
|
IamClient::connect(iam_client_config(state))
|
|
.await
|
|
.map_err(|e| {
|
|
error_response(
|
|
StatusCode::SERVICE_UNAVAILABLE,
|
|
"SERVICE_UNAVAILABLE",
|
|
&format!("Failed to connect: {}", e),
|
|
)
|
|
})
|
|
}
|
|
|
|
async fn issue_token(
|
|
headers: HeaderMap,
|
|
State(state): State<RestApiState>,
|
|
Json(req): Json<TokenRequest>,
|
|
) -> Result<Json<SuccessResponse<TokenResponse>>, (StatusCode, Json<ErrorResponse>)> {
|
|
require_admin(&state, &headers)?;
|
|
if !allow_insecure_rest_token_issue() {
|
|
return Err(error_response(
|
|
StatusCode::FORBIDDEN,
|
|
"TOKEN_ISSUE_DISABLED",
|
|
"token issuance is disabled; enable IAM_REST_ALLOW_INSECURE_TOKEN=true for dev",
|
|
));
|
|
}
|
|
|
|
let TokenRequest {
|
|
username,
|
|
password: _password,
|
|
ttl_seconds,
|
|
} = req;
|
|
|
|
let client = connect_client(&state).await?;
|
|
let principal_ref = PrincipalRef::new(PrincipalKind::User, &username);
|
|
let principal = match client.get_principal(&principal_ref).await.map_err(|e| {
|
|
error_response(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"PRINCIPAL_LOOKUP_FAILED",
|
|
&e.to_string(),
|
|
)
|
|
})? {
|
|
Some(principal) => principal,
|
|
None => {
|
|
return Err(error_response(
|
|
StatusCode::NOT_FOUND,
|
|
"PRINCIPAL_NOT_FOUND",
|
|
"principal not found",
|
|
))
|
|
}
|
|
};
|
|
|
|
if !principal.enabled {
|
|
return Err(error_response(
|
|
StatusCode::FORBIDDEN,
|
|
"PRINCIPAL_DISABLED",
|
|
"principal is disabled",
|
|
));
|
|
}
|
|
|
|
let token = client
|
|
.issue_token(&principal, vec![], Scope::System, ttl_seconds)
|
|
.await
|
|
.map_err(|e| {
|
|
error_response(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"TOKEN_ISSUE_FAILED",
|
|
&e.to_string(),
|
|
)
|
|
})?;
|
|
|
|
let expires_at = chrono::Utc::now() + chrono::Duration::seconds(ttl_seconds as i64);
|
|
|
|
Ok(Json(SuccessResponse::new(TokenResponse {
|
|
token,
|
|
expires_at: expires_at.to_rfc3339(),
|
|
})))
|
|
}
|
|
|
|
fn allow_insecure_rest_token_issue() -> bool {
|
|
std::env::var("IAM_REST_ALLOW_INSECURE_TOKEN")
|
|
.or_else(|_| std::env::var("PHOTON_IAM_REST_ALLOW_INSECURE_TOKEN"))
|
|
.ok()
|
|
.map(|value| {
|
|
matches!(
|
|
value.trim().to_lowercase().as_str(),
|
|
"1" | "true" | "yes" | "y" | "on"
|
|
)
|
|
})
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
async fn verify_token(
|
|
State(state): State<RestApiState>,
|
|
Json(req): Json<VerifyRequest>,
|
|
) -> Result<Json<SuccessResponse<VerifyResponse>>, (StatusCode, Json<ErrorResponse>)> {
|
|
let client = connect_client(&state).await?;
|
|
let result = client.validate_token(&req.token).await;
|
|
|
|
match result {
|
|
Ok(claims) => Ok(Json(SuccessResponse::new(VerifyResponse {
|
|
valid: true,
|
|
principal_id: Some(claims.principal_id),
|
|
principal_name: Some(claims.principal_name),
|
|
roles: Some(claims.roles),
|
|
}))),
|
|
Err(_) => Ok(Json(SuccessResponse::new(VerifyResponse {
|
|
valid: false,
|
|
principal_id: None,
|
|
principal_name: None,
|
|
roles: None,
|
|
}))),
|
|
}
|
|
}
|
|
|
|
async fn create_user(
|
|
headers: HeaderMap,
|
|
State(state): State<RestApiState>,
|
|
Json(req): Json<CreateUserRequest>,
|
|
) -> Result<(StatusCode, Json<SuccessResponse<UserResponse>>), (StatusCode, Json<ErrorResponse>)> {
|
|
require_admin(&state, &headers)?;
|
|
let client = connect_client(&state).await?;
|
|
let principal = client.create_user(&req.id, &req.name).await.map_err(|e| {
|
|
error_response(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"USER_CREATE_FAILED",
|
|
&e.to_string(),
|
|
)
|
|
})?;
|
|
|
|
Ok((
|
|
StatusCode::CREATED,
|
|
Json(SuccessResponse::new(UserResponse::from(principal))),
|
|
))
|
|
}
|
|
|
|
async fn list_users(
|
|
headers: HeaderMap,
|
|
State(state): State<RestApiState>,
|
|
) -> Result<Json<SuccessResponse<UsersResponse>>, (StatusCode, Json<ErrorResponse>)> {
|
|
require_admin(&state, &headers)?;
|
|
let client = connect_client(&state).await?;
|
|
let principals = client.list_users().await.map_err(|e| {
|
|
error_response(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"USER_LIST_FAILED",
|
|
&e.to_string(),
|
|
)
|
|
})?;
|
|
|
|
let users: Vec<UserResponse> = principals.into_iter().map(UserResponse::from).collect();
|
|
Ok(Json(SuccessResponse::new(UsersResponse { users })))
|
|
}
|
|
|
|
async fn get_user(
|
|
headers: HeaderMap,
|
|
Path(id): Path<String>,
|
|
State(state): State<RestApiState>,
|
|
) -> Result<Json<SuccessResponse<UserResponse>>, (StatusCode, Json<ErrorResponse>)> {
|
|
require_admin(&state, &headers)?;
|
|
let client = connect_client(&state).await?;
|
|
let principal = client
|
|
.get_principal(&PrincipalRef::user(id))
|
|
.await
|
|
.map_err(|e| {
|
|
error_response(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"USER_LOOKUP_FAILED",
|
|
&e.to_string(),
|
|
)
|
|
})?
|
|
.ok_or_else(|| error_response(StatusCode::NOT_FOUND, "USER_NOT_FOUND", "user not found"))?;
|
|
Ok(Json(SuccessResponse::new(UserResponse::from(principal))))
|
|
}
|
|
|
|
async fn create_organization(
|
|
headers: HeaderMap,
|
|
State(state): State<RestApiState>,
|
|
Json(req): Json<CreateOrganizationRequest>,
|
|
) -> Result<
|
|
(StatusCode, Json<SuccessResponse<OrganizationResponse>>),
|
|
(StatusCode, Json<ErrorResponse>),
|
|
> {
|
|
require_admin(&state, &headers)?;
|
|
let client = connect_client(&state).await?;
|
|
let org = client
|
|
.create_organization(&req.id, &req.name, &req.description)
|
|
.await
|
|
.map_err(|e| {
|
|
error_response(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"ORG_CREATE_FAILED",
|
|
&e.to_string(),
|
|
)
|
|
})?;
|
|
Ok((StatusCode::CREATED, Json(SuccessResponse::new(org.into()))))
|
|
}
|
|
|
|
async fn list_organizations(
|
|
headers: HeaderMap,
|
|
State(state): State<RestApiState>,
|
|
) -> Result<Json<SuccessResponse<OrganizationsResponse>>, (StatusCode, Json<ErrorResponse>)> {
|
|
require_admin(&state, &headers)?;
|
|
let client = connect_client(&state).await?;
|
|
let organizations = client.list_organizations().await.map_err(|e| {
|
|
error_response(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"ORG_LIST_FAILED",
|
|
&e.to_string(),
|
|
)
|
|
})?;
|
|
Ok(Json(SuccessResponse::new(OrganizationsResponse {
|
|
organizations: organizations.into_iter().map(Into::into).collect(),
|
|
})))
|
|
}
|
|
|
|
async fn get_organization(
|
|
headers: HeaderMap,
|
|
Path(org_id): Path<String>,
|
|
State(state): State<RestApiState>,
|
|
) -> Result<Json<SuccessResponse<OrganizationResponse>>, (StatusCode, Json<ErrorResponse>)> {
|
|
require_admin(&state, &headers)?;
|
|
let client = connect_client(&state).await?;
|
|
let organization = client
|
|
.get_organization(&org_id)
|
|
.await
|
|
.map_err(|e| {
|
|
error_response(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"ORG_LOOKUP_FAILED",
|
|
&e.to_string(),
|
|
)
|
|
})?
|
|
.ok_or_else(|| {
|
|
error_response(
|
|
StatusCode::NOT_FOUND,
|
|
"ORG_NOT_FOUND",
|
|
"organization not found",
|
|
)
|
|
})?;
|
|
Ok(Json(SuccessResponse::new(organization.into())))
|
|
}
|
|
|
|
async fn update_organization(
|
|
headers: HeaderMap,
|
|
Path(org_id): Path<String>,
|
|
State(state): State<RestApiState>,
|
|
Json(req): Json<UpdateOrganizationRequest>,
|
|
) -> Result<Json<SuccessResponse<OrganizationResponse>>, (StatusCode, Json<ErrorResponse>)> {
|
|
require_admin(&state, &headers)?;
|
|
let client = connect_client(&state).await?;
|
|
let organization = client
|
|
.update_organization(
|
|
&org_id,
|
|
req.name.as_deref(),
|
|
req.description.as_deref(),
|
|
req.enabled,
|
|
)
|
|
.await
|
|
.map_err(|e| {
|
|
error_response(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"ORG_UPDATE_FAILED",
|
|
&e.to_string(),
|
|
)
|
|
})?;
|
|
Ok(Json(SuccessResponse::new(organization.into())))
|
|
}
|
|
|
|
async fn delete_organization(
|
|
headers: HeaderMap,
|
|
Path(org_id): Path<String>,
|
|
State(state): State<RestApiState>,
|
|
) -> Result<Json<SuccessResponse<serde_json::Value>>, (StatusCode, Json<ErrorResponse>)> {
|
|
require_admin(&state, &headers)?;
|
|
let client = connect_client(&state).await?;
|
|
let deleted = client.delete_organization(&org_id).await.map_err(|e| {
|
|
error_response(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"ORG_DELETE_FAILED",
|
|
&e.to_string(),
|
|
)
|
|
})?;
|
|
Ok(Json(SuccessResponse::new(
|
|
serde_json::json!({ "deleted": deleted }),
|
|
)))
|
|
}
|
|
|
|
async fn create_project(
|
|
headers: HeaderMap,
|
|
State(state): State<RestApiState>,
|
|
Json(req): Json<CreateProjectRequest>,
|
|
) -> Result<(StatusCode, Json<SuccessResponse<ProjectResponse>>), (StatusCode, Json<ErrorResponse>)>
|
|
{
|
|
require_admin(&state, &headers)?;
|
|
let client = connect_client(&state).await?;
|
|
let project = client
|
|
.create_project(&req.org_id, &req.id, &req.name, &req.description)
|
|
.await
|
|
.map_err(|e| {
|
|
error_response(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"PROJECT_CREATE_FAILED",
|
|
&e.to_string(),
|
|
)
|
|
})?;
|
|
Ok((
|
|
StatusCode::CREATED,
|
|
Json(SuccessResponse::new(project.into())),
|
|
))
|
|
}
|
|
|
|
async fn list_projects(
|
|
headers: HeaderMap,
|
|
Query(query): Query<ProjectsQuery>,
|
|
State(state): State<RestApiState>,
|
|
) -> Result<Json<SuccessResponse<ProjectsResponse>>, (StatusCode, Json<ErrorResponse>)> {
|
|
require_admin(&state, &headers)?;
|
|
let client = connect_client(&state).await?;
|
|
let projects = client
|
|
.list_projects(query.org_id.as_deref())
|
|
.await
|
|
.map_err(|e| {
|
|
error_response(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"PROJECT_LIST_FAILED",
|
|
&e.to_string(),
|
|
)
|
|
})?;
|
|
Ok(Json(SuccessResponse::new(ProjectsResponse {
|
|
projects: projects.into_iter().map(Into::into).collect(),
|
|
})))
|
|
}
|
|
|
|
async fn get_project(
|
|
headers: HeaderMap,
|
|
Path((org_id, project_id)): Path<(String, String)>,
|
|
State(state): State<RestApiState>,
|
|
) -> Result<Json<SuccessResponse<ProjectResponse>>, (StatusCode, Json<ErrorResponse>)> {
|
|
require_admin(&state, &headers)?;
|
|
let client = connect_client(&state).await?;
|
|
let project = client
|
|
.get_project(&org_id, &project_id)
|
|
.await
|
|
.map_err(|e| {
|
|
error_response(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"PROJECT_LOOKUP_FAILED",
|
|
&e.to_string(),
|
|
)
|
|
})?
|
|
.ok_or_else(|| {
|
|
error_response(
|
|
StatusCode::NOT_FOUND,
|
|
"PROJECT_NOT_FOUND",
|
|
"project not found",
|
|
)
|
|
})?;
|
|
Ok(Json(SuccessResponse::new(project.into())))
|
|
}
|
|
|
|
async fn update_project(
|
|
headers: HeaderMap,
|
|
Path((org_id, project_id)): Path<(String, String)>,
|
|
State(state): State<RestApiState>,
|
|
Json(req): Json<UpdateProjectRequest>,
|
|
) -> Result<Json<SuccessResponse<ProjectResponse>>, (StatusCode, Json<ErrorResponse>)> {
|
|
require_admin(&state, &headers)?;
|
|
let client = connect_client(&state).await?;
|
|
let project = client
|
|
.update_project(
|
|
&org_id,
|
|
&project_id,
|
|
req.name.as_deref(),
|
|
req.description.as_deref(),
|
|
req.enabled,
|
|
)
|
|
.await
|
|
.map_err(|e| {
|
|
error_response(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"PROJECT_UPDATE_FAILED",
|
|
&e.to_string(),
|
|
)
|
|
})?;
|
|
Ok(Json(SuccessResponse::new(project.into())))
|
|
}
|
|
|
|
async fn delete_project(
|
|
headers: HeaderMap,
|
|
Path((org_id, project_id)): Path<(String, String)>,
|
|
State(state): State<RestApiState>,
|
|
) -> Result<Json<SuccessResponse<serde_json::Value>>, (StatusCode, Json<ErrorResponse>)> {
|
|
require_admin(&state, &headers)?;
|
|
let client = connect_client(&state).await?;
|
|
let deleted = client
|
|
.delete_project(&org_id, &project_id)
|
|
.await
|
|
.map_err(|e| {
|
|
error_response(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"PROJECT_DELETE_FAILED",
|
|
&e.to_string(),
|
|
)
|
|
})?;
|
|
Ok(Json(SuccessResponse::new(
|
|
serde_json::json!({ "deleted": deleted }),
|
|
)))
|
|
}
|
|
|
|
fn error_response(
|
|
status: StatusCode,
|
|
code: &str,
|
|
message: &str,
|
|
) -> (StatusCode, Json<ErrorResponse>) {
|
|
(
|
|
status,
|
|
Json(ErrorResponse {
|
|
error: ErrorDetail {
|
|
code: code.to_string(),
|
|
message: message.to_string(),
|
|
details: None,
|
|
},
|
|
meta: ResponseMeta::new(),
|
|
}),
|
|
)
|
|
}
|