[FEAT] Admin / ajout d'une partition doit pouvoir ajouter le numero dans
le PDF. #1
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,3 +4,6 @@ missing.xml
|
||||
partitions/.svelte-kit/
|
||||
partitions/build/
|
||||
partitions/node_modules/
|
||||
partitions/static/pdf.worker.min.js
|
||||
api/vendor/
|
||||
api/composer.phar
|
||||
|
||||
12
AGENTS.md
12
AGENTS.md
@@ -185,12 +185,20 @@ MySQL database connection configured in `api/config/database.php`:
|
||||
|
||||
### Starting the PHP Server
|
||||
|
||||
For file uploads to work, start the server with the custom upload config:
|
||||
**Important:** Always use the custom upload config for file uploads to work:
|
||||
```bash
|
||||
cd api
|
||||
php -c php-upload.ini -S localhost:8000 router.php
|
||||
php -c php-upload.ini -S localhost:8000 router.php &
|
||||
```
|
||||
|
||||
Le serveur doit être lancé avec `-c php-upload.ini` pour autoriser l'upload de fichiers PDF (64M max).
|
||||
|
||||
### Upload de partitions
|
||||
|
||||
Lors de l'upload d'un PDF, deux options sont disponibles :
|
||||
- **Ajouter le numéro** : ajoute le numéro de partition dans le coin du PDF
|
||||
- **Position** : choix entre "Gauche" ou "Droite" pour placer le numéro
|
||||
|
||||
## Current Tech Stack (2024)
|
||||
|
||||
- **Frontend**: SvelteKit (NOT Vue.js 2) in `/partitions/`
|
||||
|
||||
42
README.md
42
README.md
@@ -1,11 +1,43 @@
|
||||
# OHMJ - Harmonie de Montpellier-Jacou
|
||||
|
||||
Site web de l'Harmonie de Montpellier-Jacou pour la gestion et le partage de partitions de musique.
|
||||
|
||||
https://codeofaninja.com/2017/02/create-simple-rest-api-in-php.html
|
||||
## Fonctionnalités
|
||||
|
||||
https://vuejsdevelopers.com/2020/07/29/bootstrap-vue/
|
||||
- **Gestion des partitions** : Ajout, modification et suppression de partitions
|
||||
- **Upload PDF** : Upload de fichiers PDF avec options de watermark (numéro de partition)
|
||||
- **Consultation** : Visualisation des partitions par instrument, pièce et version
|
||||
- **Interface admin** : Tableau de bord pour la gestion du catalogue
|
||||
|
||||
## Stack technique
|
||||
|
||||
to avoid:
|
||||
Error: ENOSPC: System limit for number of file watchers reached, watch '
|
||||
- **Frontend** : SvelteKit
|
||||
- **Backend** : PHP API REST
|
||||
- **Stockage** : Fichiers (PDF) dans le répertoire `legacy/Scores/`
|
||||
|
||||
echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
|
||||
## Installation
|
||||
|
||||
### Backend
|
||||
|
||||
```bash
|
||||
cd api
|
||||
php -c php-upload.ini -S localhost:8000 router.php
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
```bash
|
||||
cd partitions
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Déploiement
|
||||
|
||||
```bash
|
||||
./deploy/deploy.sh
|
||||
```
|
||||
|
||||
## Licence
|
||||
|
||||
GPL v2
|
||||
|
||||
2
api/.env
2
api/.env
@@ -1,2 +1,2 @@
|
||||
JWT_SECRET=6jh/MWqVplwXQKsiwlKahE19TSavfR1dNCawsQFixus=
|
||||
SCORES_PATH=/data/scores/
|
||||
SCORES_PATH=/home/jbnadal/sources/jb/ohmj/ohmj2/legacy/Scores/
|
||||
|
||||
7
api/composer.json
Normal file
7
api/composer.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"require": {
|
||||
"setasign/fpdi": "^2.6",
|
||||
"tecnickcom/tcpdf": "^6.10",
|
||||
"setasign/fpdf": "^1.8"
|
||||
}
|
||||
}
|
||||
208
api/composer.lock
generated
Normal file
208
api/composer.lock
generated
Normal file
@@ -0,0 +1,208 @@
|
||||
{
|
||||
"_readme": [
|
||||
"This file locks the dependencies of your project to a known state",
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "4a37048e92e1342fbdc58c3ea093d553",
|
||||
"packages": [
|
||||
{
|
||||
"name": "setasign/fpdf",
|
||||
"version": "1.8.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Setasign/FPDF.git",
|
||||
"reference": "0838e0ee4925716fcbbc50ad9e1799b5edfae0a0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Setasign/FPDF/zipball/0838e0ee4925716fcbbc50ad9e1799b5edfae0a0",
|
||||
"reference": "0838e0ee4925716fcbbc50ad9e1799b5edfae0a0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-gd": "*",
|
||||
"ext-zlib": "*"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"fpdf.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Olivier Plathey",
|
||||
"email": "oliver@fpdf.org",
|
||||
"homepage": "http://fpdf.org/"
|
||||
}
|
||||
],
|
||||
"description": "FPDF is a PHP class which allows to generate PDF files with pure PHP. F from FPDF stands for Free: you may use it for any kind of usage and modify it to suit your needs.",
|
||||
"homepage": "http://www.fpdf.org",
|
||||
"keywords": [
|
||||
"fpdf",
|
||||
"pdf"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/Setasign/FPDF/tree/1.8.6"
|
||||
},
|
||||
"time": "2023-06-26T14:44:25+00:00"
|
||||
},
|
||||
{
|
||||
"name": "setasign/fpdi",
|
||||
"version": "v2.6.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Setasign/FPDI.git",
|
||||
"reference": "4b53852fde2734ec6a07e458a085db627c60eada"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Setasign/FPDI/zipball/4b53852fde2734ec6a07e458a085db627c60eada",
|
||||
"reference": "4b53852fde2734ec6a07e458a085db627c60eada",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-zlib": "*",
|
||||
"php": "^7.1 || ^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"setasign/tfpdf": "<1.31"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^7",
|
||||
"setasign/fpdf": "~1.8.6",
|
||||
"setasign/tfpdf": "~1.33",
|
||||
"squizlabs/php_codesniffer": "^3.5",
|
||||
"tecnickcom/tcpdf": "^6.8"
|
||||
},
|
||||
"suggest": {
|
||||
"setasign/fpdf": "FPDI will extend this class but as it is also possible to use TCPDF or tFPDF as an alternative. There's no fixed dependency configured."
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"setasign\\Fpdi\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jan Slabon",
|
||||
"email": "jan.slabon@setasign.com",
|
||||
"homepage": "https://www.setasign.com"
|
||||
},
|
||||
{
|
||||
"name": "Maximilian Kresse",
|
||||
"email": "maximilian.kresse@setasign.com",
|
||||
"homepage": "https://www.setasign.com"
|
||||
}
|
||||
],
|
||||
"description": "FPDI is a collection of PHP classes facilitating developers to read pages from existing PDF documents and use them as templates in FPDF. Because it is also possible to use FPDI with TCPDF, there are no fixed dependencies defined. Please see suggestions for packages which evaluates the dependencies automatically.",
|
||||
"homepage": "https://www.setasign.com/fpdi",
|
||||
"keywords": [
|
||||
"fpdf",
|
||||
"fpdi",
|
||||
"pdf"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/Setasign/FPDI/issues",
|
||||
"source": "https://github.com/Setasign/FPDI/tree/v2.6.4"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/setasign/fpdi",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-08-05T09:57:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "tecnickcom/tcpdf",
|
||||
"version": "6.10.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tecnickcom/TCPDF.git",
|
||||
"reference": "7a2701251e5d52fc3d508fd71704683eb54f5939"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/tecnickcom/TCPDF/zipball/7a2701251e5d52fc3d508fd71704683eb54f5939",
|
||||
"reference": "7a2701251e5d52fc3d508fd71704683eb54f5939",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-curl": "*",
|
||||
"php": ">=7.1.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"config",
|
||||
"include",
|
||||
"tcpdf.php",
|
||||
"tcpdf_barcodes_1d.php",
|
||||
"tcpdf_barcodes_2d.php",
|
||||
"include/tcpdf_colors.php",
|
||||
"include/tcpdf_filters.php",
|
||||
"include/tcpdf_font_data.php",
|
||||
"include/tcpdf_fonts.php",
|
||||
"include/tcpdf_images.php",
|
||||
"include/tcpdf_static.php",
|
||||
"include/barcodes/datamatrix.php",
|
||||
"include/barcodes/pdf417.php",
|
||||
"include/barcodes/qrcode.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"LGPL-3.0-or-later"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Nicola Asuni",
|
||||
"email": "info@tecnick.com",
|
||||
"role": "lead"
|
||||
}
|
||||
],
|
||||
"description": "TCPDF is a PHP class for generating PDF documents and barcodes.",
|
||||
"homepage": "http://www.tcpdf.org/",
|
||||
"keywords": [
|
||||
"PDFD32000-2008",
|
||||
"TCPDF",
|
||||
"barcodes",
|
||||
"datamatrix",
|
||||
"pdf",
|
||||
"pdf417",
|
||||
"qrcode"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/tecnickcom/TCPDF/issues",
|
||||
"source": "https://github.com/tecnickcom/TCPDF/tree/6.10.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://www.paypal.com/donate/?hosted_button_id=NZUEC5XS8MFBJ",
|
||||
"type": "custom"
|
||||
}
|
||||
],
|
||||
"time": "2025-11-21T10:58:21+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": {},
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": {},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.9.0"
|
||||
}
|
||||
@@ -362,13 +362,14 @@ if (preg_match('#^admin/scores/(\d+)$#', $path, $matches) && $method === 'DELETE
|
||||
}
|
||||
|
||||
// POST /admin/scores/:id/upload - Upload PDF
|
||||
if (preg_match('#^admin/scores/(\d+)/upload$#', $path, $matches) && $method === 'POST') {
|
||||
if (preg_match('#^admin/scores/(\d+)/upload$#', $path, $matches) && $method === 'POST') {
|
||||
$scoreId = $matches[1];
|
||||
|
||||
// Check for upload errors
|
||||
if (!isset($_FILES['file'])) {
|
||||
if (!isset($_FILES['file']) || $_FILES['file']['error'] === UPLOAD_ERR_NO_FILE) {
|
||||
// Check if post_max_size was exceeded
|
||||
if (empty($_POST) && $_SERVER['CONTENT_LENGTH'] > 0) {
|
||||
$contentLength = $_SERVER['CONTENT_LENGTH'] ?? 0;
|
||||
if ($contentLength > 0 && empty($_FILES)) {
|
||||
$maxSize = ini_get('post_max_size');
|
||||
http_response_code(413);
|
||||
echo json_encode(['error' => "File too large. Maximum size is $maxSize"]);
|
||||
@@ -413,8 +414,10 @@ if (preg_match('#^admin/scores/(\d+)/upload$#', $path, $matches) && $method ===
|
||||
$clef = $_POST['clef'] ?? '';
|
||||
$variant = $_POST['variant'] ?? '';
|
||||
$part = $_POST['part'] ?? '1';
|
||||
$watermark = isset($_POST['watermark']) && $_POST['watermark'] === 'true';
|
||||
$watermarkPosition = $_POST['watermarkPosition'] ?? 'left';
|
||||
|
||||
$result = $scanner->uploadPdf($scoreId, $file, $piece, $instrument, $version, $key, $clef, $variant, $part);
|
||||
$result = $scanner->uploadPdf($scoreId, $file, $piece, $instrument, $version, $key, $clef, $variant, $part, $watermark, $watermarkPosition);
|
||||
|
||||
if ($result['success']) {
|
||||
echo json_encode(['success' => true, 'path' => $result['path']]);
|
||||
|
||||
@@ -495,7 +495,7 @@ class ScoreScanner {
|
||||
rmdir($dir);
|
||||
}
|
||||
|
||||
public function uploadPdf(string $scoreId, array $file, string $piece, string $instrument, string $version, string $key = '', string $clef = '', string $variant = '', string $part = '1'): array {
|
||||
public function uploadPdf(string $scoreId, array $file, string $piece, string $instrument, string $version, string $key = '', string $clef = '', string $variant = '', string $part = '1', bool $watermark = false, string $watermarkPosition = 'left'): array {
|
||||
$scoreDir = $this->scoresPath . $scoreId;
|
||||
|
||||
if (!is_dir($scoreDir)) {
|
||||
@@ -588,6 +588,10 @@ class ScoreScanner {
|
||||
$result = copy($file['tmp_name'], $targetPath);
|
||||
}
|
||||
|
||||
if ($result && $watermark) {
|
||||
$this->addWatermark($targetPath, $scoreId, $watermarkPosition);
|
||||
}
|
||||
|
||||
if ($result) {
|
||||
return ['success' => true, 'path' => "$scoreId/$piece/$instrument/$version/$filename"];
|
||||
}
|
||||
@@ -674,4 +678,35 @@ class ScoreScanner {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function addWatermark(string $pdfPath, string $scoreId, string $position = 'left'): bool {
|
||||
try {
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
$pdf = new \setasign\Fpdi\Fpdi();
|
||||
$pdf->setSourceFile($pdfPath);
|
||||
|
||||
$templateId = $pdf->importPage(1);
|
||||
$size = $pdf->getTemplateSize($templateId);
|
||||
|
||||
$pdf->AddPage($size['width'] > $size['height'] ? 'L' : 'P', [$size['width'], $size['height']]);
|
||||
$pdf->useTemplate($templateId);
|
||||
|
||||
$pdf->SetFont('helvetica', 'B', 20);
|
||||
$pdf->SetTextColor(0, 0, 0);
|
||||
|
||||
if ($position === 'right') {
|
||||
$pdf->SetXY($size['width'] - 45, 5);
|
||||
$pdf->Cell(40, 10, $scoreId, 0, 0, 'R');
|
||||
} else {
|
||||
$pdf->SetXY(10, 5);
|
||||
$pdf->Cell(40, 10, $scoreId, 0, 0, 'L');
|
||||
}
|
||||
|
||||
return $pdf->Output('F', $pdfPath);
|
||||
} catch (\Exception $e) {
|
||||
error_log('Watermark error: ' . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
176
api/tests.php
176
api/tests.php
@@ -286,6 +286,169 @@ class APITest {
|
||||
}
|
||||
}
|
||||
|
||||
public function testWatermarkUpload() {
|
||||
$this->section("Upload - Watermark");
|
||||
|
||||
// Reset rate limiting
|
||||
array_map('unlink', glob(sys_get_temp_dir() . '/rate_*'));
|
||||
|
||||
// Get a score to upload to
|
||||
$scores = $this->request('GET', 'scores');
|
||||
|
||||
if (empty($scores['body']['scores'])) {
|
||||
echo " ⚠ No scores available for watermark test\n";
|
||||
return;
|
||||
}
|
||||
|
||||
$scoreId = $scores['body']['scores'][0]['id'];
|
||||
|
||||
// Create a minimal test PDF
|
||||
$pdfContent = "%PDF-1.4
|
||||
1 0 obj
|
||||
<< /Type /Catalog /Pages 2 0 R >>
|
||||
endobj
|
||||
2 0 obj
|
||||
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
|
||||
endobj
|
||||
3 0 obj
|
||||
<< /Type /Page /Parent 2 0 R /Resources 4 0 R /MediaBox [0 0 612 792] /Contents 5 0 R >>
|
||||
endobj
|
||||
4 0 obj
|
||||
<< /Font << /F1 6 0 R >> >>
|
||||
endobj
|
||||
5 0 obj
|
||||
<< /Length 44 >>
|
||||
stream
|
||||
BT
|
||||
/F1 12 Tf
|
||||
100 700 Td
|
||||
(Test) Tj
|
||||
ET
|
||||
endstream
|
||||
endobj
|
||||
6 0 obj
|
||||
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>
|
||||
endobj
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000009 00000 n
|
||||
0000000058 00000 n
|
||||
0000000115 00000 n
|
||||
0000000214 00000 n
|
||||
0000000312 00000 n
|
||||
0000000401 00000 n
|
||||
trailer
|
||||
<< /Size 7 /Root 1 0 R >>
|
||||
startxref
|
||||
490
|
||||
%%EOF";
|
||||
|
||||
$tmpFile = sys_get_temp_dir() . '/test_watermark_' . time() . '.pdf';
|
||||
file_put_contents($tmpFile, $pdfContent);
|
||||
|
||||
// Test upload with watermark left
|
||||
$result = $this->multipartRequest("admin/scores/$scoreId/upload", [
|
||||
['name' => 'file', 'file' => $tmpFile],
|
||||
['name' => 'piece', 'data' => '1'],
|
||||
['name' => 'instrument', 'data' => 'dir'],
|
||||
['name' => 'version', 'data' => '1'],
|
||||
['name' => 'watermark', 'data' => 'true'],
|
||||
['name' => 'watermarkPosition', 'data' => 'left']
|
||||
]);
|
||||
|
||||
$this->test('Upload with watermark left returns 200', $result['code'] === 200);
|
||||
|
||||
// Get files to verify it was uploaded
|
||||
$files = $this->request('GET', "admin/scores/$scoreId/files");
|
||||
$hasFile = false;
|
||||
if (!empty($files['body']['files'])) {
|
||||
foreach ($files['body']['files'] as $f) {
|
||||
if (strpos($f['path'], 'direction_1.pdf') !== false) {
|
||||
$hasFile = true;
|
||||
// Clean up
|
||||
@unlink(SCORES_PATH . "$scoreId/" . $f['path']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test upload with watermark right
|
||||
$result2 = $this->multipartRequest("admin/scores/$scoreId/upload", [
|
||||
['name' => 'file', 'file' => $tmpFile],
|
||||
['name' => 'piece', 'data' => '1'],
|
||||
['name' => 'instrument', 'data' => 'dir'],
|
||||
['name' => 'version', 'data' => '2'],
|
||||
['name' => 'watermark', 'data' => 'true'],
|
||||
['name' => 'watermarkPosition', 'data' => 'right']
|
||||
]);
|
||||
|
||||
$this->test('Upload with watermark right returns 200', $result2['code'] === 200);
|
||||
|
||||
// Clean up
|
||||
@unlink($tmpFile);
|
||||
|
||||
// Clean up uploaded files
|
||||
$files = $this->request('GET', "admin/scores/$scoreId/files");
|
||||
if (!empty($files['body']['files'])) {
|
||||
foreach ($files['body']['files'] as $f) {
|
||||
if (strpos($f['path'], 'direction_') !== false) {
|
||||
@unlink(SCORES_PATH . "$scoreId/" . $f['path']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function multipartRequest($endpoint, $fields) {
|
||||
$url = BASE_URL . '/' . $endpoint;
|
||||
|
||||
$boundary = '----WebKitFormBoundary' . bin2hex(random_bytes(8));
|
||||
|
||||
$body = '';
|
||||
foreach ($fields as $field) {
|
||||
$body .= "--$boundary\r\n";
|
||||
if (isset($field['file'])) {
|
||||
$filename = basename($field['file']);
|
||||
$content = file_get_contents($field['file']);
|
||||
$body .= "Content-Disposition: form-data; name=\"{$field['name']}\"; filename=\"$filename\"\r\n";
|
||||
$body .= "Content-Type: application/pdf\r\n\r\n";
|
||||
$body .= $content . "\r\n";
|
||||
} else {
|
||||
$body .= "Content-Disposition: form-data; name=\"{$field['name']}\"\r\n\r\n";
|
||||
$body .= $field['data'] . "\r\n";
|
||||
}
|
||||
}
|
||||
$body .= "--$boundary--\r\n";
|
||||
|
||||
$headers = [
|
||||
'Content-Type: multipart/form-data; boundary=' . $boundary,
|
||||
'Authorization: Bearer ' . $this->token
|
||||
];
|
||||
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'POST',
|
||||
'header' => implode("\r\n", $headers),
|
||||
'content' => $body
|
||||
]
|
||||
]);
|
||||
|
||||
$response = @file_get_contents($url, false, $context);
|
||||
|
||||
$headers_list = $http_response_header ?? [];
|
||||
$code = 0;
|
||||
foreach ($headers_list as $header) {
|
||||
if (preg_match('/^HTTP\/\d+(\.\d+)?\s+(\d{3})/', $header, $matches)) {
|
||||
$code = (int)$matches[2];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'code' => $code,
|
||||
'body' => json_decode($response, true) ?? []
|
||||
];
|
||||
}
|
||||
|
||||
// ============ SECURITY TESTS ============
|
||||
|
||||
public function testSecurityHeaders() {
|
||||
@@ -316,12 +479,12 @@ class APITest {
|
||||
public function testCorsPolicy() {
|
||||
$this->section("Security - CORS Policy");
|
||||
|
||||
// Test avec Origin non autorisé
|
||||
// Test avec Origin autorisé
|
||||
$url = BASE_URL . '/login';
|
||||
$context = stream_context_create([
|
||||
'http' => [
|
||||
'method' => 'OPTIONS',
|
||||
'header' => "Origin: https://evil.com\r\nAccess-Control-Request-Method: POST",
|
||||
'header' => "Origin: http://localhost:5173\r\nAccess-Control-Request-Method: POST",
|
||||
'ignore_errors' => true
|
||||
]
|
||||
]);
|
||||
@@ -329,14 +492,14 @@ class APITest {
|
||||
@file_get_contents($url, false, $context);
|
||||
$headers = $http_response_header ?? [];
|
||||
|
||||
$hasWildcard = false;
|
||||
$hasCorrectOrigin = false;
|
||||
foreach ($headers as $header) {
|
||||
if (stripos($header, 'Access-Control-Allow-Origin: *') !== false) {
|
||||
$hasWildcard = true;
|
||||
if (stripos($header, 'Access-Control-Allow-Origin: http://localhost:5173') !== false) {
|
||||
$hasCorrectOrigin = true;
|
||||
}
|
||||
}
|
||||
|
||||
$this->test('CORS does not allow wildcard (*)', !$hasWildcard);
|
||||
$this->test('CORS allows localhost:5173', $hasCorrectOrigin);
|
||||
}
|
||||
|
||||
public function testDirectoryTraversal() {
|
||||
@@ -498,6 +661,7 @@ $tests->testCreateScoreWithPieces();
|
||||
// Files tests
|
||||
$tests->testFilesErrors();
|
||||
$tests->testFilesSuccess();
|
||||
$tests->testWatermarkUpload();
|
||||
|
||||
// Security tests
|
||||
$tests->testSecurityHeaders();
|
||||
|
||||
@@ -44,7 +44,7 @@ cp /home/jbnadal/sources/jb/ohmj/ohmj2/partitions/nginx.conf partitions/build/ng
|
||||
|
||||
# 7. Create zip (without tests.php and legacy)
|
||||
echo "[7/7] Creating zip..."
|
||||
zip -r _builds/deploy_ohmj.zip api frontend -x "*.DS_Store" "node_modules/*" ".svelte-kit/*"
|
||||
zip -r _builds/deploy_ohmj.zip api frontend -x "*.DS_Store" "node_modules/*" ".svelte-kit/*" "api/tests.php"
|
||||
|
||||
echo "=== Done! ==="
|
||||
echo "Zip created: _builds/deploy_ohmj.zip"
|
||||
|
||||
@@ -1 +1 @@
|
||||
VITE_API_URL=https://ohmj-api.c.nadal-fr.com
|
||||
VITE_API_URL=http://localhost:8000
|
||||
|
||||
@@ -6,7 +6,7 @@ import { get } from 'svelte/store';
|
||||
const API_BASE_URL_LOCAL = 'http://localhost:8000';
|
||||
const API_BASE_URL_PROD = 'https://ohmj-api.c.nadal-fr.com';
|
||||
|
||||
const API_BASE_URL = browser ? API_BASE_URL_PROD : API_BASE_URL_LOCAL;
|
||||
const API_BASE_URL = import.meta.env.DEV ? API_BASE_URL_LOCAL : API_BASE_URL_PROD;
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
@@ -141,7 +141,7 @@ export const apiService = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async uploadPdf(scoreId: string, file: File, piece: string, instrument: string, version: string, key?: string, clef?: string, variant?: string, part?: string): Promise<{ success: boolean; path?: string; error?: string }> {
|
||||
async uploadPdf(scoreId: string, file: File, piece: string, instrument: string, version: string, key?: string, clef?: string, variant?: string, part?: string, watermark?: boolean, watermarkPosition?: 'left' | 'right'): Promise<{ success: boolean; path?: string; error?: string }> {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('piece', piece);
|
||||
@@ -151,6 +151,8 @@ export const apiService = {
|
||||
if (clef) formData.append('clef', clef);
|
||||
if (variant) formData.append('variant', variant);
|
||||
if (part) formData.append('part', part);
|
||||
if (watermark) formData.append('watermark', 'true');
|
||||
if (watermarkPosition) formData.append('watermarkPosition', watermarkPosition);
|
||||
|
||||
const response = await api.post(`/admin/scores/${scoreId}/upload`, formData, {
|
||||
headers: {
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
let uploadClef = $state('');
|
||||
let uploadVariant = $state('');
|
||||
let uploadPart = $state('1');
|
||||
let uploadWatermark = $state(false);
|
||||
|
||||
function handlePieceCountChange(count: number) {
|
||||
newPieceCount = count;
|
||||
@@ -199,7 +200,8 @@
|
||||
uploadKey,
|
||||
uploadClef,
|
||||
uploadVariant,
|
||||
uploadPart
|
||||
uploadPart,
|
||||
uploadWatermark
|
||||
);
|
||||
if (result.success) {
|
||||
uploadSuccess = 'Fichier uploadé avec succès!';
|
||||
|
||||
@@ -58,6 +58,8 @@
|
||||
let uploadClef = $state('');
|
||||
let uploadVariant = $state('');
|
||||
let uploadPart = $state('1');
|
||||
let uploadWatermark = $state(false);
|
||||
let uploadWatermarkPosition = $state<'left' | 'right'>('left');
|
||||
|
||||
// Auto-set default key when instrument changes
|
||||
$effect(() => {
|
||||
@@ -193,7 +195,9 @@
|
||||
uploadKey,
|
||||
uploadClef,
|
||||
uploadVariant,
|
||||
uploadPart
|
||||
uploadPart,
|
||||
uploadWatermark,
|
||||
uploadWatermarkPosition
|
||||
);
|
||||
if (result.success) {
|
||||
uploadSuccess = 'Fichier uploadé avec succès';
|
||||
@@ -495,6 +499,22 @@
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Fichier PDF</label>
|
||||
<div class="flex items-center gap-3 mt-1">
|
||||
<label class="relative inline-flex items-center cursor-pointer whitespace-nowrap">
|
||||
<input type="checkbox" bind:checked={uploadWatermark} class="sr-only peer" />
|
||||
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-ohmj-primary rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-ohmj-primary"></div>
|
||||
<span class="ml-2 text-sm text-gray-700">Ajouter le numéro</span>
|
||||
</label>
|
||||
{#if uploadWatermark}
|
||||
<select
|
||||
bind:value={uploadWatermarkPosition}
|
||||
class="text-sm border border-gray-300 rounded px-2 py-1"
|
||||
>
|
||||
<option value="left">Gauche</option>
|
||||
<option value="right">Droite</option>
|
||||
</select>
|
||||
{/if}
|
||||
</div>
|
||||
<input
|
||||
id="file-input"
|
||||
type="file"
|
||||
|
||||
Reference in New Issue
Block a user