\App\Models\Project::class, 'phase' => \App\Models\Phase::class, 'layer' => \App\Models\Layer::class, 'feature' => \App\Models\Feature::class, 'inspection' => \App\Models\Inspection::class, 'issue' => \App\Models\Issue::class, ]; public function storePending(Request $request) { $payload = $request->validate([ 'action' => 'required|in:progress_update,inspection,feature_create,media_upload,task_complete', 'payload' => 'required|array', ]); PendingSync::create([ 'user_id' => Auth::id(), 'action' => $payload['action'], 'payload' => $payload['payload'], ]); return response()->json(['queued' => true]); } public function sync(Request $request) { $user = Auth::user(); $pendings = PendingSync::where('user_id', $user->id)->whereNull('synced_at')->get(); $results = []; foreach ($pendings as $pending) { $result = ['id' => $pending->id, 'action' => $pending->action, 'success' => false, 'error' => null]; try { if ($pending->action === 'progress_update') { $phaseId = (int) ($pending->payload['phase_id'] ?? 0); $progress = (int) ($pending->payload['progress'] ?? 0); $progress = max(0, min(100, $progress)); $phase = Phase::find($phaseId); if ($phase) { // Verify user has access to this phase's project if (!$user->hasRole('Admin') && !$phase->project->users()->where('user_id', $user->id)->exists()) { $result['error'] = 'Access denied to this project.'; } else { $phase->progress_percent = $progress; $phase->save(); $phase->progressUpdates()->create([ 'user_id' => $user->id, 'progress_percent' => $progress, 'comment' => substr($pending->payload['comment'] ?? '', 0, 500), ]); $result['success'] = true; } } else { $result['error'] = 'Phase not found.'; } } elseif ($pending->action === 'inspection') { $p = $pending->payload; $inspection = Inspection::create([ 'project_id' => (int) ($p['project_id'] ?? 0), 'feature_id' => isset($p['feature_id']) ? (int) $p['feature_id'] : null, 'layer_id' => isset($p['layer_id']) ? (int) $p['layer_id'] : null, 'template_id' => isset($p['template_id'])? (int) $p['template_id']: null, 'user_id' => $user->id, 'inspector_user_id' => $user->id, 'status' => 'completed', 'completed_at' => now(), 'result' => in_array($p['result'] ?? '', Inspection::RESULTS) ? $p['result'] : null, 'notes' => substr($p['notes'] ?? '', 0, 2000), 'data' => is_array($p['data'] ?? null) ? $p['data'] : [], ]); $result['success'] = true; $result['data'] = ['inspection_id' => $inspection->id]; } elseif ($pending->action === 'feature_create') { $p = $pending->payload; $feature = Feature::create([ 'layer_id' => (int) ($p['layer_id'] ?? 0), 'name' => substr($p['name'] ?? 'Elemento', 0, 255), 'geometry' => is_array($p['geometry'] ?? null) ? $p['geometry'] : null, 'properties' => is_array($p['properties'] ?? null) ? $p['properties'] : [], 'template_id' => isset($p['template_id']) ? (int) $p['template_id'] : null, 'progress' => max(0, min(100, (int) ($p['progress'] ?? 0))), 'status' => in_array($p['status'] ?? '', Feature::STATUSES) ? $p['status'] : 'planned', 'responsible' => isset($p['responsible']) ? substr($p['responsible'], 0, 255) : null, ]); $result['success'] = true; $result['data'] = ['feature_id' => $feature->id]; } elseif ($pending->action === 'media_upload') { if (isset($pending->payload['file'], $pending->payload['path'])) { // Restrict path to safe uploads directory $safePath = 'uploads/' . ltrim(basename($pending->payload['path']), '/'); $decoded = base64_decode($pending->payload['file'], true); if ($decoded !== false) { Storage::disk('public')->put($safePath, $decoded); // Whitelist-based model type resolution (prevents RCE) if (isset($pending->payload['model_type'], $pending->payload['model_id'])) { $typeKey = strtolower(trim($pending->payload['model_type'])); if (array_key_exists($typeKey, self::ALLOWED_MEDIABLE_TYPES)) { $modelClass = self::ALLOWED_MEDIABLE_TYPES[$typeKey]; $model = $modelClass::find((int) $pending->payload['model_id']); if ($model) { $model->media()->create([ 'name' => substr($pending->payload['name'] ?? 'unnamed', 0, 255), 'file_path' => $safePath, 'file_type' => substr($pending->payload['mime_type'] ?? 'application/octet-stream', 0, 100), 'file_extension' => pathinfo($safePath, PATHINFO_EXTENSION), 'file_size' => strlen($decoded), 'category' => 'other', 'uploaded_by' => $user->id, ]); } } } $result['success'] = true; $result['data'] = ['path' => $safePath]; } else { $result['error'] = 'Failed to decode base64 file.'; } } else { $result['error'] = 'Missing file or path in payload.'; } } elseif ($pending->action === 'task_complete') { // No-op placeholder, just mark as synced $result['success'] = true; } else { $result['error'] = 'Unknown action type.'; } } catch (\Exception $e) { $result['error'] = $e->getMessage(); } if ($result['success']) { $pending->synced_at = now(); $pending->save(); } $results[] = $result; } return response()->json(['synced' => $results]); } }