# cdn-relay Integration — Next Steps for BukidBountyApp The `cdn-relay` microservice (separate repo, deployed on Dokploy) is ready. This document captures the BukidBountyApp-side wiring that has **not yet been merged** into the main app, to be added later "at less critical times." When you come back to this: the cdn-relay container is running, but it polls a `/internal/cdn/*` surface that doesn't exist on this app yet. The poll loop will keep returning 401/404 until you ship the changes below. That's harmless — it's a no-op tick. ## What's already done - `docs/tasks/cdn-microservice-plan.md` — full design doc (updated to reflect the live `cdn-relay` repo + Go + simple-upload mode + auth). - The microservice itself (in `~/development/personal/cdn-relay`). - DB plumbing already in place from earlier commits: - `file_list.cdn_url`, `file_list.is_public`, `file_list.file_type` columns. - `FileList::resolvedUrl()` and the redirect short-circuit in `FilesMainController::viewFilebyFileListHash`. ## What's still missing 1. **Bearer middleware** validating `CDN_SERVICE_TOKEN` from `.env`. 2. **Internal CDN controller** with three actions: `pending`, `bytes`, `published`. 3. **Routes** at `/internal/cdn/*`. 4. **`CDN_SERVICE_TOKEN` env var** on prod (must match the `MAIN_APP_TOKEN` on the relay). 5. **Optional UI** for flipping `is_public` / `file_type` on existing rows (out of scope for this doc — operator-driven SQL is fine for v1). The implementation below was drafted and verified to lint clean (`php -l`); paste it in when ready. --- ## 1. Middleware — `app/Http/Middleware/InternalCdnAuth.php` ```php json(['error' => 'CDN_SERVICE_TOKEN not configured'], 503); } $header = (string) $request->header('Authorization', ''); if (!str_starts_with($header, 'Bearer ')) { return response()->json(['error' => 'missing bearer token'], 401); } $provided = substr($header, 7); if (!hash_equals($expected, $provided)) { return response()->json(['error' => 'invalid token'], 401); } return $next($request); } } ``` ## 2. Controller — `app/Http/Controllers/Internal/CdnController.php` ```php query('limit', 50); if ($limit < 1) { $limit = 1; } if ($limit > 500) { $limit = 500; } $maxBytes = (int) $request->query('max_size', 50 * 1024 * 1024); $rows = FileList::query() ->where('file_list.is_public', true) ->whereNull('file_list.cdn_url') ->join('file_content', 'file_content.uid', '=', 'file_list.contentuid') ->where('file_content.size_in_bytes', '<=', $maxBytes) ->orderBy('file_list.id', 'asc') ->limit($limit) ->get([ 'file_list.hashkey as hashkey', 'file_content.filehash as filehash', 'file_content.mimetype as mimetype', 'file_content.size_in_bytes as size_in_bytes', 'file_list.file_type as file_type', 'file_list.filename as filename', ]); return Response::json(['items' => $rows]); } /** * GET /internal/cdn/bytes/{hashkey} * * Streams the raw bytes for a file_list row, bypassing the cdn_url redirect * (since the relay is what populates cdn_url in the first place). */ public function bytes(string $hashkey) { $filelist = FileList::where('hashkey', $hashkey)->first(); if (!$filelist) { return Response::json(['error' => 'not found'], 404); } $fileContent = $filelist->fileContent; if (!$fileContent) { return Response::json(['error' => 'content missing'], 404); } $content = $fileContent->content; $driver = DB::connection()->getDriverName(); if (is_resource($content) && $driver !== 'mysql') { $content = stream_get_contents($content); } if ($driver === 'mysql') { $content = base64_decode($content); } $mime = (string) ($fileContent->mimetype ?? ''); if ($mime === '') { $finfo = new \finfo(FILEINFO_MIME_TYPE); $mime = (string) $finfo->buffer($content); } return Response::make($content, 200, [ 'Content-Type' => $mime !== '' ? $mime : 'application/octet-stream', 'Content-Length' => (string) strlen($content), 'Cache-Control' => 'no-store', ]); } /** * POST /internal/cdn/published * Body: { "hashkey": "...", "cdn_url": "https://cdn.jsdelivr.net/..." } * * Idempotent: if cdn_url is already set, returns 200 without overwriting. */ public function published(Request $request) { $hashkey = (string) $request->input('hashkey', ''); $cdnUrl = (string) $request->input('cdn_url', ''); if ($hashkey === '' || $cdnUrl === '') { return Response::json(['error' => 'hashkey and cdn_url are required'], 422); } if (!filter_var($cdnUrl, FILTER_VALIDATE_URL)) { return Response::json(['error' => 'cdn_url must be a valid URL'], 422); } $row = FileList::where('hashkey', $hashkey)->first(); if (!$row) { return Response::json(['error' => 'file_list not found'], 404); } if (!empty($row->cdn_url)) { return Response::json(['ok' => true, 'cdn_url' => $row->cdn_url, 'noop' => true]); } $row->cdn_url = $cdnUrl; $row->save(); return Response::json(['ok' => true, 'cdn_url' => $cdnUrl]); } } ``` ## 3. Kernel alias — `app/Http/Kernel.php` In the `$middlewareAliases` array, add: ```php 'internal.cdn' => \App\Http\Middleware\InternalCdnAuth::class, ``` ## 4. Routes — append to `routes/web.php` ```php // --- cdn-relay microservice integration --- // All /internal/cdn/* endpoints are protected by a static bearer token (CDN_SERVICE_TOKEN). Route::get('/internal/cdn/pending', [ 'uses' => \App\Http\Controllers\Internal\CdnController::class . '@pending', 'middleware' => 'internal.cdn', ]); Route::get('/internal/cdn/bytes/{hashkey}', [ 'uses' => \App\Http\Controllers\Internal\CdnController::class . '@bytes', 'middleware' => 'internal.cdn', ]); Route::post('/internal/cdn/published', [ 'uses' => \App\Http\Controllers\Internal\CdnController::class . '@published', 'middleware' => 'internal.cdn', ]); ``` ## 5. Env — `.env` (prod) and `.env.example` ```env # --- cdn-relay microservice --- # Shared secret the cdn-relay sends in `Authorization: Bearer ` to /internal/cdn/* endpoints. CDN_SERVICE_TOKEN= ``` Generate with `openssl rand -hex 32`. Use the **same value** for `MAIN_APP_TOKEN` on the cdn-relay Dokploy service. --- ## Verification after merging ```bash # From the Dokploy host, on dokploy-network: docker exec -it cdn-relay sh -c "wget -qO- http://bukidapp:9501/internal/cdn/pending" # Expect 401 (no token) docker exec -it cdn-relay sh -c "wget -qO- --header='Authorization: Bearer $MAIN_APP_TOKEN' http://bukidapp:9501/internal/cdn/pending" # Expect {"items":[]} or actual rows ``` Watch the relay logs after merging — the poll loop should stop logging 401s and start logging successful publishes once you flip `is_public = 1` on a row. --- ## Why this is being deferred The `/internal/cdn/*` endpoints expose binary content streams and a write path. Adding them to BukidBountyApp during a critical window risks: - Routing-layer regressions (a bad route definition can taint the whole router). - Auth-layer regressions (the new middleware reads `env()`; misconfig on prod yields 503s on every relay tick). - The Internal/ namespace is new — a typo in PSR-4 autoload would surface as a `ClassNotFoundException` only when the relay first calls in. Better to merge during a low-traffic window and verify with the relay's `/v1/admin/audit` endpoint that requests are landing.