feat: complete Epic 1 production foundation

This commit is contained in:
2026-08-25 01:24:11 +03:00
parent 767428436d
commit 182bde8ac0
298 changed files with 35719 additions and 5299 deletions
+92 -5
View File
@@ -1,15 +1,19 @@
use axum::{
Json,
Extension, Json,
extract::{Path, State},
http::{HeaderMap, header},
response::IntoResponse,
};
use serde::Deserialize;
use serde_json::{Value, json};
use crate::{
auth::AuthenticatedSession,
error::ApiError,
request_context::RequestContext,
service::{
AgentCatalogPayload, AgentPayload, PlatformApiKeyPayload, PublishPayload,
ToolSearchPreviewPayload, UpdateAgentPayload,
AdminAuditContext, AgentCatalogPayload, AgentPayload, PlatformApiKeyPayload,
PublishPayload, ToolSearchPreviewPayload, UpdateAgentPayload,
},
state::AppState,
};
@@ -77,7 +81,7 @@ pub async fn create_agent(
pub async fn get_agent(
Path(path): Path<WorkspaceAgentPath>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
) -> Result<impl IntoResponse, ApiError> {
let agent = state
.service
.get_agent(
@@ -85,20 +89,30 @@ pub async fn get_agent(
&path.agent_id.as_str().into(),
)
.await?;
Ok(Json(json!(agent)))
let etag = crate::service::AdminService::agent_state_etag(&agent);
let mut headers = HeaderMap::new();
headers.insert(
header::ETAG,
header::HeaderValue::from_str(&etag)
.map_err(|_| ApiError::internal("invalid agent etag"))?,
);
Ok((headers, Json(json!(agent))))
}
pub async fn update_agent(
Path(path): Path<WorkspaceAgentPath>,
State(state): State<AppState>,
headers: HeaderMap,
Json(payload): Json<UpdateAgentPayload>,
) -> Result<Json<Value>, ApiError> {
let expected_state = require_agent_precondition(&state, &path, &headers).await?;
let updated = state
.service
.update_agent(
&path.workspace_id.as_str().into(),
&path.agent_id.as_str().into(),
payload,
Some(&expected_state),
)
.await?;
Ok(Json(json!(updated)))
@@ -107,12 +121,15 @@ pub async fn update_agent(
pub async fn delete_agent(
Path(path): Path<WorkspaceAgentPath>,
State(state): State<AppState>,
headers: HeaderMap,
) -> Result<Json<Value>, ApiError> {
let expected_state = require_agent_precondition(&state, &path, &headers).await?;
let deleted = state
.service
.delete_agent(
&path.workspace_id.as_str().into(),
&path.agent_id.as_str().into(),
Some(&expected_state),
)
.await?;
Ok(Json(json!(deleted)))
@@ -136,30 +153,79 @@ pub async fn get_agent_version(
pub async fn save_agent_bindings(
Path(path): Path<WorkspaceAgentPath>,
State(state): State<AppState>,
headers: HeaderMap,
Json(payload): Json<AgentCatalogPayload>,
) -> Result<Json<Value>, ApiError> {
let expected_state = require_agent_precondition(&state, &path, &headers).await?;
let record = state
.service
.save_agent_bindings(
&path.workspace_id.as_str().into(),
&path.agent_id.as_str().into(),
payload,
Some(&expected_state),
)
.await?;
Ok(Json(json!(record)))
}
async fn require_agent_precondition(
state: &AppState,
path: &WorkspaceAgentPath,
headers: &HeaderMap,
) -> Result<crank_registry::AgentStateExpectation, ApiError> {
let agent = state
.service
.get_agent(
&path.workspace_id.as_str().into(),
&path.agent_id.as_str().into(),
)
.await?;
let expected = crate::service::AdminService::agent_state_etag(&agent);
let provided = headers
.get(header::IF_MATCH)
.and_then(|value| value.to_str().ok())
.ok_or_else(|| {
ApiError::precondition_required_with_context(
"If-Match is required for Agent mutation",
json!({
"error_code": "agent_precondition_required",
"current_version": agent.current_draft_version,
"latest_published_version": agent.latest_published_version,
"catalog_revision": agent.catalog_revision,
"recovery": "reload"
}),
)
})?;
if provided != expected {
return Err(ApiError::conflict_with_context(
"Agent state changed; reload before retrying",
json!({
"error_code": "agent_stale_revision",
"current_version": agent.current_draft_version,
"latest_published_version": agent.latest_published_version,
"catalog_revision": agent.catalog_revision,
"recovery": "reload"
}),
));
}
crate::service::AdminService::agent_state_expectation(&agent)
}
pub async fn publish_agent(
Path(path): Path<WorkspaceAgentPath>,
State(state): State<AppState>,
headers: HeaderMap,
Json(payload): Json<PublishPayload>,
) -> Result<Json<Value>, ApiError> {
let expected_state = require_agent_precondition(&state, &path, &headers).await?;
let published = state
.service
.publish_agent(
&path.workspace_id.as_str().into(),
&path.agent_id.as_str().into(),
payload.version,
Some(&expected_state),
)
.await?;
Ok(Json(json!(published)))
@@ -168,12 +234,15 @@ pub async fn publish_agent(
pub async fn unpublish_agent(
Path(path): Path<WorkspaceAgentPath>,
State(state): State<AppState>,
headers: HeaderMap,
) -> Result<Json<Value>, ApiError> {
let expected_state = require_agent_precondition(&state, &path, &headers).await?;
let updated = state
.service
.unpublish_agent(
&path.workspace_id.as_str().into(),
&path.agent_id.as_str().into(),
Some(&expected_state),
)
.await?;
Ok(Json(json!(updated)))
@@ -182,12 +251,15 @@ pub async fn unpublish_agent(
pub async fn archive_agent(
Path(path): Path<WorkspaceAgentPath>,
State(state): State<AppState>,
headers: HeaderMap,
) -> Result<Json<Value>, ApiError> {
let expected_state = require_agent_precondition(&state, &path, &headers).await?;
let updated = state
.service
.archive_agent(
&path.workspace_id.as_str().into(),
&path.agent_id.as_str().into(),
Some(&expected_state),
)
.await?;
Ok(Json(json!(updated)))
@@ -210,14 +282,19 @@ pub async fn list_agent_platform_api_keys(
pub async fn create_agent_platform_api_key(
Path(path): Path<WorkspaceAgentPath>,
State(state): State<AppState>,
Extension(session): Extension<AuthenticatedSession>,
Extension(request_context): Extension<RequestContext>,
Json(payload): Json<PlatformApiKeyPayload>,
) -> Result<Json<Value>, ApiError> {
let audit_context =
AdminAuditContext::from_session_and_correlation(&session, &request_context.correlation);
let created = state
.service
.create_agent_platform_api_key(
&path.workspace_id.as_str().into(),
&path.agent_id.as_str().into(),
payload,
Some(&audit_context),
)
.await?;
Ok(Json(json!(created)))
@@ -226,13 +303,18 @@ pub async fn create_agent_platform_api_key(
pub async fn revoke_agent_platform_api_key(
Path(path): Path<WorkspaceAgentPlatformApiKeyPath>,
State(state): State<AppState>,
Extension(session): Extension<AuthenticatedSession>,
Extension(request_context): Extension<RequestContext>,
) -> Result<axum::http::StatusCode, ApiError> {
let audit_context =
AdminAuditContext::from_session_and_correlation(&session, &request_context.correlation);
state
.service
.revoke_agent_platform_api_key(
&path.workspace_id.as_str().into(),
&path.agent_id.as_str().into(),
&path.key_id.as_str().into(),
Some(&audit_context),
)
.await?;
Ok(axum::http::StatusCode::NO_CONTENT)
@@ -241,13 +323,18 @@ pub async fn revoke_agent_platform_api_key(
pub async fn delete_agent_platform_api_key(
Path(path): Path<WorkspaceAgentPlatformApiKeyPath>,
State(state): State<AppState>,
Extension(session): Extension<AuthenticatedSession>,
Extension(request_context): Extension<RequestContext>,
) -> Result<axum::http::StatusCode, ApiError> {
let audit_context =
AdminAuditContext::from_session_and_correlation(&session, &request_context.correlation);
state
.service
.delete_agent_platform_api_key(
&path.workspace_id.as_str().into(),
&path.agent_id.as_str().into(),
&path.key_id.as_str().into(),
Some(&audit_context),
)
.await?;
Ok(axum::http::StatusCode::NO_CONTENT)
+53 -2
View File
@@ -5,18 +5,52 @@ use serde_json::json;
use crate::{
auth::{AuthenticatedSession, cleared_session_cookie, extract_session_token, session_cookie},
error::ApiError,
rate_limit::ClientIdentityBucket,
service::{
ChangePasswordPayload, LoginPayload, UpdateCurrentWorkspacePayload, UpdateProfilePayload,
ChangePasswordPayload, CompleteBootstrapPayload, LoginPayload,
UpdateCurrentWorkspacePayload, UpdateProfilePayload,
},
state::AppState,
};
pub async fn bootstrap_status(
State(state): State<AppState>,
) -> Result<Json<serde_json::Value>, ApiError> {
Ok(Json(serde_json::json!(
state.service.bootstrap_status().await?
)))
}
pub async fn complete_bootstrap(
State(state): State<AppState>,
jar: CookieJar,
Json(payload): Json<CompleteBootstrapPayload>,
) -> Result<impl IntoResponse, ApiError> {
let (session_data, session) = state.service.complete_bootstrap(payload).await?;
let cookie_value = format!(
"{}.{}",
session_data.session_id.as_str(),
session_data.value
);
let jar = jar.add(session_cookie(state.service.auth_settings(), &cookie_value));
Ok((jar, Json(serde_json::json!(session))))
}
pub async fn login(
State(state): State<AppState>,
jar: CookieJar,
client_bucket: Option<Extension<ClientIdentityBucket>>,
Json(payload): Json<LoginPayload>,
) -> Result<impl IntoResponse, ApiError> {
let (session_data, session) = state.service.login(payload).await?;
let client_bucket = client_bucket
.as_ref()
.map(|Extension(bucket)| bucket.0.as_str())
.unwrap_or("anonymous:/api/auth/login");
let (session_data, session) = state
.service
.login_with_client_bucket(payload, client_bucket)
.await?;
let cookie_value = format!(
"{}.{}",
session_data.session_id.as_str(),
@@ -54,6 +88,22 @@ pub async fn get_session(
Ok(Json(serde_json::json!(session)))
}
pub async fn refresh_session_csrf(
State(state): State<AppState>,
jar: CookieJar,
) -> Result<Json<serde_json::Value>, ApiError> {
let (session_id, session_value) = extract_session_token(&jar)
.ok_or_else(|| ApiError::unauthorized("authentication required"))?;
state
.service
.get_session(&session_id, &session_value)
.await?
.ok_or_else(|| ApiError::unauthorized("session is invalid or expired"))?;
let csrf_token = state.service.rotate_session_csrf_token(&session_id).await?;
Ok(Json(json!({ "csrf_token": csrf_token })))
}
pub async fn get_profile(
Extension(session): Extension<AuthenticatedSession>,
) -> Result<Json<serde_json::Value>, ApiError> {
@@ -73,6 +123,7 @@ pub async fn update_profile(
.service
.update_profile(
&session.user.id,
&session.session_id,
session.current_workspace_id.as_ref(),
payload,
)
+17 -3
View File
@@ -1,11 +1,17 @@
use axum::{
Json,
Extension, Json,
extract::{Path, State},
};
use serde::Deserialize;
use serde_json::{Value, json};
use crate::{error::ApiError, service::AuthProfilePayload, state::AppState};
use crate::{
auth::AuthenticatedSession,
error::ApiError,
request_context::RequestContext,
service::{AdminAuditContext, AuthProfilePayload},
state::AppState,
};
#[derive(Deserialize)]
pub struct WorkspacePath {
@@ -32,11 +38,19 @@ pub async fn list_auth_profiles(
pub async fn create_auth_profile(
Path(path): Path<WorkspacePath>,
State(state): State<AppState>,
Extension(session): Extension<AuthenticatedSession>,
Extension(request_context): Extension<RequestContext>,
Json(payload): Json<AuthProfilePayload>,
) -> Result<Json<Value>, ApiError> {
let audit_context =
AdminAuditContext::from_session_and_correlation(&session, &request_context.correlation);
let profile = state
.service
.create_auth_profile(&path.workspace_id.as_str().into(), payload)
.create_auth_profile(
&path.workspace_id.as_str().into(),
payload,
Some(&audit_context),
)
.await?;
Ok(Json(json!(profile)))
}
+81 -2
View File
@@ -1,13 +1,18 @@
use axum::{
Json,
extract::{Path, Query, State},
http::{
HeaderValue, StatusCode,
header::{CONTENT_DISPOSITION, CONTENT_TYPE},
},
response::{IntoResponse, Response},
};
use serde_json::{Value, json};
use crate::{
error::ApiError,
routes::access::WorkspacePath,
service::{ApprovalsQuery, LogsQuery, UsageRequestQuery},
service::{ApprovalDecisionPayload, ApprovalsQuery, LogsQuery, UsageRequestQuery},
state::AppState,
};
@@ -44,7 +49,49 @@ pub async fn list_logs(
.service
.list_logs(&path.workspace_id.as_str().into(), query)
.await?;
Ok(Json(json!({ "items": items })))
Ok(Json(json!(items)))
}
pub async fn export_logs_csv(
Path(path): Path<WorkspacePath>,
Query(query): Query<LogsQuery>,
State(state): State<AppState>,
) -> Result<Response, ApiError> {
let csv = state
.service
.export_logs_csv(&path.workspace_id.as_str().into(), query)
.await?;
let mut response = (StatusCode::OK, csv).into_response();
response.headers_mut().insert(
CONTENT_TYPE,
HeaderValue::from_static("text/csv; charset=utf-8"),
);
response.headers_mut().insert(
CONTENT_DISPOSITION,
HeaderValue::from_static("attachment; filename=\"crank-invocation-history.csv\""),
);
Ok(response)
}
pub async fn export_usage_csv(
Path(path): Path<WorkspacePath>,
Query(query): Query<UsageRequestQuery>,
State(state): State<AppState>,
) -> Result<Response, ApiError> {
let csv = state
.service
.export_usage_csv(&path.workspace_id.as_str().into(), query)
.await?;
let mut response = (StatusCode::OK, csv).into_response();
response.headers_mut().insert(
CONTENT_TYPE,
HeaderValue::from_static("text/csv; charset=utf-8"),
);
response.headers_mut().insert(
CONTENT_DISPOSITION,
HeaderValue::from_static("attachment; filename=\"crank-usage.csv\""),
);
Ok(response)
}
pub async fn get_log(
@@ -87,6 +134,38 @@ pub async fn get_approval(
Ok(Json(json!(item)))
}
pub async fn approve_approval(
Path(path): Path<WorkspaceApprovalPath>,
State(state): State<AppState>,
Json(payload): Json<ApprovalDecisionPayload>,
) -> Result<Json<Value>, ApiError> {
let item = state
.service
.approve_approval(
&path.workspace_id.as_str().into(),
&path.approval_id.as_str().into(),
payload,
)
.await?;
Ok(Json(json!(item)))
}
pub async fn deny_approval(
Path(path): Path<WorkspaceApprovalPath>,
State(state): State<AppState>,
Json(payload): Json<ApprovalDecisionPayload>,
) -> Result<Json<Value>, ApiError> {
let item = state
.service
.deny_approval(
&path.workspace_id.as_str().into(),
&path.approval_id.as_str().into(),
payload,
)
.await?;
Ok(Json(json!(item)))
}
pub async fn get_usage(
Path(path): Path<WorkspacePath>,
Query(query): Query<UsageRequestQuery>,
+59
View File
@@ -0,0 +1,59 @@
use axum::{
Json,
extract::{Path, State},
};
use crate::{
error::ApiError,
service::{
OnboardingEventPayload, OnboardingEventResponse, OnboardingResponse,
ResetOnboardingSelectionPayload, ResetOnboardingSelectionResponse,
},
state::AppState,
};
#[derive(serde::Deserialize)]
pub struct WorkspaceOnboardingPath {
pub workspace_id: String,
}
pub async fn reset_onboarding_selection(
Path(path): Path<WorkspaceOnboardingPath>,
State(state): State<AppState>,
Json(payload): Json<ResetOnboardingSelectionPayload>,
) -> Result<Json<ResetOnboardingSelectionResponse>, ApiError> {
Ok(Json(
state
.service
.reset_onboarding_selection(
&path.workspace_id.as_str().into(),
payload.expected_revision,
)
.await?,
))
}
pub async fn get_onboarding(
Path(path): Path<WorkspaceOnboardingPath>,
State(state): State<AppState>,
) -> Result<Json<OnboardingResponse>, ApiError> {
Ok(Json(
state
.service
.get_onboarding(&path.workspace_id.as_str().into())
.await?,
))
}
pub async fn record_onboarding_event(
Path(path): Path<WorkspaceOnboardingPath>,
State(state): State<AppState>,
Json(payload): Json<OnboardingEventPayload>,
) -> Result<Json<OnboardingEventResponse>, ApiError> {
Ok(Json(
state
.service
.record_onboarding_event(&path.workspace_id.as_str().into(), payload)
.await?,
))
}
+102 -7
View File
@@ -1,9 +1,10 @@
use axum::{
Json,
extract::{Extension, Path, Query, State},
extract::{Extension, Path, Query, State, rejection::StringRejection},
http::{HeaderMap, StatusCode, header},
response::IntoResponse,
};
use crank_registry::OperationStateExpectation;
use serde::Deserialize;
use serde_json::{Value, json};
@@ -79,7 +80,7 @@ pub async fn analyze_operation_quality(
pub async fn get_operation(
Path(path): Path<WorkspaceOperationPath>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
) -> Result<impl IntoResponse, ApiError> {
let operation = state
.service
.get_operation(
@@ -87,20 +88,30 @@ pub async fn get_operation(
&path.operation_id.as_str().into(),
)
.await?;
Ok(Json(json!(operation)))
let etag = crate::service::AdminService::operation_state_etag(&operation);
let mut headers = HeaderMap::new();
headers.insert(
header::ETAG,
header::HeaderValue::from_str(&etag)
.map_err(|_| ApiError::internal("invalid operation etag"))?,
);
Ok((headers, Json(json!(operation))))
}
pub async fn update_operation(
Path(path): Path<WorkspaceOperationPath>,
State(state): State<AppState>,
headers: HeaderMap,
Json(payload): Json<UpdateOperationPayload>,
) -> Result<Json<Value>, ApiError> {
let expected = require_operation_precondition(&state, &path, &headers).await?;
let result = state
.service
.update_operation(
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
payload,
&expected,
)
.await?;
Ok(Json(json!(result)))
@@ -109,12 +120,15 @@ pub async fn update_operation(
pub async fn delete_operation(
Path(path): Path<WorkspaceOperationPath>,
State(state): State<AppState>,
headers: HeaderMap,
) -> Result<Json<Value>, ApiError> {
let expected = require_operation_precondition(&state, &path, &headers).await?;
let result = state
.service
.delete_operation(
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
&expected,
)
.await?;
Ok(Json(json!(result)))
@@ -123,7 +137,7 @@ pub async fn delete_operation(
pub async fn get_operation_version(
Path(path): Path<WorkspaceOperationVersionPath>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
) -> Result<impl IntoResponse, ApiError> {
let version = state
.service
.get_operation_version(
@@ -132,20 +146,30 @@ pub async fn get_operation_version(
path.version,
)
.await?;
Ok(Json(json!(version)))
let etag = crate::service::AdminService::operation_version_etag(&version)?;
let mut headers = HeaderMap::new();
headers.insert(
header::ETAG,
header::HeaderValue::from_str(&etag)
.map_err(|_| ApiError::internal("invalid operation version etag"))?,
);
Ok((headers, Json(json!(version))))
}
pub async fn create_version(
Path(path): Path<WorkspaceOperationPath>,
State(state): State<AppState>,
headers: HeaderMap,
Json(payload): Json<NewVersionPayload>,
) -> Result<Json<Value>, ApiError> {
let expected = require_operation_precondition(&state, &path, &headers).await?;
let created = state
.service
.create_version(
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
payload,
&expected,
)
.await?;
Ok(Json(json!(created)))
@@ -154,14 +178,17 @@ pub async fn create_version(
pub async fn publish_operation(
Path(path): Path<WorkspaceOperationPath>,
State(state): State<AppState>,
headers: HeaderMap,
Json(payload): Json<PublishPayload>,
) -> Result<Json<Value>, ApiError> {
let expected = require_operation_precondition(&state, &path, &headers).await?;
let published = state
.service
.publish_operation(
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
payload.version,
&expected,
)
.await?;
Ok(Json(json!(published)))
@@ -170,17 +197,63 @@ pub async fn publish_operation(
pub async fn archive_operation(
Path(path): Path<WorkspaceOperationPath>,
State(state): State<AppState>,
headers: HeaderMap,
) -> Result<Json<Value>, ApiError> {
let expected = require_operation_precondition(&state, &path, &headers).await?;
let archived = state
.service
.archive_operation(
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
&expected,
)
.await?;
Ok(Json(json!(archived)))
}
async fn require_operation_precondition(
state: &AppState,
path: &WorkspaceOperationPath,
headers: &HeaderMap,
) -> Result<OperationStateExpectation, ApiError> {
let detail = state
.service
.get_operation(
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
)
.await?;
let expected = crate::service::AdminService::operation_state_etag(&detail);
let provided = headers
.get(header::IF_MATCH)
.and_then(|value| value.to_str().ok())
.ok_or_else(|| {
ApiError::precondition_required_with_context(
"If-Match is required for Operation mutation",
json!({
"error_code": "operation_precondition_required",
"current_version": detail.current_draft_version,
"recovery": "reload"
}),
)
})?;
if provided != expected {
return Err(ApiError::conflict_with_context(
"Operation state changed; reload before retrying",
json!({
"error_code": "operation_stale_version",
"current_version": detail.current_draft_version,
"recovery": "reload"
}),
));
}
Ok(OperationStateExpectation {
current_draft_version: detail.current_draft_version,
status: detail.status,
latest_published_version: detail.latest_published_version,
})
}
pub async fn run_test(
Path(path): Path<WorkspaceOperationPath>,
State(state): State<AppState>,
@@ -285,11 +358,33 @@ pub async fn import_operation(
Path(path): Path<WorkspacePath>,
Query(query): Query<ImportQuery>,
State(state): State<AppState>,
body: String,
headers: HeaderMap,
body: Result<String, StringRejection>,
) -> Result<Json<Value>, ApiError> {
let body = body.map_err(|rejection| match rejection {
StringRejection::InvalidUtf8(_) => ApiError::unprocessable_with_context(
"operation yaml is not valid UTF-8",
json!({ "error_code": "operation_yaml_invalid" }),
),
StringRejection::FailedToBufferBody(_) => ApiError::payload_too_large_with_context(
"operation yaml exceeds the 256 KiB limit",
json!({ "error_code": "operation_yaml_too_large" }),
),
_ => ApiError::unprocessable_with_context(
"operation yaml body is invalid",
json!({ "error_code": "operation_yaml_invalid" }),
),
})?;
let imported = state
.service
.import_operation(&path.workspace_id.as_str().into(), query, &body)
.import_operation(
&path.workspace_id.as_str().into(),
query,
&body,
headers
.get(header::IF_MATCH)
.and_then(|value| value.to_str().ok()),
)
.await?;
Ok(Json(json!(imported)))
}
+15 -1
View File
@@ -8,7 +8,8 @@ use serde_json::{Value, json};
use crate::{
auth::AuthenticatedSession,
error::ApiError,
service::{RotateSecretPayload, SecretPayload},
request_context::RequestContext,
service::{AdminAuditContext, RotateSecretPayload, SecretPayload},
state::AppState,
};
@@ -38,14 +39,18 @@ pub async fn create_secret(
Path(path): Path<WorkspacePath>,
State(state): State<AppState>,
Extension(session): Extension<AuthenticatedSession>,
Extension(request_context): Extension<RequestContext>,
Json(payload): Json<SecretPayload>,
) -> Result<Json<Value>, ApiError> {
let audit_context =
AdminAuditContext::from_session_and_correlation(&session, &request_context.correlation);
let secret = state
.service
.create_secret(
&path.workspace_id.as_str().into(),
Some(&session.user.id),
payload,
Some(&audit_context),
)
.await?;
Ok(Json(json!(secret)))
@@ -69,8 +74,11 @@ pub async fn rotate_secret(
Path(path): Path<WorkspaceSecretPath>,
State(state): State<AppState>,
Extension(session): Extension<AuthenticatedSession>,
Extension(request_context): Extension<RequestContext>,
Json(payload): Json<RotateSecretPayload>,
) -> Result<Json<Value>, ApiError> {
let audit_context =
AdminAuditContext::from_session_and_correlation(&session, &request_context.correlation);
let secret = state
.service
.rotate_secret(
@@ -78,6 +86,7 @@ pub async fn rotate_secret(
&path.secret_id.as_str().into(),
Some(&session.user.id),
payload,
Some(&audit_context),
)
.await?;
Ok(Json(json!(secret)))
@@ -86,12 +95,17 @@ pub async fn rotate_secret(
pub async fn delete_secret(
Path(path): Path<WorkspaceSecretPath>,
State(state): State<AppState>,
Extension(session): Extension<AuthenticatedSession>,
Extension(request_context): Extension<RequestContext>,
) -> Result<Json<Value>, ApiError> {
let audit_context =
AdminAuditContext::from_session_and_correlation(&session, &request_context.correlation);
state
.service
.delete_secret(
&path.workspace_id.as_str().into(),
&path.secret_id.as_str().into(),
Some(&audit_context),
)
.await?;
Ok(Json(json!({ "ok": true })))