use axum::extract::{Path, State};
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::routing::{get, post};
use axum::{Extension, Json, Router};
use kyma_catalog::PgCredentialStore;
use kyma_core::credentials::CredentialValue;
use kyma_core::tenant::TenantId;
use serde::Deserialize;
use serde_json::Value;
use std::sync::Arc;
use uuid::Uuid;
#[derive(Clone)]
pub struct CredentialsState {
pub store: Arc<PgCredentialStore>,
}
pub fn router(state: CredentialsState) -> Router {
Router::new()
.route("/v1/credentials", post(create).get(list))
.route("/v1/credentials/:id", get(get_one).delete(delete_one))
.with_state(state)
}
#[derive(Deserialize)]
struct CreateReq {
label: String,
value: CredentialValue,
#[serde(default)]
metadata: Value,
}
async fn create(
Extension(tenant): Extension<TenantId>,
State(s): State<CredentialsState>,
Json(req): Json<CreateReq>,
) -> impl IntoResponse {
if req.label.trim().is_empty() {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": "label is required" })),
)
.into_response();
}
let meta = if req.metadata.is_null() {
Value::Object(Default::default())
} else {
req.metadata
};
match s
.store
.create(tenant, req.label.trim(), &req.value, meta)
.await
{
Ok(summary) => (StatusCode::CREATED, Json(summary)).into_response(),
Err(e) => (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "error": e.to_string() })),
)
.into_response(),
}
}
async fn list(
Extension(tenant): Extension<TenantId>,
State(s): State<CredentialsState>,
) -> impl IntoResponse {
match s.store.list(tenant).await {
Ok(items) => (StatusCode::OK, Json(serde_json::json!({ "items": items }))).into_response(),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e.to_string() })),
)
.into_response(),
}
}
async fn get_one(
Extension(tenant): Extension<TenantId>,
State(s): State<CredentialsState>,
Path(id): Path<Uuid>,
) -> impl IntoResponse {
match s.store.fetch(tenant, id).await {
Ok(c) => {
let preview = c.value.preview();
(
StatusCode::OK,
Json(serde_json::json!({
"id": c.id,
"label": c.label,
"kind": c.kind,
"preview": preview,
"metadata": c.metadata,
"created_at": c.created_at,
"updated_at": c.updated_at,
})),
)
.into_response()
}
Err(e) => {
let msg = e.to_string();
if msg.contains("not found") {
StatusCode::NOT_FOUND.into_response()
} else {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": msg })),
)
.into_response()
}
}
}
}
async fn delete_one(
Extension(tenant): Extension<TenantId>,
State(s): State<CredentialsState>,
Path(id): Path<Uuid>,
) -> impl IntoResponse {
match s.store.delete(tenant, id).await {
Ok(_) => StatusCode::NO_CONTENT.into_response(),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": e.to_string() })),
)
.into_response(),
}
}