fix(admin-api): accept slug or UUID on per-app endpoints
The Queues tab at /admin/apps/default/queues was returning "Cannot parse `app_id` with value `default`: UUID parsing failed". 27 admin endpoints across 6 files used strict `Path<AppId>` instead of the canonical `Path<String>` + `resolve_app()` pattern from app_repo.rs:34. Working endpoints (apps, app_members, users_admin) all use the lenient pattern; this commit brings the remaining 6 files into line: - queues_api.rs — 2 handlers - files_api.rs — 2 handlers - secrets_api.rs — 3 handlers - topics_api.rs — 4 handlers - dead_letters_api.rs — 5 handlers - triggers_api.rs — 10 handlers The handler bodies (authz, repo calls) are unchanged; only the path extractor and the per-file `ensure_app_exists` helper (now renamed `resolve_app`) move. Lib tests updated to pass `.to_string()` at the call site (Path now takes String, not AppId). email_inbound_api.rs deliberately stays strict-UUID — it's a public webhook receiver consumed by external providers, not by the slug-based dashboard. Adds Playwright spec `dashboard/tests/e2e/navigation/tabs.spec.ts` covering every per-app tab (queues, files, dead-letters, users, invitations, plus the main page hosting triggers/secrets/topics) with a negative assertion against the "Cannot parse" error text plus a focused regression test for the original queues report. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -92,10 +92,10 @@ fn validate_topic_name(name: &str) -> Result<(), TopicsApiError> {
|
||||
async fn create_topic(
|
||||
State(s): State<TopicsState>,
|
||||
Extension(principal): Extension<Principal>,
|
||||
Path(app_id): Path<AppId>,
|
||||
Path(id_or_slug): Path<String>,
|
||||
Json(input): Json<CreateTopicRequest>,
|
||||
) -> Result<(StatusCode, Json<Topic>), TopicsApiError> {
|
||||
ensure_app_exists(&*s.apps, app_id).await?;
|
||||
let app_id = resolve_app(&*s.apps, &id_or_slug).await?;
|
||||
require(
|
||||
s.authz.as_ref(),
|
||||
&principal,
|
||||
@@ -123,9 +123,9 @@ struct ListTopicsResponse {
|
||||
async fn list_topics(
|
||||
State(s): State<TopicsState>,
|
||||
Extension(principal): Extension<Principal>,
|
||||
Path(app_id): Path<AppId>,
|
||||
Path(id_or_slug): Path<String>,
|
||||
) -> Result<Json<ListTopicsResponse>, TopicsApiError> {
|
||||
ensure_app_exists(&*s.apps, app_id).await?;
|
||||
let app_id = resolve_app(&*s.apps, &id_or_slug).await?;
|
||||
require(s.authz.as_ref(), &principal, Capability::AppRead(app_id)).await?;
|
||||
let topics = s.topics.list(app_id).await?;
|
||||
Ok(Json(ListTopicsResponse { topics }))
|
||||
@@ -134,10 +134,10 @@ async fn list_topics(
|
||||
async fn update_topic(
|
||||
State(s): State<TopicsState>,
|
||||
Extension(principal): Extension<Principal>,
|
||||
Path((app_id, name)): Path<(AppId, String)>,
|
||||
Path((id_or_slug, name)): Path<(String, String)>,
|
||||
Json(input): Json<UpdateTopicRequest>,
|
||||
) -> Result<Json<Topic>, TopicsApiError> {
|
||||
ensure_app_exists(&*s.apps, app_id).await?;
|
||||
let app_id = resolve_app(&*s.apps, &id_or_slug).await?;
|
||||
require(
|
||||
s.authz.as_ref(),
|
||||
&principal,
|
||||
@@ -155,9 +155,9 @@ async fn update_topic(
|
||||
async fn delete_topic(
|
||||
State(s): State<TopicsState>,
|
||||
Extension(principal): Extension<Principal>,
|
||||
Path((app_id, name)): Path<(AppId, String)>,
|
||||
Path((id_or_slug, name)): Path<(String, String)>,
|
||||
) -> Result<StatusCode, TopicsApiError> {
|
||||
ensure_app_exists(&*s.apps, app_id).await?;
|
||||
let app_id = resolve_app(&*s.apps, &id_or_slug).await?;
|
||||
require(
|
||||
s.authz.as_ref(),
|
||||
&principal,
|
||||
@@ -172,12 +172,12 @@ async fn delete_topic(
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
async fn ensure_app_exists(apps: &dyn AppRepository, app_id: AppId) -> Result<(), TopicsApiError> {
|
||||
apps.get_by_id(app_id)
|
||||
async fn resolve_app(apps: &dyn AppRepository, ident: &str) -> Result<AppId, TopicsApiError> {
|
||||
crate::app_repo::resolve_app(apps, ident)
|
||||
.await
|
||||
.map_err(|e| TopicsApiError::Backend(e.to_string()))?
|
||||
.ok_or(TopicsApiError::AppNotFound)?;
|
||||
Ok(())
|
||||
.map(|l| l.app.id)
|
||||
.ok_or(TopicsApiError::AppNotFound)
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
@@ -501,7 +501,7 @@ mod tests {
|
||||
let (status, Json(topic)) = create_topic(
|
||||
State(s),
|
||||
Extension(member()),
|
||||
Path(app),
|
||||
Path(app.to_string()),
|
||||
Json(CreateTopicRequest {
|
||||
name: "chat".into(),
|
||||
external_subscribable: false,
|
||||
@@ -527,7 +527,7 @@ mod tests {
|
||||
let err = update_topic(
|
||||
State(s),
|
||||
Extension(member()),
|
||||
Path((app, "chat".to_string())),
|
||||
Path((app.to_string(), "chat".to_string())),
|
||||
Json(UpdateTopicRequest {
|
||||
external_subscribable: Some(true),
|
||||
auth_mode: None,
|
||||
@@ -551,7 +551,7 @@ mod tests {
|
||||
let Json(updated) = update_topic(
|
||||
State(s),
|
||||
Extension(member()),
|
||||
Path((app, "chat".to_string())),
|
||||
Path((app.to_string(), "chat".to_string())),
|
||||
Json(UpdateTopicRequest {
|
||||
external_subscribable: Some(true),
|
||||
auth_mode: Some(TopicAuthMode::Token),
|
||||
@@ -574,7 +574,7 @@ mod tests {
|
||||
let status = delete_topic(
|
||||
State(s),
|
||||
Extension(member()),
|
||||
Path((app, "chat".to_string())),
|
||||
Path((app.to_string(), "chat".to_string())),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -596,7 +596,7 @@ mod tests {
|
||||
let err = create_topic(
|
||||
State(s),
|
||||
Extension(member()),
|
||||
Path(app_b),
|
||||
Path(app_b.to_string()),
|
||||
Json(CreateTopicRequest {
|
||||
name: "chat".into(),
|
||||
external_subscribable: true,
|
||||
@@ -615,7 +615,7 @@ mod tests {
|
||||
let err = create_topic(
|
||||
State(s),
|
||||
Extension(member()),
|
||||
Path(app),
|
||||
Path(app.to_string()),
|
||||
Json(CreateTopicRequest {
|
||||
name: "user.*".into(),
|
||||
external_subscribable: true,
|
||||
|
||||
Reference in New Issue
Block a user