feat: connect workspace access lifecycle to admin api
This commit is contained in:
@@ -219,6 +219,11 @@ pub struct InvitationPayload {
|
||||
pub expires_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct UpdateMembershipPayload {
|
||||
pub role: MembershipRole,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct CreatedInvitationResponse {
|
||||
pub invitation: InvitationRecord,
|
||||
@@ -237,6 +242,17 @@ pub struct CreatedPlatformApiKeyResponse {
|
||||
pub secret: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct WorkspaceExportResponse {
|
||||
pub workspace: WorkspaceRecord,
|
||||
pub memberships: Vec<MembershipRecord>,
|
||||
pub invitations: Vec<Value>,
|
||||
pub operations: Vec<OperationSummaryView>,
|
||||
pub agents: Vec<AgentSummaryView>,
|
||||
pub platform_api_keys: Vec<PlatformApiKeyRecord>,
|
||||
pub exported_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct LogsQuery {
|
||||
pub level: Option<InvocationLevel>,
|
||||
@@ -746,6 +762,107 @@ impl AdminService {
|
||||
Ok(self.registry.list_memberships(workspace_id).await?)
|
||||
}
|
||||
|
||||
pub async fn update_membership_role(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
actor_user_id: &crank_core::UserId,
|
||||
target_user_id: &crank_core::UserId,
|
||||
payload: UpdateMembershipPayload,
|
||||
) -> Result<Vec<MembershipRecord>, ApiError> {
|
||||
let memberships = self.list_memberships(workspace_id).await?;
|
||||
let actor_membership = memberships
|
||||
.iter()
|
||||
.find(|membership| &membership.user.id == actor_user_id)
|
||||
.ok_or_else(|| ApiError::forbidden("workspace access denied"))?;
|
||||
let target_membership = memberships
|
||||
.iter()
|
||||
.find(|membership| &membership.user.id == target_user_id)
|
||||
.ok_or_else(|| {
|
||||
ApiError::not_found(format!(
|
||||
"membership for user {} in workspace {} was not found",
|
||||
target_user_id.as_str(),
|
||||
workspace_id.as_str()
|
||||
))
|
||||
})?;
|
||||
|
||||
if !matches!(
|
||||
actor_membership.role,
|
||||
MembershipRole::Owner | MembershipRole::Admin
|
||||
) {
|
||||
return Err(ApiError::forbidden(
|
||||
"only owners and admins can manage workspace members",
|
||||
));
|
||||
}
|
||||
|
||||
if matches!(target_membership.role, MembershipRole::Owner)
|
||||
&& !matches!(payload.role, MembershipRole::Owner)
|
||||
{
|
||||
let owner_count = memberships
|
||||
.iter()
|
||||
.filter(|membership| matches!(membership.role, MembershipRole::Owner))
|
||||
.count();
|
||||
if owner_count <= 1 {
|
||||
return Err(ApiError::validation(
|
||||
"workspace must keep at least one owner",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
self.registry
|
||||
.update_membership_role(workspace_id, target_user_id, payload.role)
|
||||
.await?;
|
||||
self.list_memberships(workspace_id).await
|
||||
}
|
||||
|
||||
pub async fn remove_membership(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
actor_user_id: &crank_core::UserId,
|
||||
target_user_id: &crank_core::UserId,
|
||||
) -> Result<(), ApiError> {
|
||||
let memberships = self.list_memberships(workspace_id).await?;
|
||||
let actor_membership = memberships
|
||||
.iter()
|
||||
.find(|membership| &membership.user.id == actor_user_id)
|
||||
.ok_or_else(|| ApiError::forbidden("workspace access denied"))?;
|
||||
let target_membership = memberships
|
||||
.iter()
|
||||
.find(|membership| &membership.user.id == target_user_id)
|
||||
.ok_or_else(|| {
|
||||
ApiError::not_found(format!(
|
||||
"membership for user {} in workspace {} was not found",
|
||||
target_user_id.as_str(),
|
||||
workspace_id.as_str()
|
||||
))
|
||||
})?;
|
||||
|
||||
if !matches!(
|
||||
actor_membership.role,
|
||||
MembershipRole::Owner | MembershipRole::Admin
|
||||
) {
|
||||
return Err(ApiError::forbidden(
|
||||
"only owners and admins can manage workspace members",
|
||||
));
|
||||
}
|
||||
|
||||
if matches!(target_membership.role, MembershipRole::Owner) {
|
||||
let owner_count = memberships
|
||||
.iter()
|
||||
.filter(|membership| matches!(membership.role, MembershipRole::Owner))
|
||||
.count();
|
||||
if owner_count <= 1 {
|
||||
return Err(ApiError::validation(
|
||||
"workspace must keep at least one owner",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
self.registry
|
||||
.delete_membership(workspace_id, target_user_id)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub async fn list_invitations(
|
||||
&self,
|
||||
@@ -804,6 +921,63 @@ impl AdminService {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn export_workspace(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
) -> Result<WorkspaceExportResponse, ApiError> {
|
||||
let workspace = self.get_workspace(workspace_id).await?;
|
||||
let memberships = self.list_memberships(workspace_id).await?;
|
||||
let invitations = self
|
||||
.list_invitations(workspace_id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|record| {
|
||||
json!({
|
||||
"id": record.invitation.id,
|
||||
"email": record.invitation.email,
|
||||
"role": record.invitation.role,
|
||||
"status": record.invitation.status,
|
||||
"expires_at": record.invitation.expires_at,
|
||||
"created_at": record.invitation.created_at,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let operations = self.list_operations(workspace_id).await?;
|
||||
let agents = self.list_agents(workspace_id).await?;
|
||||
let platform_api_keys = self.list_platform_api_keys(workspace_id).await?;
|
||||
|
||||
Ok(WorkspaceExportResponse {
|
||||
workspace,
|
||||
memberships,
|
||||
invitations,
|
||||
operations,
|
||||
agents,
|
||||
platform_api_keys,
|
||||
exported_at: now_string()?,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn delete_workspace(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
actor_user_id: &crank_core::UserId,
|
||||
) -> Result<(), ApiError> {
|
||||
let memberships = self.list_memberships(workspace_id).await?;
|
||||
let actor_membership = memberships
|
||||
.iter()
|
||||
.find(|membership| &membership.user.id == actor_user_id)
|
||||
.ok_or_else(|| ApiError::forbidden("workspace access denied"))?;
|
||||
|
||||
if !matches!(actor_membership.role, MembershipRole::Owner) {
|
||||
return Err(ApiError::forbidden(
|
||||
"only workspace owners can delete a workspace",
|
||||
));
|
||||
}
|
||||
|
||||
self.registry.delete_workspace(workspace_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub async fn list_platform_api_keys(
|
||||
&self,
|
||||
|
||||
Reference in New Issue
Block a user