im profil kann ein nutzer nun seine daten exportieren. man kann seinen account löschen. nach 2 jahren werden inaktive accounts gelöscht, 1 monat vorher wird eine email geschickt. Hilfetexte und Legal Pages in der Guest PWA korrigiert und vom layout her optimiert (dark mode).
This commit is contained in:
68
app/Jobs/AnonymizeAccount.php
Normal file
68
app/Jobs/AnonymizeAccount.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Services\Compliance\AccountAnonymizer;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class AnonymizeAccount implements ShouldQueue
|
||||
{
|
||||
use Dispatchable;
|
||||
use InteractsWithQueue;
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
public function __construct(private readonly ?int $userId = null, private readonly ?int $tenantId = null)
|
||||
{
|
||||
if ($this->userId === null && $this->tenantId === null) {
|
||||
throw new \InvalidArgumentException('An anonymization job requires either a user or tenant id.');
|
||||
}
|
||||
|
||||
$this->onQueue('default');
|
||||
}
|
||||
|
||||
public function handle(AccountAnonymizer $anonymizer): void
|
||||
{
|
||||
if ($this->userId) {
|
||||
$user = User::with('tenant')->find($this->userId);
|
||||
|
||||
if (! $user || $user->tenant?->anonymized_at) {
|
||||
return;
|
||||
}
|
||||
|
||||
$anonymizer->anonymize($user);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->tenantId) {
|
||||
$tenant = Tenant::with('user')->find($this->tenantId);
|
||||
|
||||
if (! $tenant || $tenant->anonymized_at) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($tenant->user) {
|
||||
$anonymizer->anonymize($tenant->user);
|
||||
} else {
|
||||
$anonymizer->anonymizeTenantOnly($tenant);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function userId(): ?int
|
||||
{
|
||||
return $this->userId;
|
||||
}
|
||||
|
||||
public function tenantId(): ?int
|
||||
{
|
||||
return $this->tenantId;
|
||||
}
|
||||
}
|
||||
224
app/Jobs/GenerateDataExport.php
Normal file
224
app/Jobs/GenerateDataExport.php
Normal file
@@ -0,0 +1,224 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\DataExport;
|
||||
use App\Models\Event;
|
||||
use App\Models\PackagePurchase;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use ZipArchive;
|
||||
|
||||
class GenerateDataExport implements ShouldQueue
|
||||
{
|
||||
use Dispatchable;
|
||||
use InteractsWithQueue;
|
||||
use Queueable;
|
||||
use SerializesModels;
|
||||
|
||||
public function __construct(private readonly int $exportId)
|
||||
{
|
||||
$this->onQueue('default');
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$export = DataExport::with(['user', 'tenant'])->find($this->exportId);
|
||||
|
||||
if (! $export) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! $export->user) {
|
||||
$export->update([
|
||||
'status' => DataExport::STATUS_FAILED,
|
||||
'error_message' => 'User no longer exists.',
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$export->update(['status' => DataExport::STATUS_PROCESSING, 'error_message' => null]);
|
||||
|
||||
try {
|
||||
$payload = $this->buildPayload($export->user, $export->tenant);
|
||||
$zipPath = $this->writeArchive($export, $payload);
|
||||
$export->update([
|
||||
'status' => DataExport::STATUS_READY,
|
||||
'path' => $zipPath,
|
||||
'size_bytes' => Storage::disk('local')->size($zipPath),
|
||||
'expires_at' => now()->addDays(14),
|
||||
]);
|
||||
} catch (\Throwable $exception) {
|
||||
$export->update([
|
||||
'status' => DataExport::STATUS_FAILED,
|
||||
'error_message' => $exception->getMessage(),
|
||||
]);
|
||||
|
||||
report($exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function buildPayload(User $user, ?Tenant $tenant): array
|
||||
{
|
||||
$profile = [
|
||||
'generated_at' => now()->toIso8601String(),
|
||||
'user' => [
|
||||
'id' => $user->id,
|
||||
'name' => $user->name,
|
||||
'email' => $user->email,
|
||||
'username' => $user->username,
|
||||
'preferred_locale' => $user->preferred_locale,
|
||||
'created_at' => optional($user->created_at)->toIso8601String(),
|
||||
'email_verified_at' => optional($user->email_verified_at)->toIso8601String(),
|
||||
],
|
||||
];
|
||||
|
||||
if ($tenant) {
|
||||
$profile['tenant'] = [
|
||||
'id' => $tenant->id,
|
||||
'name' => $tenant->name,
|
||||
'slug' => $tenant->slug,
|
||||
'contact_email' => $tenant->contact_email,
|
||||
'contact_phone' => $tenant->contact_phone,
|
||||
'subscription_status' => $tenant->subscription_status,
|
||||
'subscription_expires_at' => optional($tenant->subscription_expires_at)->toIso8601String(),
|
||||
'created_at' => optional($tenant->created_at)->toIso8601String(),
|
||||
];
|
||||
}
|
||||
|
||||
$events = $tenant
|
||||
? $this->collectEvents($tenant)
|
||||
: [];
|
||||
$invoices = $tenant
|
||||
? $this->collectInvoices($tenant)
|
||||
: [];
|
||||
|
||||
return [
|
||||
'profile' => $profile,
|
||||
'events' => $events,
|
||||
'invoices' => $invoices,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
protected function collectEvents(Tenant $tenant): array
|
||||
{
|
||||
$events = Event::query()
|
||||
->withCount([
|
||||
'photos as photos_total',
|
||||
'photos as featured_photos_total' => fn ($query) => $query->where('is_featured', true),
|
||||
'joinTokens',
|
||||
'members',
|
||||
])
|
||||
->where('tenant_id', $tenant->id)
|
||||
->orderBy('date')
|
||||
->get();
|
||||
|
||||
$likeCounts = DB::table('photo_likes')
|
||||
->join('photos', 'photo_likes.photo_id', '=', 'photos.id')
|
||||
->where('photos.tenant_id', $tenant->id)
|
||||
->groupBy('photos.event_id')
|
||||
->pluck(DB::raw('COUNT(*)'), 'photos.event_id');
|
||||
|
||||
return $events
|
||||
->map(function (Event $event) use ($likeCounts): array {
|
||||
$likes = (int) ($likeCounts[$event->id] ?? 0);
|
||||
|
||||
return [
|
||||
'id' => $event->id,
|
||||
'slug' => $event->slug,
|
||||
'status' => $event->status,
|
||||
'name' => $event->name,
|
||||
'location' => $event->location,
|
||||
'date' => optional($event->date)->toIso8601String(),
|
||||
'photos_total' => (int) ($event->photos_total ?? 0),
|
||||
'featured_photos_total' => (int) ($event->featured_photos_total ?? 0),
|
||||
'join_tokens_total' => (int) ($event->join_tokens_count ?? 0),
|
||||
'members_total' => (int) ($event->members_count ?? 0),
|
||||
'likes_total' => $likes,
|
||||
'created_at' => optional($event->created_at)->toIso8601String(),
|
||||
'updated_at' => optional($event->updated_at)->toIso8601String(),
|
||||
];
|
||||
})
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
protected function collectInvoices(Tenant $tenant): array
|
||||
{
|
||||
return PackagePurchase::query()
|
||||
->where('tenant_id', $tenant->id)
|
||||
->with('package')
|
||||
->latest('purchased_at')
|
||||
->get()
|
||||
->map(function (PackagePurchase $purchase): array {
|
||||
return [
|
||||
'id' => $purchase->id,
|
||||
'package' => $purchase->package?->getNameForLocale(app()->getLocale())
|
||||
?? $purchase->package?->name,
|
||||
'price' => $purchase->price !== null ? (float) $purchase->price : null,
|
||||
'currency' => 'EUR',
|
||||
'type' => $purchase->type,
|
||||
'provider' => $purchase->provider,
|
||||
'provider_id' => $purchase->provider_id,
|
||||
'purchased_at' => optional($purchase->purchased_at)->toIso8601String(),
|
||||
'refunded' => (bool) $purchase->refunded,
|
||||
];
|
||||
})
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
*/
|
||||
protected function writeArchive(DataExport $export, array $payload): string
|
||||
{
|
||||
$directory = 'exports/user-'.$export->user_id;
|
||||
Storage::disk('local')->makeDirectory($directory);
|
||||
$filename = sprintf('data-export-%s.zip', Str::uuid());
|
||||
$path = $directory.'/'.$filename;
|
||||
|
||||
$zip = new ZipArchive;
|
||||
$fullPath = Storage::disk('local')->path($path);
|
||||
|
||||
if ($zip->open($fullPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
|
||||
throw new \RuntimeException('Unable to create export archive.');
|
||||
}
|
||||
|
||||
$zip->addFromString('profile.json', json_encode($payload['profile'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
$zip->addFromString('events.json', json_encode($payload['events'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
$zip->addFromString('invoices.json', json_encode($payload['invoices'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
||||
|
||||
$locale = $export->user?->preferred_locale ?? app()->getLocale();
|
||||
$readme = implode("\n", [
|
||||
__('profile.export.readme.title', [], $locale),
|
||||
'---------------------',
|
||||
__('profile.export.readme.description', [], $locale),
|
||||
__('profile.export.readme.generated', [
|
||||
'date' => now()->copy()->locale($locale)->translatedFormat('d. F Y H:i'),
|
||||
], $locale),
|
||||
__('profile.export.readme.expiry', [], $locale),
|
||||
]);
|
||||
$zip->addFromString('README.txt', $readme);
|
||||
|
||||
$zip->close();
|
||||
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user