security: fix 27 vulnerabilities + UI integration (Issues tab, project nav, validation)

Security fixes (27 vulnerabilities across 20 files):
CRITICAL:
- MediaManager: whitelist mediable types prevents RCE via class instantiation
- MediaManager/OfflineSyncController: IDOR fixes, remove Auth::id()??1 fallback
- ClientProjects: verify project ownership on all mutations (IDOR)
- CompanyManagement: Admin role check on mount() and mutations (auth bypass)
- ProjectMap: scope feature/template lookups to current project (IDOR x5)
- PhaseList/TemplateManager/LayerManager: scope mutations to owned resources (IDOR)
- ProjectEditTabs: Gate::authorize on mount() and updateProject()
- routes/web.php: reports routes moved inside can:manage all middleware (auth bypass)

MEDIUM:
- layer-manager: escapeHtml() on Leaflet popup interpolations (XSS)
- MediaManager: server-side MIME validation + 50MB limit
- ProjectList/ProjectUsers/ProjectCompanies/PhaseProgress: auth checks added
- AdminUsers/ReportsDashboard/ExportController: role/permission checks added

LOW:
- config/session.php: secure cookie tied to production env
- OfflineSyncController: sanitize storage path (path traversal)

UI integration:
- project-map: Issues tab (4th) with open-count badge
- project-map: project navigation bar (Dashboard/Map/Gantt/Report/Issues)
- project-dashboard: action buttons for Map/Gantt/Report/Issues
- project-form: validation error summary + per-field @error spans
- template-manager: validation error display

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-16 18:25:36 +02:00
parent 7d854ffb0a
commit f8a1310c0f
26 changed files with 1166 additions and 1195 deletions
@@ -2,27 +2,40 @@
<div class="max-w-2xl mx-auto p-4">
<h1 class="text-2xl font-bold mb-6">{{ $projectId ? __('Edit Project') : __('New Project') }}</h1>
@if($errors->any())
<div class="alert alert-error text-sm mb-4">
<x-heroicon-o-exclamation-circle class="w-5 h-5 shrink-0" />
<ul class="list-disc pl-3 space-y-0.5">
@foreach($errors->all() as $e) <li>{{ $e }}</li> @endforeach
</ul>
</div>
@endif
<form wire:submit.prevent="save" class="space-y-6">
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<label class="block text-sm font-medium mb-2">{{ __('Name') }}</label>
<input type="text" wire:model="name" class="input input-bordered w-full" placeholder="{{ __('Project name') }}" required>
<input type="text" wire:model="name" class="input input-bordered w-full {{ $errors->has('name') ? 'input-error' : '' }}" placeholder="{{ __('Project name') }}" required>
@error('name') <span class="text-error text-xs mt-1">{{ $message }}</span> @enderror
</div>
<div>
<label class="block text-sm font-medium mb-2">{{ __('Address') }}</label>
<input type="text" wire:model="address" class="input input-bordered w-full" placeholder="{{ __('Address') }}">
<input type="text" wire:model="address" class="input input-bordered w-full" placeholder="{{ __('Street address, city, etc.') }}">
@error('address') <span class="text-error text-xs mt-1">{{ $message }}</span> @enderror
</div>
<div>
<label class="block text-sm font-medium mb-2">{{ __('Coordinates') }}</label>
<input type="text" wire:model="country" class="input input-bordered w-full" placeholder="{{ __('No country') }}" readonly>
<label class="block text-sm font-medium mb-2">{{ __('Country') }}</label>
<input type="text" wire:model="country" class="input input-bordered w-full" placeholder="{{ __('Country (auto-filled)') }}" readonly>
</div>
<div class="md:col-span-2">
<label class="block text-sm font-medium mb-2">{{ __('Start date') }}</label>
<input type="date" wire:model="start_date" class="input input-bordered w-full" required>
<label class="block text-sm font-medium mb-2">{{ __('Start Date') }}</label>
<input type="date" wire:model="start_date" class="input input-bordered w-full {{ $errors->has('start_date') ? 'input-error' : '' }}" required>
@error('start_date') <span class="text-error text-xs mt-1">{{ $message }}</span> @enderror
</div>
<div class="md:col-span-2">
<label class="block text-sm font-medium mb-2">{{ __('Estimated end date') }}</label>
<input type="date" wire:model="end_date_estimated" class="input input-bordered w-full">
<label class="block text-sm font-medium mb-2">{{ __('End Date (estimated)') }}</label>
<input type="date" wire:model="end_date_estimated" class="input input-bordered w-full {{ $errors->has('end_date_estimated') ? 'input-error' : '' }}">
@error('end_date_estimated') <span class="text-error text-xs mt-1">{{ $message }}</span> @enderror
</div>
<div class="md:col-span-2">
<label class="block text-sm font-medium mb-2">{{ __('Status') }}</label>
@@ -32,13 +45,14 @@
<option value="paused">{{ __('Paused') }}</option>
<option value="completed">{{ __('Completed') }}</option>
</select>
@error('status') <span class="text-error text-xs mt-1">{{ $message }}</span> @enderror
</div>
</div>
<div class="border rounded-lg p-4">
<h2 class="text-xl font-bold mb-4">{{ __('Location') }}</h2>
<h2 class="text-xl font-bold mb-4">{{ __('Project Location') }}</h2>
<p class="text-sm text-gray-500 mb-2">
{{ __('Click on the map or drag the marker to update the location') }}
{{ __('Click on the map to set the project location. The address and country will be filled automatically.') }}
</p>
<div id="projectMap" style="height: 400px; width: 100%; background: #e2e8f0; border-radius: 0.5rem;"></div>
<input type="hidden" wire:model="lat">
@@ -47,7 +61,7 @@
<div class="flex justify-end space-x-4">
<button type="button" wire:click="resetForm" class="btn btn-outline">
{{ __('Cancel') }}
{{ __('Reset') }}
</button>
<button type="submit" class="btn btn-primary">
{{ $projectId ? __('Update') : __('Create') }}