chore: publish clean community baseline
Deploy / deploy (push) Successful in 30s
CI / Rust Checks (push) Successful in 4m59s
CI / UI Checks (push) Successful in 5s
CI / Deployment Manifests (push) Successful in 3s
CI / Frontend E2E (push) Successful in 4m8s

This commit is contained in:
github-ops
2026-06-17 06:15:46 +00:00
commit b546063998
307 changed files with 71048 additions and 0 deletions
+287
View File
@@ -0,0 +1,287 @@
use axum::{
Extension, Json,
extract::{Path, State},
http::StatusCode,
};
use crank_core::{
AuditActor, AuditEvent, AuditEventId, AuditTarget, AuditTargetKind, PolicyAction,
PolicyDecision, PolicyScope, SessionActor,
};
use serde::Deserialize;
use serde_json::{Value, json};
use time::OffsetDateTime;
use uuid::Uuid;
use crate::{
auth::AuthenticatedSession,
error::ApiError,
service::{InvitationPayload, UpdateMembershipPayload},
state::AppState,
};
#[derive(Deserialize)]
pub struct WorkspacePath {
pub workspace_id: String,
}
#[derive(Deserialize)]
pub struct WorkspaceInvitationPath {
pub workspace_id: String,
pub invitation_id: String,
}
#[derive(Deserialize)]
pub struct WorkspaceMembershipPath {
pub workspace_id: String,
pub user_id: String,
}
pub async fn list_memberships(
Path(path): Path<WorkspacePath>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let items = state
.service
.list_memberships(&path.workspace_id.as_str().into())
.await?;
Ok(Json(json!({ "items": items })))
}
pub async fn update_membership(
Path(path): Path<WorkspaceMembershipPath>,
State(state): State<AppState>,
Extension(session): Extension<AuthenticatedSession>,
Json(payload): Json<UpdateMembershipPayload>,
) -> Result<Json<Value>, ApiError> {
let workspace_id: crank_core::WorkspaceId = path.workspace_id.as_str().into();
enforce_workspace_policy(
&state,
&session,
&workspace_id,
PolicyAction::WriteWorkspaceAccess,
)?;
let items = state
.service
.update_membership_role(
&workspace_id,
&session.user.id,
&path.user_id.as_str().into(),
payload,
)
.await?;
record_access_audit(
&state,
&session,
&workspace_id,
"membership.role_updated",
AuditTargetKind::Membership,
path.user_id.clone(),
json!({ "user_id": path.user_id }),
)
.await?;
Ok(Json(json!({ "items": items })))
}
pub async fn delete_membership(
Path(path): Path<WorkspaceMembershipPath>,
State(state): State<AppState>,
Extension(session): Extension<AuthenticatedSession>,
) -> Result<StatusCode, ApiError> {
let workspace_id: crank_core::WorkspaceId = path.workspace_id.as_str().into();
enforce_workspace_policy(
&state,
&session,
&workspace_id,
PolicyAction::WriteWorkspaceAccess,
)?;
state
.service
.remove_membership(
&workspace_id,
&session.user.id,
&path.user_id.as_str().into(),
)
.await?;
record_access_audit(
&state,
&session,
&workspace_id,
"membership.removed",
AuditTargetKind::Membership,
path.user_id.clone(),
json!({ "user_id": path.user_id }),
)
.await?;
Ok(StatusCode::NO_CONTENT)
}
pub async fn list_invitations(
Path(path): Path<WorkspacePath>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let items = state
.service
.list_invitations(&path.workspace_id.as_str().into())
.await?;
Ok(Json(json!({ "items": items })))
}
pub async fn create_invitation(
Path(path): Path<WorkspacePath>,
State(state): State<AppState>,
Extension(session): Extension<AuthenticatedSession>,
Json(payload): Json<InvitationPayload>,
) -> Result<Json<Value>, ApiError> {
let workspace_id: crank_core::WorkspaceId = path.workspace_id.as_str().into();
enforce_workspace_policy(
&state,
&session,
&workspace_id,
PolicyAction::WriteWorkspaceAccess,
)?;
let created = state
.service
.create_invitation(&workspace_id, payload)
.await?;
record_access_audit(
&state,
&session,
&workspace_id,
"invitation.created",
AuditTargetKind::Invitation,
created.invitation.invitation.id.as_str().to_owned(),
json!({ "invitation_id": created.invitation.invitation.id.as_str() }),
)
.await?;
Ok(Json(json!(created)))
}
pub async fn delete_invitation(
Path(path): Path<WorkspaceInvitationPath>,
State(state): State<AppState>,
Extension(session): Extension<AuthenticatedSession>,
) -> Result<StatusCode, ApiError> {
let workspace_id: crank_core::WorkspaceId = path.workspace_id.as_str().into();
enforce_workspace_policy(
&state,
&session,
&workspace_id,
PolicyAction::WriteWorkspaceAccess,
)?;
state
.service
.delete_invitation(&workspace_id, &path.invitation_id.as_str().into())
.await?;
record_access_audit(
&state,
&session,
&workspace_id,
"invitation.deleted",
AuditTargetKind::Invitation,
path.invitation_id.clone(),
json!({ "invitation_id": path.invitation_id }),
)
.await?;
Ok(StatusCode::NO_CONTENT)
}
pub async fn export_workspace(
Path(path): Path<WorkspacePath>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let exported = state
.service
.export_workspace(&path.workspace_id.as_str().into())
.await?;
Ok(Json(json!(exported)))
}
pub async fn delete_workspace(
Path(path): Path<WorkspacePath>,
State(state): State<AppState>,
Extension(session): Extension<AuthenticatedSession>,
) -> Result<StatusCode, ApiError> {
let workspace_id: crank_core::WorkspaceId = path.workspace_id.as_str().into();
enforce_workspace_policy(
&state,
&session,
&workspace_id,
PolicyAction::WriteWorkspace,
)?;
state
.service
.delete_workspace(&workspace_id, &session.user.id)
.await?;
record_access_audit(
&state,
&session,
&workspace_id,
"workspace.deleted",
AuditTargetKind::Workspace,
workspace_id.as_str().to_owned(),
json!({ "workspace_id": workspace_id.as_str() }),
)
.await?;
Ok(StatusCode::NO_CONTENT)
}
fn enforce_workspace_policy(
state: &AppState,
session: &AuthenticatedSession,
workspace_id: &crank_core::WorkspaceId,
action: PolicyAction,
) -> Result<(), ApiError> {
let membership = session
.memberships
.iter()
.find(|membership| membership.workspace.id == *workspace_id)
.ok_or_else(|| ApiError::forbidden("workspace access denied"))?;
let actor = SessionActor {
user_id: session.user.id.clone(),
workspace_id: workspace_id.clone(),
role: membership.role,
};
match state.service.policy_engine().check(
&actor,
action,
PolicyScope::Workspace(workspace_id.clone()),
) {
PolicyDecision::Allow => Ok(()),
PolicyDecision::Deny { reason } => Err(ApiError::forbidden(reason)),
}
}
async fn record_access_audit(
state: &AppState,
session: &AuthenticatedSession,
workspace_id: &crank_core::WorkspaceId,
action: &str,
target_kind: AuditTargetKind,
target_id: String,
payload: Value,
) -> Result<(), ApiError> {
state
.service
.audit_sink()
.record(AuditEvent {
id: AuditEventId::new(format!("audit_{}", Uuid::now_v7().simple())),
occurred_at: OffsetDateTime::now_utc(),
actor: AuditActor {
user_id: session.user.id.clone(),
email: session.user.email.clone(),
session_id: Some(session.session_id.clone()),
},
action: action.to_owned(),
target: AuditTarget {
workspace_id: workspace_id.clone(),
kind: target_kind,
id: target_id,
},
payload,
source_ip: None,
user_agent: None,
})
.await
.map_err(|error| ApiError::internal(format!("failed to record audit event: {error}")))
}
+242
View File
@@ -0,0 +1,242 @@
use axum::{
Json,
extract::{Path, State},
};
use serde::Deserialize;
use serde_json::{Value, json};
use crate::{
error::ApiError,
service::{
AgentBindingPayload, AgentPayload, PlatformApiKeyPayload, PublishPayload,
UpdateAgentPayload,
},
state::AppState,
};
#[derive(Deserialize)]
pub struct WorkspacePath {
pub workspace_id: String,
}
#[derive(Deserialize)]
pub struct WorkspaceAgentPath {
pub workspace_id: String,
pub agent_id: String,
}
#[derive(Deserialize)]
pub struct WorkspaceAgentVersionPath {
pub workspace_id: String,
pub agent_id: String,
pub version: u32,
}
#[derive(Deserialize)]
pub struct WorkspaceAgentPlatformApiKeyPath {
pub workspace_id: String,
pub agent_id: String,
pub key_id: String,
}
pub async fn list_agents(
Path(path): Path<WorkspacePath>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let items = state
.service
.list_agents(&path.workspace_id.as_str().into())
.await?;
Ok(Json(json!({ "items": items })))
}
pub async fn create_agent(
Path(path): Path<WorkspacePath>,
State(state): State<AppState>,
Json(payload): Json<AgentPayload>,
) -> Result<Json<Value>, ApiError> {
let created = state
.service
.create_agent(&path.workspace_id.as_str().into(), payload)
.await?;
Ok(Json(json!(created)))
}
pub async fn get_agent(
Path(path): Path<WorkspaceAgentPath>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let agent = state
.service
.get_agent(
&path.workspace_id.as_str().into(),
&path.agent_id.as_str().into(),
)
.await?;
Ok(Json(json!(agent)))
}
pub async fn update_agent(
Path(path): Path<WorkspaceAgentPath>,
State(state): State<AppState>,
Json(payload): Json<UpdateAgentPayload>,
) -> Result<Json<Value>, ApiError> {
let updated = state
.service
.update_agent(
&path.workspace_id.as_str().into(),
&path.agent_id.as_str().into(),
payload,
)
.await?;
Ok(Json(json!(updated)))
}
pub async fn delete_agent(
Path(path): Path<WorkspaceAgentPath>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let deleted = state
.service
.delete_agent(
&path.workspace_id.as_str().into(),
&path.agent_id.as_str().into(),
)
.await?;
Ok(Json(json!(deleted)))
}
pub async fn get_agent_version(
Path(path): Path<WorkspaceAgentVersionPath>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let version = state
.service
.get_agent_version(
&path.workspace_id.as_str().into(),
&path.agent_id.as_str().into(),
path.version,
)
.await?;
Ok(Json(json!(version)))
}
pub async fn save_agent_bindings(
Path(path): Path<WorkspaceAgentPath>,
State(state): State<AppState>,
Json(payload): Json<Vec<AgentBindingPayload>>,
) -> Result<Json<Value>, ApiError> {
let record = state
.service
.save_agent_bindings(
&path.workspace_id.as_str().into(),
&path.agent_id.as_str().into(),
payload,
)
.await?;
Ok(Json(json!(record)))
}
pub async fn publish_agent(
Path(path): Path<WorkspaceAgentPath>,
State(state): State<AppState>,
Json(payload): Json<PublishPayload>,
) -> Result<Json<Value>, ApiError> {
let published = state
.service
.publish_agent(
&path.workspace_id.as_str().into(),
&path.agent_id.as_str().into(),
payload.version,
)
.await?;
Ok(Json(json!(published)))
}
pub async fn unpublish_agent(
Path(path): Path<WorkspaceAgentPath>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let updated = state
.service
.unpublish_agent(
&path.workspace_id.as_str().into(),
&path.agent_id.as_str().into(),
)
.await?;
Ok(Json(json!(updated)))
}
pub async fn archive_agent(
Path(path): Path<WorkspaceAgentPath>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let updated = state
.service
.archive_agent(
&path.workspace_id.as_str().into(),
&path.agent_id.as_str().into(),
)
.await?;
Ok(Json(json!(updated)))
}
pub async fn list_agent_platform_api_keys(
Path(path): Path<WorkspaceAgentPath>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let items = state
.service
.list_agent_platform_api_keys(
&path.workspace_id.as_str().into(),
&path.agent_id.as_str().into(),
)
.await?;
Ok(Json(json!({ "items": items })))
}
pub async fn create_agent_platform_api_key(
Path(path): Path<WorkspaceAgentPath>,
State(state): State<AppState>,
Json(payload): Json<PlatformApiKeyPayload>,
) -> Result<Json<Value>, ApiError> {
let created = state
.service
.create_agent_platform_api_key(
&path.workspace_id.as_str().into(),
&path.agent_id.as_str().into(),
payload,
)
.await?;
Ok(Json(json!(created)))
}
pub async fn revoke_agent_platform_api_key(
Path(path): Path<WorkspaceAgentPlatformApiKeyPath>,
State(state): State<AppState>,
) -> Result<axum::http::StatusCode, ApiError> {
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(),
)
.await?;
Ok(axum::http::StatusCode::NO_CONTENT)
}
pub async fn delete_agent_platform_api_key(
Path(path): Path<WorkspaceAgentPlatformApiKeyPath>,
State(state): State<AppState>,
) -> Result<axum::http::StatusCode, ApiError> {
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(),
)
.await?;
Ok(axum::http::StatusCode::NO_CONTENT)
}
+109
View File
@@ -0,0 +1,109 @@
use axum::{Extension, Json, extract::State, http::StatusCode, response::IntoResponse};
use axum_extra::extract::cookie::CookieJar;
use serde_json::json;
use crate::{
auth::{AuthenticatedSession, cleared_session_cookie, extract_session_token, session_cookie},
error::ApiError,
service::{
ChangePasswordPayload, LoginPayload, UpdateCurrentWorkspacePayload, UpdateProfilePayload,
},
state::AppState,
};
pub async fn login(
State(state): State<AppState>,
jar: CookieJar,
Json(payload): Json<LoginPayload>,
) -> Result<impl IntoResponse, ApiError> {
let (session_data, session) = state.service.login(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 logout(
State(state): State<AppState>,
jar: CookieJar,
) -> Result<impl IntoResponse, ApiError> {
if let Some((session_id, session_value)) = extract_session_token(&jar) {
state.service.logout(&session_id, &session_value).await?;
}
let jar = jar.remove(cleared_session_cookie(state.service.auth_settings()));
Ok((jar, StatusCode::NO_CONTENT))
}
pub async fn get_session(
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"))?;
let session = state
.service
.session_response(&session_id, &session_value)
.await?
.ok_or_else(|| ApiError::unauthorized("session is invalid or expired"))?;
Ok(Json(serde_json::json!(session)))
}
pub async fn get_profile(
Extension(session): Extension<AuthenticatedSession>,
) -> Result<Json<serde_json::Value>, ApiError> {
Ok(Json(json!({
"user": session.user,
"memberships": session.memberships,
"current_workspace_id": session.current_workspace_id
})))
}
pub async fn update_profile(
State(state): State<AppState>,
Extension(session): Extension<AuthenticatedSession>,
Json(payload): Json<UpdateProfilePayload>,
) -> Result<Json<serde_json::Value>, ApiError> {
let updated = state
.service
.update_profile(
&session.user.id,
session.current_workspace_id.as_ref(),
payload,
)
.await?;
Ok(Json(json!(updated)))
}
pub async fn update_current_workspace(
State(state): State<AppState>,
Extension(session): Extension<AuthenticatedSession>,
Json(payload): Json<UpdateCurrentWorkspacePayload>,
) -> Result<Json<serde_json::Value>, ApiError> {
let updated = state
.service
.set_current_workspace(
&session.session_id,
&session.user.id,
&payload.workspace_id.as_str().into(),
)
.await?;
Ok(Json(json!(updated)))
}
pub async fn change_password(
State(state): State<AppState>,
Extension(session): Extension<AuthenticatedSession>,
Json(payload): Json<ChangePasswordPayload>,
) -> Result<StatusCode, ApiError> {
state
.service
.change_password(&session.user.id, payload)
.await?;
Ok(StatusCode::NO_CONTENT)
}
@@ -0,0 +1,56 @@
use axum::{
Json,
extract::{Path, State},
};
use serde::Deserialize;
use serde_json::{Value, json};
use crate::{error::ApiError, service::AuthProfilePayload, state::AppState};
#[derive(Deserialize)]
pub struct WorkspacePath {
pub workspace_id: String,
}
#[derive(Deserialize)]
pub struct WorkspaceAuthProfilePath {
pub workspace_id: String,
pub auth_profile_id: String,
}
pub async fn list_auth_profiles(
Path(path): Path<WorkspacePath>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let items = state
.service
.list_auth_profiles(&path.workspace_id.as_str().into())
.await?;
Ok(Json(json!({ "items": items })))
}
pub async fn create_auth_profile(
Path(path): Path<WorkspacePath>,
State(state): State<AppState>,
Json(payload): Json<AuthProfilePayload>,
) -> Result<Json<Value>, ApiError> {
let profile = state
.service
.create_auth_profile(&path.workspace_id.as_str().into(), payload)
.await?;
Ok(Json(json!(profile)))
}
pub async fn get_auth_profile(
Path(path): Path<WorkspaceAuthProfilePath>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let profile = state
.service
.get_auth_profile(
&path.workspace_id.as_str().into(),
&path.auth_profile_id.as_str().into(),
)
.await?;
Ok(Json(json!(profile)))
}
@@ -0,0 +1,8 @@
use axum::{Json, extract::State};
use serde_json::json;
use crate::state::AppState;
pub async fn get_capabilities(State(state): State<AppState>) -> Json<serde_json::Value> {
Json(json!(state.service.capability_profile().capabilities()))
}
+124
View File
@@ -0,0 +1,124 @@
use axum::{Json, extract::State};
use crank_core::{
IssueAgentTokenRequest, IssueOneTimeAgentTokenRequest, MachineAccessMode, MembershipRole,
TokenIssuerActor, TokenIssuerError, UserId, WorkspaceId,
};
use serde_json::json;
use crate::{error::ApiError, state::AppState};
pub async fn issue_agent_token(
State(state): State<AppState>,
Json(payload): Json<IssueAgentTokenRequest>,
) -> Result<Json<serde_json::Value>, ApiError> {
let edition = state.service.capability_profile().capabilities().edition;
let grant_type = payload.grant_type.clone();
let response = state
.service
.token_issuer()
.issue_short_lived(payload, &community_token_issuer_actor())
.await
.map_err(|error| {
map_token_issuer_error(
error,
edition,
MachineAccessMode::ShortLivedToken,
json!({
"grant_type": grant_type,
"upgrade_required": true,
}),
)
})?;
Ok(Json(serde_json::json!(response)))
}
pub async fn issue_one_time_agent_token(
State(state): State<AppState>,
Json(payload): Json<IssueOneTimeAgentTokenRequest>,
) -> Result<Json<serde_json::Value>, ApiError> {
let edition = state.service.capability_profile().capabilities().edition;
let operation_id = payload.operation_id.as_str().to_owned();
let response = state
.service
.token_issuer()
.issue_one_time(payload, &community_token_issuer_actor())
.await
.map_err(|error| {
map_token_issuer_error(
error,
edition,
MachineAccessMode::OneTimeToken,
json!({
"operation_id": operation_id,
"upgrade_required": true,
}),
)
})?;
Ok(Json(serde_json::json!(response)))
}
fn community_token_issuer_actor() -> TokenIssuerActor {
TokenIssuerActor {
user_id: UserId::new("user_community_public"),
workspace_id: WorkspaceId::new("ws_community_public"),
role: MembershipRole::Viewer,
}
}
fn map_token_issuer_error(
error: TokenIssuerError,
edition: crank_core::ProductEdition,
machine_access_mode: MachineAccessMode,
extra_context: serde_json::Value,
) -> ApiError {
match error {
TokenIssuerError::NotSupportedInEdition => ApiError::forbidden_with_context(
match machine_access_mode {
MachineAccessMode::ShortLivedToken => {
"short-lived machine access is not available in Community"
}
MachineAccessMode::OneTimeToken => {
"one-time machine access is not available in Community"
}
MachineAccessMode::StaticAgentKey => {
"static agent key machine access is not available for token issue"
}
},
merge_machine_auth_context(edition, machine_access_mode, extra_context),
),
TokenIssuerError::InvalidGrant(reason) => ApiError::validation_with_context(
"invalid machine token grant",
json!({ "reason": reason }),
),
TokenIssuerError::AgentKeyUnknown => ApiError::validation("agent key is unknown"),
TokenIssuerError::OperationNotStrict => ApiError::validation("operation is not strict"),
TokenIssuerError::OperationNotPublishedForAgent => {
ApiError::validation("operation is not published for agent")
}
TokenIssuerError::RegistryFailure(details) => {
ApiError::internal(format!("token issuer registry failure: {details}"))
}
TokenIssuerError::ReplayGuardFailure(details) => {
ApiError::internal(format!("token issuer replay guard failure: {details}"))
}
}
}
fn merge_machine_auth_context(
edition: crank_core::ProductEdition,
machine_access_mode: MachineAccessMode,
extra_context: serde_json::Value,
) -> serde_json::Value {
let mut context = json!({
"edition": edition,
"machine_access_mode": machine_access_mode,
});
if let (Some(base), Some(extra)) = (context.as_object_mut(), extra_context.as_object()) {
for (key, value) in extra {
base.insert(key.clone(), value.clone());
}
}
context
}
+100
View File
@@ -0,0 +1,100 @@
use axum::{
Json,
extract::{Path, Query, State},
};
use serde_json::{Value, json};
use crate::{
error::ApiError,
routes::access::WorkspacePath,
service::{LogsQuery, UsageRequestQuery},
state::AppState,
};
#[derive(serde::Deserialize)]
pub struct WorkspaceLogPath {
pub workspace_id: String,
pub log_id: String,
}
#[derive(serde::Deserialize)]
pub struct WorkspaceOperationUsagePath {
pub workspace_id: String,
pub operation_id: String,
}
#[derive(serde::Deserialize)]
pub struct WorkspaceAgentUsagePath {
pub workspace_id: String,
pub agent_id: String,
}
pub async fn list_logs(
Path(path): Path<WorkspacePath>,
Query(query): Query<LogsQuery>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let items = state
.service
.list_logs(&path.workspace_id.as_str().into(), query)
.await?;
Ok(Json(json!({ "items": items })))
}
pub async fn get_log(
Path(path): Path<WorkspaceLogPath>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let item = state
.service
.get_log(
&path.workspace_id.as_str().into(),
&path.log_id.as_str().into(),
)
.await?;
Ok(Json(json!(item)))
}
pub async fn get_usage(
Path(path): Path<WorkspacePath>,
Query(query): Query<UsageRequestQuery>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let usage = state
.service
.get_usage_overview(&path.workspace_id.as_str().into(), query)
.await?;
Ok(Json(json!(usage)))
}
pub async fn get_operation_usage(
Path(path): Path<WorkspaceOperationUsagePath>,
Query(query): Query<UsageRequestQuery>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let usage = state
.service
.get_operation_usage(
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
query,
)
.await?;
Ok(Json(json!(usage)))
}
pub async fn get_agent_usage(
Path(path): Path<WorkspaceAgentUsagePath>,
Query(query): Query<UsageRequestQuery>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let usage = state
.service
.get_agent_usage(
&path.workspace_id.as_str().into(),
&path.agent_id.as_str().into(),
query,
)
.await?;
Ok(Json(json!(usage)))
}
+283
View File
@@ -0,0 +1,283 @@
use axum::{
Json,
extract::{Extension, Path, Query, State},
http::{HeaderMap, StatusCode, header},
response::IntoResponse,
};
use serde::Deserialize;
use serde_json::{Value, json};
use crate::{
error::ApiError,
request_context::RequestContext,
service::{
ExportQuery, GenerateDraftPayload, ImportQuery, NewVersionPayload, OperationPayload,
PublishPayload, TestRunPayload, UpdateOperationPayload,
},
state::AppState,
};
#[derive(Deserialize)]
pub struct WorkspacePath {
pub workspace_id: String,
}
#[derive(Deserialize)]
pub struct WorkspaceOperationPath {
pub workspace_id: String,
pub operation_id: String,
}
#[derive(Deserialize)]
pub struct WorkspaceOperationVersionPath {
pub workspace_id: String,
pub operation_id: String,
pub version: u32,
}
pub async fn list_operations(
Path(path): Path<WorkspacePath>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let items = state
.service
.list_operations(&path.workspace_id.as_str().into())
.await?;
let total = items.len();
Ok(Json(json!({
"items": items,
"page": 1,
"page_size": total,
"total": total
})))
}
pub async fn create_operation(
Path(path): Path<WorkspacePath>,
State(state): State<AppState>,
Json(payload): Json<OperationPayload>,
) -> Result<Json<Value>, ApiError> {
let created = state
.service
.create_operation(&path.workspace_id.as_str().into(), payload)
.await?;
Ok(Json(json!(created)))
}
pub async fn get_operation(
Path(path): Path<WorkspaceOperationPath>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let operation = state
.service
.get_operation(
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
)
.await?;
Ok(Json(json!(operation)))
}
pub async fn update_operation(
Path(path): Path<WorkspaceOperationPath>,
State(state): State<AppState>,
Json(payload): Json<UpdateOperationPayload>,
) -> Result<Json<Value>, ApiError> {
let result = state
.service
.update_operation(
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
payload,
)
.await?;
Ok(Json(json!(result)))
}
pub async fn delete_operation(
Path(path): Path<WorkspaceOperationPath>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let result = state
.service
.delete_operation(
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
)
.await?;
Ok(Json(json!(result)))
}
pub async fn get_operation_version(
Path(path): Path<WorkspaceOperationVersionPath>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let version = state
.service
.get_operation_version(
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
path.version,
)
.await?;
Ok(Json(json!(version)))
}
pub async fn create_version(
Path(path): Path<WorkspaceOperationPath>,
State(state): State<AppState>,
Json(payload): Json<NewVersionPayload>,
) -> Result<Json<Value>, ApiError> {
let created = state
.service
.create_version(
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
payload,
)
.await?;
Ok(Json(json!(created)))
}
pub async fn publish_operation(
Path(path): Path<WorkspaceOperationPath>,
State(state): State<AppState>,
Json(payload): Json<PublishPayload>,
) -> Result<Json<Value>, ApiError> {
let published = state
.service
.publish_operation(
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
payload.version,
)
.await?;
Ok(Json(json!(published)))
}
pub async fn archive_operation(
Path(path): Path<WorkspaceOperationPath>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let archived = state
.service
.archive_operation(
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
)
.await?;
Ok(Json(json!(archived)))
}
pub async fn run_test(
Path(path): Path<WorkspaceOperationPath>,
State(state): State<AppState>,
Extension(request_context): Extension<RequestContext>,
Json(payload): Json<TestRunPayload>,
) -> Result<Json<Value>, ApiError> {
let result = state
.service
.run_test(
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
payload,
&request_context.request_id,
)
.await?;
Ok(Json(json!(result)))
}
pub async fn upload_input_json(
Path(path): Path<WorkspaceOperationPath>,
State(state): State<AppState>,
Json(payload): Json<Value>,
) -> Result<Json<Value>, ApiError> {
let sample = state
.service
.save_json_sample(
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
crank_registry::SampleKind::InputJson,
&payload,
)
.await?;
Ok(Json(json!({
"sample_id": sample.id.as_str(),
"sample_kind": sample.sample_kind,
"version": sample.version
})))
}
pub async fn upload_output_json(
Path(path): Path<WorkspaceOperationPath>,
State(state): State<AppState>,
Json(payload): Json<Value>,
) -> Result<Json<Value>, ApiError> {
let sample = state
.service
.save_json_sample(
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
crank_registry::SampleKind::OutputJson,
&payload,
)
.await?;
Ok(Json(json!({
"sample_id": sample.id.as_str(),
"sample_kind": sample.sample_kind,
"version": sample.version
})))
}
pub async fn generate_draft(
Path(path): Path<WorkspaceOperationPath>,
State(state): State<AppState>,
Json(payload): Json<GenerateDraftPayload>,
) -> Result<Json<Value>, ApiError> {
let draft = state
.service
.generate_draft(
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
payload,
)
.await?;
Ok(Json(json!(draft)))
}
pub async fn export_operation(
Path(path): Path<WorkspaceOperationPath>,
Query(query): Query<ExportQuery>,
State(state): State<AppState>,
) -> Result<impl IntoResponse, ApiError> {
let yaml = state
.service
.export_operation(
&path.workspace_id.as_str().into(),
&path.operation_id.as_str().into(),
query,
)
.await?;
let mut headers = HeaderMap::new();
headers.insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static("application/yaml"),
);
Ok((StatusCode::OK, headers, yaml))
}
pub async fn import_operation(
Path(path): Path<WorkspacePath>,
Query(query): Query<ImportQuery>,
State(state): State<AppState>,
body: String,
) -> Result<Json<Value>, ApiError> {
let imported = state
.service
.import_operation(&path.workspace_id.as_str().into(), query, &body)
.await?;
Ok(Json(json!(imported)))
}
+98
View File
@@ -0,0 +1,98 @@
use axum::{
Extension, Json,
extract::{Path, State},
};
use serde::Deserialize;
use serde_json::{Value, json};
use crate::{
auth::AuthenticatedSession,
error::ApiError,
service::{RotateSecretPayload, SecretPayload},
state::AppState,
};
#[derive(Deserialize)]
pub struct WorkspacePath {
pub workspace_id: String,
}
#[derive(Deserialize)]
pub struct WorkspaceSecretPath {
pub workspace_id: String,
pub secret_id: String,
}
pub async fn list_secrets(
Path(path): Path<WorkspacePath>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let items = state
.service
.list_secrets(&path.workspace_id.as_str().into())
.await?;
Ok(Json(json!({ "items": items })))
}
pub async fn create_secret(
Path(path): Path<WorkspacePath>,
State(state): State<AppState>,
Extension(session): Extension<AuthenticatedSession>,
Json(payload): Json<SecretPayload>,
) -> Result<Json<Value>, ApiError> {
let secret = state
.service
.create_secret(
&path.workspace_id.as_str().into(),
Some(&session.user.id),
payload,
)
.await?;
Ok(Json(json!(secret)))
}
pub async fn get_secret(
Path(path): Path<WorkspaceSecretPath>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let secret = state
.service
.get_secret(
&path.workspace_id.as_str().into(),
&path.secret_id.as_str().into(),
)
.await?;
Ok(Json(json!(secret)))
}
pub async fn rotate_secret(
Path(path): Path<WorkspaceSecretPath>,
State(state): State<AppState>,
Extension(session): Extension<AuthenticatedSession>,
Json(payload): Json<RotateSecretPayload>,
) -> Result<Json<Value>, ApiError> {
let secret = state
.service
.rotate_secret(
&path.workspace_id.as_str().into(),
&path.secret_id.as_str().into(),
Some(&session.user.id),
payload,
)
.await?;
Ok(Json(json!(secret)))
}
pub async fn delete_secret(
Path(path): Path<WorkspaceSecretPath>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
state
.service
.delete_secret(
&path.workspace_id.as_str().into(),
&path.secret_id.as_str().into(),
)
.await?;
Ok(Json(json!({ "ok": true })))
}
+16
View File
@@ -0,0 +1,16 @@
use axum::{
Json,
extract::{Path, State},
};
use serde_json::{Value, json};
use crate::{error::ApiError, routes::access::WorkspacePath, state::AppState};
pub async fn list_protocol_capabilities(
Path(_path): Path<WorkspacePath>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
Ok(Json(json!({
"items": state.service.list_protocol_capabilities().await
})))
}
+58
View File
@@ -0,0 +1,58 @@
use axum::{
Extension, Json,
extract::{Path, State},
};
use serde_json::{Value, json};
use crate::{
auth::AuthenticatedSession,
error::ApiError,
service::{UpdateWorkspacePayload, WorkspacePayload},
state::AppState,
};
pub async fn list_workspaces(
State(state): State<AppState>,
Extension(session): Extension<AuthenticatedSession>,
) -> Result<Json<Value>, ApiError> {
let items = state
.service
.list_workspaces_for_user(&session.user.id)
.await?;
Ok(Json(json!({ "items": items })))
}
pub async fn create_workspace(
State(state): State<AppState>,
Extension(session): Extension<AuthenticatedSession>,
Json(payload): Json<WorkspacePayload>,
) -> Result<Json<Value>, ApiError> {
let workspace = state
.service
.create_workspace(&session.user.id, payload)
.await?;
Ok(Json(json!(workspace)))
}
pub async fn get_workspace(
Path(workspace_id): Path<String>,
State(state): State<AppState>,
) -> Result<Json<Value>, ApiError> {
let workspace = state
.service
.get_workspace(&workspace_id.as_str().into())
.await?;
Ok(Json(json!(workspace)))
}
pub async fn update_workspace(
Path(workspace_id): Path<String>,
State(state): State<AppState>,
Json(payload): Json<UpdateWorkspacePayload>,
) -> Result<Json<Value>, ApiError> {
let workspace = state
.service
.update_workspace(&workspace_id.as_str().into(), payload)
.await?;
Ok(Json(json!(workspace)))
}