restore: bring back f8a1310 (security review) state

Restores all files to the f8a1310 security-review snapshot as requested,
plus the 2 boot-critical fixes from a24c8a2 (config/session.php env()
instead of app()->environment(), and removal of the duplicate $activeTab
in ProjectMap.php) so the application actually boots.

Forward commit, no history rewrite. The 7d854ff state remains in history.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-17 10:36:44 +02:00
parent c44958ac16
commit 941dbd5997
26 changed files with 1163 additions and 1196 deletions
+87 -95
View File
@@ -4,19 +4,15 @@ namespace App\Livewire\Client;
use Livewire\Component;
use App\Models\Project;
use App\Models\Phase;
use App\Models\Inspection;
use App\Models\Feature;
use App\Models\ChangeOrder;
use Carbon\Carbon;
class ClientProjects extends Component
{
public $projects = [];
public $projects = [];
public $selectedProject = null;
public $projectDetails = [];
public $galleryImages = [];
public $changeOrders = [];
public $projectDetails = [];
public $galleryImages = [];
public $changeOrders = [];
public function mount()
{
@@ -25,20 +21,33 @@ class ClientProjects extends Component
public function loadProjects()
{
// Get projects where the user has the 'client' role
$user = auth()->user();
$this->projects = $user->projects()
->wherePivot('role_in_project', 'client')
->with(['phases' => function($query) {
->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)
{
$this->selectedProject = $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();
}
@@ -48,10 +57,14 @@ class ClientProjects extends Component
return;
}
// Re-verify ownership on every load
if (!$this->accessibleProjectIds()->contains($this->selectedProject)) {
abort(403);
}
$project = Project::with([
'phases.features',
'inspections.template',
'changeOrders' // Load change orders for this project
'phases',
'changeOrders',
])->find($this->selectedProject);
if (!$project) {
@@ -59,112 +72,91 @@ class ClientProjects extends Component
}
$this->projectDetails = [
'id' => $project->id,
'name' => $project->name,
'description' => $project->description,
'id' => $project->id,
'name' => $project->name,
'description'=> $project->description ?? '',
'start_date' => $project->start_date,
'end_date' => $project->end_date,
'status' => $project->status,
'progress' => $project->phases->avg('progress_percent') ?? 0,
'end_date' => $project->end_date_estimated,
'status' => $project->status,
'progress' => round($project->phases->avg('progress_percent') ?? 0),
];
// Get recent images (we can fetch from media table if needed, but for now we'll keep simulated or link to real)
// For simplicity, we'll try to get some media images for the project
$mediaImages = $project->media()
->where('category', 'image')
->latest()
->take(3)
->get()
->map(function($media) {
return [
'url' => $media->url,
'title' => $media->name,
'date' => $media->created_at->format('d/m/Y')
];
})
->map(fn ($media) => [
'url' => $media->url,
'title' => $media->name,
'date' => $media->created_at->format('d/m/Y'),
])
->toArray();
// If we don't have 3 images, we can fallback to placeholders or just use what we have
if (count($mediaImages) > 0) {
$this->galleryImages = $mediaImages;
} else {
// Fallback to placeholders
$this->galleryImages = [
[
'url' => 'https://via.placeholder.com/400x300?text=Avance+1',
'title' => 'Avance inicial',
'date' => now()->subDays(30)->format('d/m/Y')
],
[
'url' => 'https://via.placeholder.com/400x300?text=Avance+2',
'title' => 'Estructura levantada',
'date' => now()->subDays(15)->format('d/m/Y')
],
[
'url' => 'https://via.placeholder.com/400x300?text=Avance+3',
'title' => 'Instalaciones',
'date' => now()->subDays(5)->format('d/m/Y')
]
];
}
$this->galleryImages = $mediaImages ?: [];
// Get change orders for this project
$this->changeOrders = $project->changeOrders
->orderBy('requested_at', 'desc')
->get()
->map(function($order) {
return [
'id' => $order->id,
'title' => $order->title,
'description' => $order->description,
'status' => $order->status,
'requested_at' => $order->requested_at->format('d/m/Y'),
'amount' => $order->amount
];
})
->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)
{
// Update the change order in the database
$changeOrder = ChangeOrder::find($orderId);
if ($changeOrder) {
// Check that the change order belongs to the selected project (security)
if ($changeOrder->project_id == $this->selectedProject) {
$changeOrder->status = 'approved';
$changeOrder->responded_at = now()->toDateString();
$changeOrder->responded_by = auth()->id();
$changeOrder->save();
$changeOrder = ChangeOrder::where('id', $orderId)
->where('project_id', $this->selectedProject)
->first();
// Refresh the change orders list
$this->loadProjectDetails();
// Notify any listeners (optional)
$this->dispatch('changeOrderUpdated', ['id' => $orderId, 'status' => 'approved']);
}
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)
{
// Update the change order in the database
$changeOrder = ChangeOrder::find($orderId);
if ($changeOrder) {
// Check that the change order belongs to the selected project (security)
if ($changeOrder->project_id == $this->selectedProject) {
$changeOrder->status = 'rejected';
$changeOrder->responded_at = now()->toDateString();
$changeOrder->responded_by = auth()->id();
$changeOrder->save();
$changeOrder = ChangeOrder::where('id', $orderId)
->where('project_id', $this->selectedProject)
->first();
// Refresh the change orders list
$this->loadProjectDetails();
// Notify any listeners (optional)
$this->dispatch('changeOrderUpdated', ['id' => $orderId, 'status' => 'rejected']);
}
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()