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:
MechaCat02
2026-06-09 18:27:50 +02:00
parent c1e4c3416b
commit aa493b9326
7 changed files with 192 additions and 115 deletions

View File

@@ -114,10 +114,10 @@ impl From<DeadLetterRow> for DeadLetterDto {
async fn list(
State(s): State<DeadLettersState>,
Extension(principal): Extension<Principal>,
Path(app_id): Path<AppId>,
Path(id_or_slug): Path<String>,
Query(q): Query<ListQuery>,
) -> Result<Json<ListResponse>, DeadLettersApiError> {
ensure_app(&*s.apps, app_id).await?;
let app_id = resolve_app(&*s.apps, &id_or_slug).await?;
require(
s.authz.as_ref(),
&principal,
@@ -136,9 +136,9 @@ async fn list(
async fn count(
State(s): State<DeadLettersState>,
Extension(principal): Extension<Principal>,
Path(app_id): Path<AppId>,
Path(id_or_slug): Path<String>,
) -> Result<Json<CountResponse>, DeadLettersApiError> {
ensure_app(&*s.apps, app_id).await?;
let app_id = resolve_app(&*s.apps, &id_or_slug).await?;
require(
s.authz.as_ref(),
&principal,
@@ -152,9 +152,9 @@ async fn count(
async fn detail(
State(s): State<DeadLettersState>,
Extension(principal): Extension<Principal>,
Path((app_id, dl_id)): Path<(AppId, DeadLetterId)>,
Path((id_or_slug, dl_id)): Path<(String, DeadLetterId)>,
) -> Result<Json<DeadLetterDto>, DeadLettersApiError> {
ensure_app(&*s.apps, app_id).await?;
let app_id = resolve_app(&*s.apps, &id_or_slug).await?;
require(
s.authz.as_ref(),
&principal,
@@ -175,9 +175,9 @@ async fn detail(
async fn replay(
State(s): State<DeadLettersState>,
Extension(principal): Extension<Principal>,
Path((app_id, dl_id)): Path<(AppId, DeadLetterId)>,
Path((id_or_slug, dl_id)): Path<(String, DeadLetterId)>,
) -> Result<StatusCode, DeadLettersApiError> {
ensure_app(&*s.apps, app_id).await?;
let app_id = resolve_app(&*s.apps, &id_or_slug).await?;
// Authz handled inside the service via SdkCallCx.
let cx = admin_cx(app_id, &principal);
s.service
@@ -190,10 +190,10 @@ async fn replay(
async fn resolve(
State(s): State<DeadLettersState>,
Extension(principal): Extension<Principal>,
Path((app_id, dl_id)): Path<(AppId, DeadLetterId)>,
Path((id_or_slug, dl_id)): Path<(String, DeadLetterId)>,
Json(body): Json<ResolveBody>,
) -> Result<StatusCode, DeadLettersApiError> {
ensure_app(&*s.apps, app_id).await?;
let app_id = resolve_app(&*s.apps, &id_or_slug).await?;
let cx = admin_cx(app_id, &principal);
s.service
.resolve(&cx, dl_id, &body.reason)
@@ -221,12 +221,12 @@ fn admin_cx(app_id: AppId, principal: &Principal) -> SdkCallCx {
}
}
async fn ensure_app(apps: &dyn AppRepository, app_id: AppId) -> Result<(), DeadLettersApiError> {
apps.get_by_id(app_id)
async fn resolve_app(apps: &dyn AppRepository, ident: &str) -> Result<AppId, DeadLettersApiError> {
crate::app_repo::resolve_app(apps, ident)
.await
.map_err(|e| DeadLettersApiError::Backend(e.to_string()))?
.ok_or_else(|| DeadLettersApiError::AppNotFound(app_id.to_string()))?;
Ok(())
.map(|l| l.app.id)
.ok_or_else(|| DeadLettersApiError::AppNotFound(ident.to_string()))
}
fn map_service_err(e: picloud_shared::DeadLetterError) -> DeadLettersApiError {