diff --git a/README.md b/README.md index 0f3159d..a2107cd 100644 --- a/README.md +++ b/README.md @@ -626,3 +626,4 @@ npx vitest # watch mode | `eas update` must be run with `--platform android` (`mobile/`), not `--platform all` | This app has no web target — `react-native-web` isn't installed — so the default `--platform all` fails bundling with `Unable to resolve module react-native-web/dist/index`. Previously published updates were all Android-only too (check `eas channel:view ` to confirm before assuming this changed). | | `mobile/node_modules` can end up silently corrupted (missing nested files) after an interrupted/partial install, without `npm install` re-detecting or fixing it | Symptom: Metro bundling fails with `Unable to resolve module ./some/nested/path.js` inside an already-installed package (seen with `@expo/cli`'s `utils/telemetry`, then `@sentry/browser` and `@sentry-internal/browser-utils` in the same session) — `npm install` reports "up to date" and does nothing. Fix: `rm -rf node_modules && npm install` (a full reinstall), not a targeted one; deleting just the one broken package's folder can just surface the *next* corrupted package one bundle-failure at a time. | | Mobile Google SSO can break with `redirect url ... does not match an authorized redirect URI` even though `GoogleSignInButton.tsx`/`app.json`'s scheme are unchanged | Clerk keeps its authorized-redirect-URL allowlist server-side per instance (`GET`/`POST https://api.clerk.com/v1/redirect_urls`, auth via `CLERK_SECRET_KEY`), entirely outside this repo — it can be missing or reset independent of any code change. Check that list before debugging app code; add the missing entry (`{"url":"pawfeed://sso-callback"}` for this app's `"scheme": "pawfeed"`) via `POST /v1/redirect_urls` — takes effect immediately, no app deploy/rebuild needed. | +| Admin music-track upload can fail even with a genuinely valid MP3/M4A/AAC file | Two independent causes, both hit in the same incident: (1) the Supabase bucket must be named **exactly** `music-tracks` (plural) — it was once created as `music-track` (singular), so every upload failed with a bucket-not-found error surfaced only as a generic "Upload fehlgeschlagen" toast; verify via `GET {SUPABASE_URL}/storage/v1/bucket` with the service-role key. (2) The upload form used to validate against the browser-reported `File.type`, which is sniffed from the OS's own MIME registry and is inconsistent for `.m4a`/`.aac` (e.g. Windows Chrome reports `.m4a` as `audio/x-m4a`, not `audio/mp4`) — it now derives the content-type from the file extension instead (`src/lib/music-content-types.ts`), matching what `MUSIC_ALLOWED_CONTENT_TYPES` (server-side, `supabase-storage.ts`) actually expects. | diff --git a/src/app/p/[secret]/music/page.tsx b/src/app/p/[secret]/music/page.tsx index 36df3cd..e85f46f 100644 --- a/src/app/p/[secret]/music/page.tsx +++ b/src/app/p/[secret]/music/page.tsx @@ -4,6 +4,7 @@ import { useState, useRef, useCallback } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useTRPC } from "@/trpc/client"; import { getMusicUrl } from "@/lib/music-url"; +import { MAX_MUSIC_FILE_SIZE, MUSIC_EXTENSION_CONTENT_TYPES, type MusicContentType } from "@/lib/music-content-types"; import { Plus, Pencil, Trash2, Loader2, Check, X, Music, Play, Pause } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -23,9 +24,34 @@ type TrackForm = { const emptyForm: TrackForm = { title: "", artist: "", licenseNote: "", storageKey: "", durationSecs: 0 }; -const ALLOWED_TYPES = ["audio/mpeg", "audio/mp4", "audio/aac"] as const; -type AllowedType = (typeof ALLOWED_TYPES)[number]; -const MAX_SIZE = 5 * 1024 * 1024; +const MAX_SIZE = MAX_MUSIC_FILE_SIZE; + +/** + * Extension -> canonical content-type, used instead of the browser-reported + * `file.type` for both validation and the actual upload's Content-Type + * header. `file.type` is sniffed by the browser/OS from its own MIME + * registry and is inconsistent for exactly these formats — Windows Chrome + * commonly reports `.m4a` as `audio/x-m4a` (not `audio/mp4`), and an + * unrecognized extension can come back as an empty string. Either would + * fail the old `ALLOWED_TYPES.includes(file.type)` check even for a + * perfectly valid MP3/M4A/AAC file — the upload looked like a rejected file + * type when it was really just an unrecognized *reported* MIME type. + * + * This does mean a file can be uploaded by simply renaming it with an + * allowed extension (e.g. `notes.txt` -> `notes.mp3`) rather than the + * browser's MIME-sniffed type having to agree first — a deliberate + * trade-off, not an oversight (flagged in code review): `file.type` was + * never a real content-based check either (browsers derive it from the same + * OS extension registry, not from inspecting bytes), this panel sits behind + * `ADMIN_SECRET` + a role check, not the public upload surface, and + * `readAudioDuration()` below still fails for genuinely non-audio bytes. + */ +function resolveContentType(file: File): MusicContentType | null { + const dotIndex = file.name.lastIndexOf("."); + if (dotIndex <= 0) return null; // no extension (or a bare ".ext"-only name) + const ext = file.name.slice(dotIndex + 1).toLowerCase(); + return MUSIC_EXTENSION_CONTENT_TYPES.get(ext) ?? null; +} /** Reads an audio file's duration client-side via a detached