Skip to content
/ loom Public

Commit f4cca72

Browse files
ghuntleyclaude
andcommitted
Fix clips API to support personal clips without org_id
- Make org_id optional in CreateClipRequest (for personal clips) - Make files default to empty vec with #[serde(default)] - Update handler to use user's display_name as owner for personal clips - Update logging and audit to handle optional org_id Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent e5d3947 commit f4cca72

File tree

2 files changed

+72
-63
lines changed

2 files changed

+72
-63
lines changed

crates/loom-server/src/routes/clips/common.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@ use uuid::Uuid;
1414

1515
#[derive(Debug, Deserialize, ToSchema)]
1616
pub struct CreateClipRequest {
17-
pub org_id: Uuid,
17+
pub org_id: Option<Uuid>,
1818
pub name: String,
1919
pub description: Option<String>,
2020
pub visibility: Option<String>,
21+
#[serde(default)]
2122
pub files: Vec<CreateClipFile>,
2223
}
2324

crates/loom-server/src/routes/clips/crud.rs

Lines changed: 70 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -87,65 +87,73 @@ pub async fn create_clip(
8787
.into_response();
8888
}
8989

90-
// Check org membership
91-
let org_id = OrgId::new(payload.org_id);
92-
let membership = match state
93-
.org_repo
94-
.get_membership(&org_id, &current_user.user.id)
95-
.await
96-
{
97-
Ok(Some(m)) => m,
98-
Ok(None) => {
99-
return (
100-
StatusCode::FORBIDDEN,
101-
Json(ClipsErrorResponse {
102-
error: "forbidden".to_string(),
103-
message: t(locale, "server.api.error.forbidden").to_string(),
104-
}),
105-
)
106-
.into_response();
107-
}
108-
Err(e) => {
109-
tracing::error!(error = %e, "Failed to check org membership");
110-
return (
111-
StatusCode::INTERNAL_SERVER_ERROR,
112-
Json(ClipsErrorResponse {
113-
error: "internal_error".to_string(),
114-
message: t(locale, "server.api.error.internal").to_string(),
115-
}),
116-
)
117-
.into_response();
118-
}
119-
};
90+
// Determine owner: org slug if org_id provided, otherwise user's display_name
91+
let (owner_slug, org_id_for_record) = if let Some(org_uuid) = payload.org_id {
92+
// Check org membership
93+
let org_id = OrgId::new(org_uuid);
94+
let _membership = match state
95+
.org_repo
96+
.get_membership(&org_id, &current_user.user.id)
97+
.await
98+
{
99+
Ok(Some(m)) => m,
100+
Ok(None) => {
101+
return (
102+
StatusCode::FORBIDDEN,
103+
Json(ClipsErrorResponse {
104+
error: "forbidden".to_string(),
105+
message: t(locale, "server.api.error.forbidden").to_string(),
106+
}),
107+
)
108+
.into_response();
109+
}
110+
Err(e) => {
111+
tracing::error!(error = %e, "Failed to check org membership");
112+
return (
113+
StatusCode::INTERNAL_SERVER_ERROR,
114+
Json(ClipsErrorResponse {
115+
error: "internal_error".to_string(),
116+
message: t(locale, "server.api.error.internal").to_string(),
117+
}),
118+
)
119+
.into_response();
120+
}
121+
};
120122

121-
// Get org for the owner name
122-
let org = match state.org_repo.get_org_by_id(&org_id).await {
123-
Ok(Some(o)) => o,
124-
Ok(None) => {
125-
return (
126-
StatusCode::NOT_FOUND,
127-
Json(ClipsErrorResponse {
128-
error: "not_found".to_string(),
129-
message: t(locale, "server.api.clips.org_not_found").to_string(),
130-
}),
131-
)
132-
.into_response();
133-
}
134-
Err(e) => {
135-
tracing::error!(error = %e, "Failed to get organization");
136-
return (
137-
StatusCode::INTERNAL_SERVER_ERROR,
138-
Json(ClipsErrorResponse {
139-
error: "internal_error".to_string(),
140-
message: t(locale, "server.api.error.internal").to_string(),
141-
}),
142-
)
143-
.into_response();
144-
}
123+
// Get org for the owner name
124+
let org = match state.org_repo.get_org_by_id(&org_id).await {
125+
Ok(Some(o)) => o,
126+
Ok(None) => {
127+
return (
128+
StatusCode::NOT_FOUND,
129+
Json(ClipsErrorResponse {
130+
error: "not_found".to_string(),
131+
message: t(locale, "server.api.clips.org_not_found").to_string(),
132+
}),
133+
)
134+
.into_response();
135+
}
136+
Err(e) => {
137+
tracing::error!(error = %e, "Failed to get organization");
138+
return (
139+
StatusCode::INTERNAL_SERVER_ERROR,
140+
Json(ClipsErrorResponse {
141+
error: "internal_error".to_string(),
142+
message: t(locale, "server.api.error.internal").to_string(),
143+
}),
144+
)
145+
.into_response();
146+
}
147+
};
148+
149+
(org.slug, Some(org_uuid))
150+
} else {
151+
// Personal clip - use user's display name as owner
152+
(current_user.user.display_name.clone(), None)
145153
};
146154

147-
// Check if clip name already exists
148-
if let Ok(true) = clips_repo.clip_name_exists(&org.slug, &payload.name).await {
155+
// Check if clip name already exists for this owner
156+
if let Ok(true) = clips_repo.clip_name_exists(&owner_slug, &payload.name).await {
149157
return (
150158
StatusCode::CONFLICT,
151159
Json(ClipsErrorResponse {
@@ -162,12 +170,12 @@ pub async fn create_clip(
162170

163171
let params = CreateClipParams {
164172
id: clip_id,
165-
owner: org.slug.clone(),
173+
owner: owner_slug.clone(),
166174
name: payload.name.clone(),
167175
description: payload.description.clone(),
168176
visibility,
169177
created_by: current_user.user.id.into_inner(),
170-
org_id: Some(payload.org_id),
178+
org_id: org_id_for_record,
171179
is_fork: false,
172180
forked_from: None,
173181
};
@@ -245,7 +253,8 @@ pub async fn create_clip(
245253
tracing::info!(
246254
clip_id = %clip_id,
247255
name = %payload.name,
248-
org_id = %payload.org_id,
256+
org_id = ?org_id_for_record,
257+
owner = %owner_slug,
249258
created_by = %current_user.user.id,
250259
"Clip created"
251260
);
@@ -256,15 +265,14 @@ pub async fn create_clip(
256265
.actor(AuditUserId::new(current_user.user.id.into_inner()))
257266
.resource("clip", clip_id.to_string())
258267
.details(serde_json::json!({
259-
"org_id": payload.org_id.to_string(),
268+
"org_id": org_id_for_record.map(|id| id.to_string()),
269+
"owner": owner_slug,
260270
"name": payload.name,
261271
"visibility": format!("{:?}", visibility),
262272
"file_count": payload.files.len(),
263273
}))
264274
.build(),
265275
);
266-
267-
let _ = membership;
268276
(
269277
StatusCode::CREATED,
270278
Json(clip_record_to_response(clip, &state.base_url)),

0 commit comments

Comments
 (0)