diff --git a/backend/app/Controllers/ElementController.php b/backend/app/Controllers/ElementController.php index 8665880..e086df6 100644 --- a/backend/app/Controllers/ElementController.php +++ b/backend/app/Controllers/ElementController.php @@ -18,10 +18,14 @@ use App\Exceptions\NotFoundException; use App\Shared\Files\FileUploader; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Http\Response; use Illuminate\Http\UploadedFile; +use Illuminate\Support\Facades\Storage; class ElementController { + private const PDF_FOLDERS = ['short', 'long']; + public function __construct( private GetElement $getElement, private CreateChildElement $createChildElement, @@ -64,6 +68,39 @@ class ElementController ], 200); } + public function showPdf( + string $folder, + string $fileName, + ): Response|JsonResponse { + if (!$this->isAllowedPdfFolder($folder)) { + return new JsonResponse([ + 'error' => 'PDF not found', + ], 404); + } + + if (!preg_match('/^[A-Za-z0-9._-]+\.pdf$/', $fileName)) { + return new JsonResponse([ + 'error' => 'PDF not found', + ], 404); + } + + $path = "element-pdfs/$folder/$fileName"; + if (!Storage::disk('public')->exists($path)) { + return new JsonResponse([ + 'error' => 'PDF not found', + ], 404); + } + + return new Response( + Storage::disk('public')->get($path), + 200, + [ + 'Content-Type' => 'application/pdf', + 'Content-Disposition' => "inline; filename=\"$fileName\"", + ], + ); + } + public function createChild(Request $request, ?int $parentId): JsonResponse { try { @@ -372,4 +409,9 @@ class ElementController return $path; } + + private function isAllowedPdfFolder(string $folder): bool + { + return in_array($folder, self::PDF_FOLDERS, true); + } } diff --git a/backend/routes/api.php b/backend/routes/api.php index f084592..82a3af4 100644 --- a/backend/routes/api.php +++ b/backend/routes/api.php @@ -12,6 +12,10 @@ Route::get('/me', [AuthController::class, 'me']) ->middleware(AuthMiddleware::class); Route::get('/sets', [SetController::class, 'index']); Route::get('/elements/{id}', [ElementController::class, 'show']); +Route::get('/element-pdfs/{folder}/{fileName}', [ + ElementController::class, + 'showPdf', +]); Route::post('/elements/{parentId}/children', [ ElementController::class, 'createChild', diff --git a/frontend/rabbi_gerzi/cypress.config.ts b/frontend/rabbi_gerzi/cypress.config.ts index 82a9470..1254b43 100644 --- a/frontend/rabbi_gerzi/cypress.config.ts +++ b/frontend/rabbi_gerzi/cypress.config.ts @@ -1,4 +1,5 @@ import { execSync } from 'node:child_process' +import { cpSync, mkdirSync, rmSync } from 'node:fs' import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { defineConfig } from 'cypress' @@ -7,7 +8,9 @@ import { Client } from 'pg' const configDirectory = dirname(fileURLToPath(import.meta.url)) const backendDirectory = resolve(configDirectory, '../../backend') +const seedAssetDirectory = resolve(backendDirectory, 'database/seeders/assets') const socketDirectory = resolve(configDirectory, '../../.postgres') +const storagePublicDirectory = resolve(backendDirectory, 'storage/app/public') const appDatabase = 'postgres' const templateDatabase = 'postgres_cypress_template' @@ -47,6 +50,7 @@ async function buildTemplate(): Promise { stdio: 'pipe', env: seedEnvironment, }) + resetSeedStorage() const markClient = controlClient() await markClient.connect() @@ -72,10 +76,24 @@ async function resetFromTemplate(): Promise { TEMPLATE ${templateDatabase}`, ) await client.end() + resetSeedStorage() return null } +function resetSeedStorage(): void { + rmSync(resolve(storagePublicDirectory, 'element-icons'), { + recursive: true, + force: true, + }) + rmSync(resolve(storagePublicDirectory, 'element-pdfs'), { + recursive: true, + force: true, + }) + mkdirSync(storagePublicDirectory, { recursive: true }) + cpSync(seedAssetDirectory, storagePublicDirectory, { recursive: true }) +} + export default defineConfig({ e2e: { baseUrl: 'http://localhost:5173', diff --git a/frontend/rabbi_gerzi/package-lock.json b/frontend/rabbi_gerzi/package-lock.json index 6a19774..707bb2c 100644 --- a/frontend/rabbi_gerzi/package-lock.json +++ b/frontend/rabbi_gerzi/package-lock.json @@ -19,6 +19,7 @@ "lucide-vue-next": "^1.0.0", "pinia": "^3.0.4", "vue": "beta", + "vue-pdf-embed": "^2.1.5", "vue-router": "^5.0.4" }, "devDependencies": { @@ -1275,6 +1276,271 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@napi-rs/canvas": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.100.tgz", + "integrity": "sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==", + "license": "MIT", + "optional": true, + "workspaces": [ + "e2e/*" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/canvas-android-arm64": "0.1.100", + "@napi-rs/canvas-darwin-arm64": "0.1.100", + "@napi-rs/canvas-darwin-x64": "0.1.100", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.100", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.100", + "@napi-rs/canvas-linux-arm64-musl": "0.1.100", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.100", + "@napi-rs/canvas-linux-x64-gnu": "0.1.100", + "@napi-rs/canvas-linux-x64-musl": "0.1.100", + "@napi-rs/canvas-win32-arm64-msvc": "0.1.100", + "@napi-rs/canvas-win32-x64-msvc": "0.1.100" + } + }, + "node_modules/@napi-rs/canvas-android-arm64": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.100.tgz", + "integrity": "sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.100.tgz", + "integrity": "sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-x64": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.100.tgz", + "integrity": "sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.100.tgz", + "integrity": "sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.100.tgz", + "integrity": "sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.100.tgz", + "integrity": "sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.100.tgz", + "integrity": "sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.100.tgz", + "integrity": "sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.100.tgz", + "integrity": "sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-arm64-msvc": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.100.tgz", + "integrity": "sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.100.tgz", + "integrity": "sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", @@ -7007,6 +7273,18 @@ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "license": "MIT" }, + "node_modules/pdfjs-dist": { + "version": "5.7.284", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.7.284.tgz", + "integrity": "sha512-h4EdYQczmGhbOlqc3PPZwxevn7ApdWPbovAuWXOB/DjIyigSnwfy2oze7c6mRcSr9XgLp3eN3EeL4DyySTPMFw==", + "license": "Apache-2.0", + "engines": { + "node": ">=22.13.0 || >=24" + }, + "optionalDependencies": { + "@napi-rs/canvas": "^0.1.100" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -8784,6 +9062,18 @@ "node": ">=10" } }, + "node_modules/vue-pdf-embed": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/vue-pdf-embed/-/vue-pdf-embed-2.1.5.tgz", + "integrity": "sha512-IGFVBYlnOz2zSql1zk4YJyBu584EZa6RUykk5f8wkHF/AR31khCa+ruJoRag+Ff2UyntkWu0brENIKoikQ7F8g==", + "license": "MIT", + "dependencies": { + "pdfjs-dist": "^5.7.284" + }, + "peerDependencies": { + "vue": "^3.3.0" + } + }, "node_modules/vue-router": { "version": "5.0.7", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.7.tgz", diff --git a/frontend/rabbi_gerzi/package.json b/frontend/rabbi_gerzi/package.json index 50ba0ca..c8ca08b 100644 --- a/frontend/rabbi_gerzi/package.json +++ b/frontend/rabbi_gerzi/package.json @@ -28,6 +28,7 @@ "lucide-vue-next": "^1.0.0", "pinia": "^3.0.4", "vue": "beta", + "vue-pdf-embed": "^2.1.5", "vue-router": "^5.0.4" }, "devDependencies": { diff --git a/frontend/rabbi_gerzi/src/components/PdfViewerModal.vue b/frontend/rabbi_gerzi/src/components/PdfViewerModal.vue new file mode 100644 index 0000000..ebfbbaf --- /dev/null +++ b/frontend/rabbi_gerzi/src/components/PdfViewerModal.vue @@ -0,0 +1,662 @@ + + + + + diff --git a/frontend/rabbi_gerzi/src/views/ElementPage.vue b/frontend/rabbi_gerzi/src/views/ElementPage.vue index c2ed707..b0aa452 100644 --- a/frontend/rabbi_gerzi/src/views/ElementPage.vue +++ b/frontend/rabbi_gerzi/src/views/ElementPage.vue @@ -1,16 +1,26 @@