[FIX] Fix some securiry issues

This commit is contained in:
NADAL Jean-Baptiste
2026-02-18 15:27:55 +01:00
parent 3abc6f6371
commit 039cecc4a6
15 changed files with 2179 additions and 200 deletions
@@ -0,0 +1,461 @@
<script lang="ts">
import { onMount } from 'svelte';
import { apiService } from '$lib/api';
import { auth } from '$lib/stores/auth';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
const INSTRUMENTS = [
{ code: 'dir', name: 'Direction', defaultKey: '' },
{ code: 'pic', name: 'Piccolo', defaultKey: 'do' },
{ code: 'flu', name: 'Flûte', defaultKey: 'do' },
{ code: 'cla', name: 'Clarinette', defaultKey: 'sib' },
{ code: 'clb', name: 'Clarinette Basse', defaultKey: 'sib' },
{ code: 'sax', name: 'Saxophone Alto', defaultKey: 'mib' },
{ code: 'sat', name: 'Saxophone Ténor', defaultKey: 'sib' },
{ code: 'sab', name: 'Saxophone Baryton', defaultKey: 'mib' },
{ code: 'coa', name: 'Cor Anglais', defaultKey: 'fa' },
{ code: 'htb', name: 'Hautbois', defaultKey: 'do' },
{ code: 'bas', name: 'Basson', defaultKey: 'do' },
{ code: 'cor', name: 'Cor', defaultKey: 'fa' },
{ code: 'trp', name: 'Trompette', defaultKey: 'sib' },
{ code: 'crn', name: 'Cornet', defaultKey: 'sib' },
{ code: 'trb', name: 'Trombone', defaultKey: 'do' },
{ code: 'eup', name: 'Euphonium', defaultKey: 'sib' },
{ code: 'tub', name: 'Tuba', defaultKey: 'do' },
{ code: 'cba', name: 'Contrebasse', defaultKey: 'sib' },
{ code: 'per', name: 'Percussions', defaultKey: '' },
{ code: 'pia', name: 'Piano', defaultKey: '' },
{ code: 'har', name: 'Harpe', defaultKey: '' }
];
let scoreId = $derived($page.params.id || '');
let scoreName = $state('');
let loading = $state(true);
let error = $state('');
let userRole = $state('');
let isAdmin = $derived(userRole === 'admin');
// Upload form
let uploadPiece = $state('1');
let uploadInstrument = $state('');
let uploadVersion = $state('1');
let uploadFile: File | null = $state(null);
let uploading = $state(false);
let uploadError = $state('');
let uploadSuccess = $state('');
let selectedScorePieceCount = $state(1);
let selectedScorePieces = $state<{id: string; name: string}[]>([]);
let showAdvanced = $state(false);
let uploadKey = $state('');
let uploadClef = $state('');
let uploadVariant = $state('');
let uploadPart = $state('1');
// File tree
let files: any[] = $state([]);
// Delete modal
let deleteFilePath = $state<string | null>(null);
let deletingFile = $state(false);
async function loadScoreInfo() {
try {
const score = await apiService.getScore(scoreId);
scoreName = score.name;
} catch (err) {
console.error(err);
error = 'Partition non trouvée';
}
}
async function loadFiles() {
try {
const result = await apiService.getScoreFiles(scoreId);
if (result.success) {
files = result.files;
}
} catch (err) {
console.error(err);
}
}
async function handleScoreSelect() {
if (scoreId) {
try {
const pieces = await apiService.getPieces(scoreId);
selectedScorePieceCount = pieces.length;
selectedScorePieces = pieces.map((p: any) => ({
id: String(p.id),
name: p.name || `Partie ${p.id}`
}));
if (selectedScorePieceCount === 1) {
uploadPiece = '1';
}
} catch (err) {
console.error(err);
selectedScorePieceCount = 1;
selectedScorePieces = [];
}
}
}
function getExpectedFilename(): string {
if (!uploadInstrument) return '';
const inst = INSTRUMENTS.find(i => i.code === uploadInstrument);
const instName = inst ? inst.name.toLowerCase().replace(/ /g, '_') : uploadInstrument;
const key = uploadKey || inst?.defaultKey || '';
const keyStr = key ? `_${key}` : '';
const clef = uploadClef ? `_${uploadClef}` : '';
const variant = uploadVariant ? `_${uploadVariant}` : '';
return `${instName}${variant}${keyStr}${clef}_${uploadPart}.pdf`;
}
let expectedFilename = $derived(getExpectedFilename());
onMount(async () => {
await new Promise(resolve => setTimeout(resolve, 100));
const unsubscribe = auth.subscribe((state) => {
userRole = state.user?.role || '';
});
if (userRole !== 'admin') {
unsubscribe();
goto('/scores');
return;
}
await loadScoreInfo();
await handleScoreSelect();
await loadFiles();
loading = false;
});
function fileExists(tree: any[], targetPath: string): boolean {
const parts = targetPath.split('/');
let current = tree;
for (const part of parts) {
const found = current.find((item: any) => item.name === part);
if (!found) return false;
if (found.type === 'file') {
return true; // Found the file
}
current = found.children || [];
}
return false;
}
async function uploadPdf() {
if (!uploadFile || !uploadInstrument) {
uploadError = 'Veuillez remplir tous les champs';
return;
}
uploadError = '';
uploadSuccess = '';
// Check if file already exists
const targetPath = `${uploadPiece}/${uploadInstrument}/${uploadVersion}/${expectedFilename}`;
if (fileExists(files, targetPath)) {
uploadError = `Le fichier existe déjà. Supprimez-le d'abord pour le remplacer.`;
return;
}
uploading = true;
try {
const result = await apiService.uploadPdf(
scoreId,
uploadFile,
uploadPiece,
uploadInstrument,
uploadVersion,
uploadKey,
uploadClef,
uploadVariant,
uploadPart
);
if (result.success) {
uploadSuccess = 'Fichier uploadé avec succès';
uploadFile = null;
(document.getElementById('file-input') as HTMLInputElement).value = '';
await loadFiles();
} else {
uploadError = result.error || 'Erreur';
}
} catch (err) {
uploadError = 'Erreur lors de l\'upload';
console.error(err);
} finally {
uploading = false;
}
}
async function deleteFile() {
if (!deleteFilePath) return;
deletingFile = true;
try {
const result = await apiService.deleteScoreFile(scoreId, deleteFilePath);
if (result.success) {
await loadFiles();
} else {
error = result.error || 'Erreur lors de la suppression';
}
} catch (err) {
error = 'Erreur lors de la suppression';
console.error(err);
} finally {
deleteFilePath = null;
deletingFile = false;
}
}
</script>
<div class="max-w-6xl mx-auto p-6">
<div class="mb-6 flex justify-between items-center">
<h1 class="text-3xl font-bold text-ohmj-primary">
{scoreName || 'Chargement...'}
</h1>
<a href="/admin" class="text-ohmj-primary hover:underline">← Retour à l'admin</a>
</div>
{#if error}
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
{error}
</div>
{/if}
{#if !isAdmin}
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
Accès refusé. Vous devez être administrateur.
</div>
{:else}
<!-- Upload PDF -->
<div class="mb-8 p-4 bg-gray-50 rounded-lg">
<h2 class="text-xl font-semibold text-gray-700 mb-4">Uploader un PDF</h2>
<form onsubmit={(e) => { e.preventDefault(); uploadPdf(); }} class="space-y-4">
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<label class="block text-sm font-medium text-gray-700">Instrument</label>
<select
bind:value={uploadInstrument}
required
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-white"
>
<option value="">Sélectionner...</option>
{#each INSTRUMENTS as inst}
<option value={inst.code}>{inst.name}</option>
{/each}
</select>
</div>
</div>
<div>
<button
type="button"
onclick={() => { showAdvanced = !showAdvanced; if (showAdvanced && !uploadKey) { uploadKey = INSTRUMENTS.find(i => i.code === uploadInstrument)?.defaultKey || ''; } }}
class="text-sm text-gray-600 hover:text-gray-800"
>
{showAdvanced ? '▼ Masquer' : '▶ Options avancées'}
</button>
</div>
{#if showAdvanced}
<div class="grid grid-cols-2 md:grid-cols-7 gap-4 p-3 bg-gray-100 rounded">
{#if selectedScorePieceCount > 1}
<div>
<label class="block text-sm font-medium text-gray-700">Pièce</label>
<select
bind:value={uploadPiece}
required
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-white"
>
{#each selectedScorePieces as piece}
<option value={piece.id}>{piece.name}</option>
{/each}
</select>
</div>
{/if}
<div>
<label class="block text-sm font-medium text-gray-700">Version</label>
<input
type="number"
min="1"
bind:value={uploadVersion}
required
class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm"
placeholder="1"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Tonalité</label>
<select bind:value={uploadKey} class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-white">
<option value="">-</option>
<option value="sib">Si♭ (sib)</option>
<option value="mib">Mi♭ (mif)</option>
<option value="fa">Fa (fa)</option>
<option value="do">Do (ut)</option>
<option value="sol">Sol</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Clé</label>
<select bind:value={uploadClef} class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-white">
<option value="">-</option>
<option value="clesol">Sol</option>
<option value="clefa">Fa</option>
<option value="cleut">Ut</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Partie</label>
<select bind:value={uploadPart} class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-white">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">Variante</label>
<select bind:value={uploadVariant} class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-white">
<option value="">-</option>
<option value="solo">Solo</option>
<option value="default">Default</option>
</select>
</div>
<div class="flex items-end">
<button
type="button"
onclick={() => showAdvanced = false}
class="text-sm text-gray-500 hover:text-gray-700"
>
Masquer
</button>
</div>
</div>
{/if}
<div>
<label class="block text-sm font-medium text-gray-700">Fichier PDF</label>
<input
id="file-input"
type="file"
accept="application/pdf"
onchange={(e) => uploadFile = (e.target as HTMLInputElement).files?.[0] || null}
required
class="mt-1 block w-full text-sm"
/>
</div>
{#if expectedFilename}
<p class="text-sm text-gray-500">Nom attendu: <code class="bg-gray-100 px-1 rounded">{expectedFilename}</code></p>
{/if}
{#if uploadError}
<p class="text-red-600 text-sm">{uploadError}</p>
{/if}
{#if uploadSuccess}
<p class="text-green-600 text-sm">{uploadSuccess}</p>
{/if}
<button
type="submit"
disabled={uploading}
class="bg-ohmj-primary text-white px-4 py-2 rounded hover:bg-ohmj-secondary disabled:opacity-50"
>
{uploading ? 'Upload...' : 'Uploader'}
</button>
</form>
</div>
<!-- File Tree -->
<div class="bg-white rounded-lg shadow overflow-hidden">
<div class="p-4 border-b bg-gray-50">
<h2 class="text-lg font-semibold text-gray-700">Fichiers</h2>
</div>
<div class="p-4">
{#if files.length === 0}
<p class="text-gray-500 text-center py-8">Aucun fichier uploadé</p>
{:else}
<div class="font-mono text-sm">
{#each files as piece}
<div class="mb-2">
<span class="text-gray-700 font-semibold">📁 {piece.name}/</span>
{#if piece.children}
{#each piece.children as instrument}
<div class="ml-4">
<span class="text-gray-600">├─ 📁 {instrument.name}/</span>
{#if instrument.children}
{#each instrument.children as version}
<div class="ml-4">
<span class="text-gray-600">├─ 📁 {version.name}/</span>
{#if version.children}
{#each version.children as file, fileIndex}
{@const isLast = fileIndex === version.children.length - 1}
<div class="ml-4">
{#if isLast}
<span class="text-gray-500">└─ 📄 {file.name}</span>
<button
onclick={() => deleteFilePath = file.path}
class="ml-2 text-red-500 hover:text-red-700"
title="Supprimer"
>
✕
</button>
{:else}
<span class="text-gray-500">├─ 📄 {file.name}</span>
<button
onclick={() => deleteFilePath = file.path}
class="ml-2 text-red-500 hover:text-red-700"
title="Supprimer"
>
✕
</button>
{/if}
</div>
{/each}
{/if}
</div>
{/each}
{/if}
</div>
{/each}
{/if}
</div>
{/each}
</div>
{/if}
</div>
</div>
{/if}
<!-- Delete Confirmation Modal -->
{#if deleteFilePath}
<div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" onclick={() => deleteFilePath = null}>
<div class="bg-white rounded-lg shadow-xl p-6 max-w-md w-full mx-4" onclick={(e) => e.stopPropagation()}>
<h3 class="text-lg font-semibold text-gray-900 mb-2">Confirmer la suppression</h3>
<p class="text-gray-600 mb-6">
Êtes-vous sûr de vouloir supprimer ce fichier ? Cette action est irréversible.
</p>
<p class="text-sm text-gray-500 mb-6 font-mono bg-gray-100 p-2 rounded">
{deleteFilePath}
</p>
<div class="flex gap-3 justify-end">
<button
onclick={() => deleteFilePath = null}
class="px-4 py-2 text-gray-700 hover:bg-gray-100 rounded transition-colors"
>
Annuler
</button>
<button
onclick={deleteFile}
disabled={deletingFile}
class="px-4 py-2 bg-red-600 text-white hover:bg-red-700 rounded transition-colors disabled:opacity-50"
>
{deletingFile ? 'Suppression...' : 'Supprimer'}
</button>
</div>
</div>
</div>
{/if}
</div>