R4 backend: team org-chart + leaderboard, Stripe credits, workspace apps
- GET /api/team/orgchart — each member grouped with the claws they manage (agents.managed_by); GET /api/team/leaderboard — every claw ranked by its real usage_events rollup (credits/tokens/runs, zeros included). Both tested against real Postgres. - Stripe Buy-credits (the Slack/Clerk integration pattern): [billing] config (stripe keys + price + webhook secret + credits_per_pack); POST /api/credits/checkout opens a real Checkout Session; POST /api/billing/stripe verifies Stripe's t=,v1= HMAC (constant-time) and grants one credit lot, idempotent on the session id; GET /api/billing/config gates the button (honest degradation when unset). Offline tests: signed grant + replay no-double-grant + forged-sig 400 + config flag. Live checkout creation deferred to a CM_LIVE_STRIPE test. - /apps global page support: clawId now optional on connect + directory; absent => workspace-wide connection (app_connections.agent_id NULL) via new connections::list_for_workspace. - ApiError gains a From<sqlx::Error> so inline queries use ? cleanly. cm-api 10 test files incl. team_tabs (2) + stripe_billing (3); clippy clean. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5
parent
67b80da695
commit
f3f08a8edd
@@ -129,6 +129,8 @@ fn catalog() -> Vec<DirectoryApp> {
|
||||
pub struct DirectoryQuery {
|
||||
#[serde(rename = "clawId")]
|
||||
claw_id: Option<AgentId>,
|
||||
/// When true (and no clawId), report workspace-wide connections.
|
||||
workspace: Option<bool>,
|
||||
}
|
||||
|
||||
/// GET /api/apps[?clawId=] — the connect directory (§8.2), with live
|
||||
@@ -144,6 +146,9 @@ pub async fn directory(
|
||||
cm_db::repo::connections::list_for_agent(&state.pool, user.workspace_id, agent.id)
|
||||
.await?
|
||||
}
|
||||
None if query.workspace.unwrap_or(false) => {
|
||||
cm_db::repo::connections::list_for_workspace(&state.pool, user.workspace_id).await?
|
||||
}
|
||||
None => Vec::new(),
|
||||
};
|
||||
let merged = catalog()
|
||||
@@ -166,7 +171,7 @@ pub async fn directory(
|
||||
#[derive(Deserialize)]
|
||||
pub struct ConnectRequest {
|
||||
#[serde(rename = "clawId")]
|
||||
claw_id: AgentId,
|
||||
claw_id: Option<AgentId>,
|
||||
provider: String,
|
||||
#[serde(rename = "authType")]
|
||||
auth_type: String,
|
||||
@@ -185,7 +190,12 @@ pub async fn connect(
|
||||
if !matches!(body.auth_type.as_str(), "keys" | "basic") {
|
||||
return Err(ApiError::Conflict);
|
||||
}
|
||||
let agent = workspace_agent(&state, &user, body.claw_id).await?;
|
||||
// clawId present => per-claw connection; absent => workspace-wide
|
||||
// (agent_id NULL), the /apps global page path.
|
||||
let agent_id = match body.claw_id {
|
||||
Some(claw_id) => Some(workspace_agent(&state, &user, claw_id).await?.id),
|
||||
None => None,
|
||||
};
|
||||
let socket = state.broker_socket.as_ref().ok_or(ApiError::Internal)?;
|
||||
let mut broker = cm_secrets::BrokerClient::connect(socket)
|
||||
.await
|
||||
@@ -201,7 +211,7 @@ pub async fn connect(
|
||||
let connection = cm_db::repo::connections::insert(
|
||||
&state.pool,
|
||||
user.workspace_id,
|
||||
Some(agent.id),
|
||||
agent_id,
|
||||
&body.provider,
|
||||
&body.auth_type,
|
||||
secret_ref,
|
||||
|
||||
Reference in New Issue
Block a user