2026-05-07 23:31:33 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
|
|
|
|
|
|
use App\Models\PendingSync;
|
|
|
|
|
use App\Models\Phase;
|
2026-05-25 17:59:03 +02:00
|
|
|
use App\Models\Inspection;
|
|
|
|
|
use App\Models\Feature;
|
|
|
|
|
use App\Models\Media;
|
2026-05-07 23:31:33 +02:00
|
|
|
use Illuminate\Http\Request;
|
|
|
|
|
use Illuminate\Support\Facades\Auth;
|
2026-05-25 17:59:03 +02:00
|
|
|
use Illuminate\Support\Facades\Storage;
|
2026-05-07 23:31:33 +02:00
|
|
|
|
|
|
|
|
class OfflineSyncController extends Controller
|
|
|
|
|
{
|
2026-06-17 10:36:44 +02:00
|
|
|
/**
|
|
|
|
|
* Allowed mediable model types (whitelist to prevent RCE via dynamic instantiation).
|
|
|
|
|
*/
|
|
|
|
|
private const ALLOWED_MEDIABLE_TYPES = [
|
|
|
|
|
'project' => \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,
|
|
|
|
|
];
|
|
|
|
|
|
2026-05-07 23:31:33 +02:00
|
|
|
public function storePending(Request $request)
|
|
|
|
|
{
|
|
|
|
|
$payload = $request->validate([
|
2026-06-17 10:36:44 +02:00
|
|
|
'action' => 'required|in:progress_update,inspection,feature_create,media_upload,task_complete',
|
2026-05-07 23:31:33 +02:00
|
|
|
'payload' => 'required|array',
|
|
|
|
|
]);
|
2026-06-17 10:36:44 +02:00
|
|
|
PendingSync::create([
|
|
|
|
|
'user_id' => Auth::id(),
|
|
|
|
|
'action' => $payload['action'],
|
2026-05-07 23:31:33 +02:00
|
|
|
'payload' => $payload['payload'],
|
|
|
|
|
]);
|
|
|
|
|
return response()->json(['queued' => true]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function sync(Request $request)
|
|
|
|
|
{
|
2026-06-17 10:36:44 +02:00
|
|
|
$user = Auth::user();
|
2026-05-07 23:31:33 +02:00
|
|
|
$pendings = PendingSync::where('user_id', $user->id)->whereNull('synced_at')->get();
|
2026-05-25 17:59:03 +02:00
|
|
|
$results = [];
|
2026-06-17 10:36:44 +02:00
|
|
|
|
2026-05-07 23:31:33 +02:00
|
|
|
foreach ($pendings as $pending) {
|
2026-05-25 17:59:03 +02:00
|
|
|
$result = ['id' => $pending->id, 'action' => $pending->action, 'success' => false, 'error' => null];
|
2026-06-17 10:36:44 +02:00
|
|
|
|
2026-05-25 17:59:03 +02:00
|
|
|
try {
|
|
|
|
|
if ($pending->action === 'progress_update') {
|
2026-06-17 10:36:44 +02:00
|
|
|
$phaseId = (int) ($pending->payload['phase_id'] ?? 0);
|
|
|
|
|
$progress = (int) ($pending->payload['progress'] ?? 0);
|
|
|
|
|
$progress = max(0, min(100, $progress));
|
|
|
|
|
|
|
|
|
|
$phase = Phase::find($phaseId);
|
2026-05-25 17:59:03 +02:00
|
|
|
if ($phase) {
|
2026-06-17 10:36:44 +02:00
|
|
|
// 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.';
|
2026-05-25 17:59:03 +02:00
|
|
|
}
|
2026-06-17 10:36:44 +02:00
|
|
|
|
2026-05-25 17:59:03 +02:00
|
|
|
} elseif ($pending->action === 'inspection') {
|
2026-06-17 10:36:44 +02:00
|
|
|
$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'] : [],
|
|
|
|
|
]);
|
2026-05-25 17:59:03 +02:00
|
|
|
$result['success'] = true;
|
2026-06-17 10:36:44 +02:00
|
|
|
$result['data'] = ['inspection_id' => $inspection->id];
|
|
|
|
|
|
2026-05-25 17:59:03 +02:00
|
|
|
} elseif ($pending->action === 'feature_create') {
|
2026-06-17 10:36:44 +02:00
|
|
|
$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,
|
|
|
|
|
]);
|
2026-05-25 17:59:03 +02:00
|
|
|
$result['success'] = true;
|
2026-06-17 10:36:44 +02:00
|
|
|
$result['data'] = ['feature_id' => $feature->id];
|
|
|
|
|
|
2026-05-25 17:59:03 +02:00
|
|
|
} elseif ($pending->action === 'media_upload') {
|
|
|
|
|
if (isset($pending->payload['file'], $pending->payload['path'])) {
|
2026-06-17 10:36:44 +02:00
|
|
|
// Restrict path to safe uploads directory
|
|
|
|
|
$safePath = 'uploads/' . ltrim(basename($pending->payload['path']), '/');
|
|
|
|
|
$decoded = base64_decode($pending->payload['file'], true);
|
|
|
|
|
|
2026-05-25 17:59:03 +02:00
|
|
|
if ($decoded !== false) {
|
2026-06-17 10:36:44 +02:00
|
|
|
Storage::disk('public')->put($safePath, $decoded);
|
|
|
|
|
|
|
|
|
|
// Whitelist-based model type resolution (prevents RCE)
|
2026-05-25 17:59:03 +02:00
|
|
|
if (isset($pending->payload['model_type'], $pending->payload['model_id'])) {
|
2026-06-17 10:36:44 +02:00
|
|
|
$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,
|
|
|
|
|
]);
|
|
|
|
|
}
|
2026-05-25 17:59:03 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
$result['success'] = true;
|
2026-06-17 10:36:44 +02:00
|
|
|
$result['data'] = ['path' => $safePath];
|
2026-05-25 17:59:03 +02:00
|
|
|
} else {
|
2026-06-17 10:36:44 +02:00
|
|
|
$result['error'] = 'Failed to decode base64 file.';
|
2026-05-25 17:59:03 +02:00
|
|
|
}
|
|
|
|
|
} else {
|
2026-06-17 10:36:44 +02:00
|
|
|
$result['error'] = 'Missing file or path in payload.';
|
2026-05-25 17:59:03 +02:00
|
|
|
}
|
2026-06-17 10:36:44 +02:00
|
|
|
|
2026-05-25 17:59:03 +02:00
|
|
|
} elseif ($pending->action === 'task_complete') {
|
2026-06-17 10:36:44 +02:00
|
|
|
// No-op placeholder, just mark as synced
|
2026-05-25 17:59:03 +02:00
|
|
|
$result['success'] = true;
|
2026-06-17 10:36:44 +02:00
|
|
|
|
2026-05-25 17:59:03 +02:00
|
|
|
} else {
|
2026-06-17 10:36:44 +02:00
|
|
|
$result['error'] = 'Unknown action type.';
|
2026-05-07 23:31:33 +02:00
|
|
|
}
|
2026-05-25 17:59:03 +02:00
|
|
|
} catch (\Exception $e) {
|
|
|
|
|
$result['error'] = $e->getMessage();
|
2026-05-07 23:31:33 +02:00
|
|
|
}
|
2026-05-25 17:59:03 +02:00
|
|
|
|
|
|
|
|
if ($result['success']) {
|
|
|
|
|
$pending->synced_at = now();
|
|
|
|
|
$pending->save();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$results[] = $result;
|
2026-05-07 23:31:33 +02:00
|
|
|
}
|
2026-06-17 10:36:44 +02:00
|
|
|
|
2026-05-25 17:59:03 +02:00
|
|
|
return response()->json(['synced' => $results]);
|
2026-05-07 23:31:33 +02:00
|
|
|
}
|
2026-05-25 17:59:03 +02:00
|
|
|
}
|