where('user_id', $user->id) ->orderByDesc('created_at') ->paginate(10); $this->viewData = array_merge($this->viewData, [ 'user' => $user, 'gdprRequests' => $requests, 'retentionPolicy' => $retentionService->policy(), 'meta_title' => 'Privacy Center', 'meta_keywords' => 'privacy,gdpr,data export,erasure', 'meta_description' => 'Manage your privacy and GDPR data requests', ]); return view('privacy-center.index', $this->viewData); } public function requestExport(Request $request, GdprExportService $exportService, GdprAuditService $auditService): RedirectResponse { /** @var User $user */ $user = Auth::user(); $validated = $request->validate([ 'notes' => ['nullable', 'string', 'max:2000'], ]); // Block regeneration while a previously generated export is still downloadable. // This bounds the heavy, synchronous export work and avoids accumulating PII files. $activeExport = GdprRequest::query() ->where('user_id', $user->id) ->where('type', GdprRequest::TYPE_EXPORT) ->where('status', GdprRequest::STATUS_COMPLETED) ->whereNotNull('export_path') ->where(function ($query): void { $query->whereNull('export_expires_at') ->orWhere('export_expires_at', '>', now()); }) ->exists(); if ($activeExport) { return redirect() ->route('privacy-center.index') ->with('error', 'You already have an active data export available for download. Please use or wait for it to expire before requesting a new one.'); } $gdprRequest = GdprRequest::create([ 'user_id' => $user->id, 'requester_username' => $user->username, 'requester_email' => $user->email, 'type' => GdprRequest::TYPE_EXPORT, 'status' => GdprRequest::STATUS_PROCESSING, 'request_payload' => [ 'requested_by' => 'self_service', 'ip_recorded' => (bool) config('nntmux_settings.store_user_ip'), ], 'notes' => $validated['notes'] ?? null, ]); $auditService->record( event: 'request_submitted', description: 'Self-service GDPR export request submitted.', subject: $user, actor: $user, request: $gdprRequest, metadata: ['type' => GdprRequest::TYPE_EXPORT] ); $exportService->generate($user, $gdprRequest, $user); return redirect() ->route('privacy-center.index') ->with('success', 'Your data export has been generated and is available for 7 days.'); } public function requestErasure(Request $request, GdprAuditService $auditService, GdprNotificationService $notificationService): RedirectResponse { /** @var User $user */ $user = Auth::user(); if ($user->hasRole('Admin')) { return redirect() ->route('privacy-center.index') ->with('error', 'Admin accounts cannot submit self-service erasure requests.'); } $validated = $request->validate([ 'confirmation' => ['required', 'string', 'in:ERASE'], 'notes' => ['nullable', 'string', 'max:2000'], ]); $openRequestExists = GdprRequest::query() ->where('user_id', $user->id) ->where('type', GdprRequest::TYPE_ERASURE) ->open() ->exists(); if ($openRequestExists) { return redirect() ->route('privacy-center.index') ->with('error', 'You already have an open account erasure request.'); } $gdprRequest = GdprRequest::create([ 'user_id' => $user->id, 'requester_username' => $user->username, 'requester_email' => $user->email, 'type' => GdprRequest::TYPE_ERASURE, 'status' => GdprRequest::STATUS_PENDING, 'request_payload' => [ 'requested_by' => 'self_service', 'confirmation' => $validated['confirmation'], ], 'notes' => $validated['notes'] ?? null, ]); $auditService->record( event: 'request_submitted', description: 'Self-service GDPR erasure request submitted.', subject: $user, actor: $user, request: $gdprRequest, metadata: ['type' => GdprRequest::TYPE_ERASURE] ); $notificationService->requestSubmitted($gdprRequest); return redirect() ->route('privacy-center.index') ->with('success', 'Your account erasure request has been submitted for administrator review.'); } public function downloadExport(GdprRequest $gdprRequest): StreamedResponse|RedirectResponse { /** @var User $user */ $user = Auth::user(); if ((int) $gdprRequest->user_id !== (int) $user->id || ! $gdprRequest->isDownloadableExport()) { return redirect()->route('privacy-center.index')->with('error', 'The requested export is not available.'); } $disk = $gdprRequest->export_disk ?: 'local'; if (! Storage::disk($disk)->exists($gdprRequest->export_path)) { return redirect()->route('privacy-center.index')->with('error', 'The requested export file has expired or is missing.'); } return Storage::disk($disk)->download($gdprRequest->export_path, 'gdpr-export-'.$user->id.'.json'); } }