Files
construprogress/app/Livewire/Client/ClientProjects.php
T
javier f8a1310c0f 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>
2026-06-16 18:25:36 +02:00

167 lines
4.7 KiB
PHP

<?php
namespace App\Livewire\Client;
use Livewire\Component;
use App\Models\Project;
use App\Models\ChangeOrder;
class ClientProjects extends Component
{
public $projects = [];
public $selectedProject = null;
public $projectDetails = [];
public $galleryImages = [];
public $changeOrders = [];
public function mount()
{
$this->loadProjects();
}
public function loadProjects()
{
$user = auth()->user();
$this->projects = $user->projects()
->wherePivot('role_in_project', 'client')
->with(['phases' => function ($query) {
$query->select('id', 'project_id', 'name', 'progress_percent');
}])
->get()
->toArray();
}
/**
* Return only project IDs the current user can access as client.
*/
private function accessibleProjectIds(): \Illuminate\Support\Collection
{
return auth()->user()->projects()
->wherePivot('role_in_project', 'client')
->pluck('projects.id');
}
public function selectProject($projectId)
{
// Verify the project is one the user is a client on
if (!$this->accessibleProjectIds()->contains((int) $projectId)) {
abort(403);
}
$this->selectedProject = (int) $projectId;
$this->loadProjectDetails();
}
public function loadProjectDetails()
{
if (!$this->selectedProject) {
return;
}
// Re-verify ownership on every load
if (!$this->accessibleProjectIds()->contains($this->selectedProject)) {
abort(403);
}
$project = Project::with([
'phases',
'changeOrders',
])->find($this->selectedProject);
if (!$project) {
return;
}
$this->projectDetails = [
'id' => $project->id,
'name' => $project->name,
'description'=> $project->description ?? '',
'start_date' => $project->start_date,
'end_date' => $project->end_date_estimated,
'status' => $project->status,
'progress' => round($project->phases->avg('progress_percent') ?? 0),
];
$mediaImages = $project->media()
->where('category', 'image')
->latest()
->take(3)
->get()
->map(fn ($media) => [
'url' => $media->url,
'title' => $media->name,
'date' => $media->created_at->format('d/m/Y'),
])
->toArray();
$this->galleryImages = $mediaImages ?: [];
$this->changeOrders = $project->changeOrders
->sortByDesc('requested_at')
->map(fn ($order) => [
'id' => $order->id,
'title' => $order->title,
'description' => $order->description,
'status' => $order->status,
'requested_at' => $order->requested_at?->format('d/m/Y') ?? '',
'amount' => $order->amount,
])
->values()
->toArray();
}
public function approveChangeOrder($orderId)
{
$changeOrder = ChangeOrder::where('id', $orderId)
->where('project_id', $this->selectedProject)
->first();
if (!$changeOrder) {
abort(403);
}
// Verify this project is accessible by the current user
if (!$this->accessibleProjectIds()->contains($this->selectedProject)) {
abort(403);
}
$changeOrder->update([
'status' => 'approved',
'responded_at' => now()->toDateString(),
'responded_by' => auth()->id(),
]);
$this->loadProjectDetails();
$this->dispatch('changeOrderUpdated', ['id' => $orderId, 'status' => 'approved']);
}
public function rejectChangeOrder($orderId)
{
$changeOrder = ChangeOrder::where('id', $orderId)
->where('project_id', $this->selectedProject)
->first();
if (!$changeOrder) {
abort(403);
}
// Verify this project is accessible by the current user
if (!$this->accessibleProjectIds()->contains($this->selectedProject)) {
abort(403);
}
$changeOrder->update([
'status' => 'rejected',
'responded_at' => now()->toDateString(),
'responded_by' => auth()->id(),
]);
$this->loadProjectDetails();
$this->dispatch('changeOrderUpdated', ['id' => $orderId, 'status' => 'rejected']);
}
public function render()
{
return view('livewire.client.client-projects');
}
}