From 68bc1bf38abab695bac958122ab9a3a9a240adf1 Mon Sep 17 00:00:00 2001 From: Levin Date: Sat, 23 Mar 2024 13:08:56 +0100 Subject: [PATCH 01/19] Add basic gdpr export without any ui --- .idea/php.xml | 3 +- app/Enum/ExportableColumn.php | 2 + app/Http/Controllers/GdprExportController.php | 21 +++++ app/Jobs/MonitoredPersonalDataExportJob.php | 15 ++++ app/Models/MastodonServer.php | 1 + app/Models/OAuthClient.php | 4 + app/Models/Status.php | 3 + app/Models/User.php | 78 ++++++++++++++++++- app/Models/WebhookCreationRequest.php | 1 + composer.json | 1 + composer.lock | 77 ++++++++++++++++++ config/filesystems.php | 5 ++ config/personal-data-export.php | 39 ++++++++++ 13 files changed, 248 insertions(+), 2 deletions(-) create mode 100644 app/Http/Controllers/GdprExportController.php create mode 100644 app/Jobs/MonitoredPersonalDataExportJob.php create mode 100644 config/personal-data-export.php diff --git a/.idea/php.xml b/.idea/php.xml index 8c4dff3ce..0edf7a9c6 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -247,6 +247,7 @@ + @@ -339,4 +340,4 @@ - \ No newline at end of file + diff --git a/app/Enum/ExportableColumn.php b/app/Enum/ExportableColumn.php index 753f1cd48..c25e47d02 100644 --- a/app/Enum/ExportableColumn.php +++ b/app/Enum/ExportableColumn.php @@ -33,4 +33,6 @@ public function title(): string { } return $title; } + +// public } diff --git a/app/Http/Controllers/GdprExportController.php b/app/Http/Controllers/GdprExportController.php new file mode 100644 index 000000000..af3badbe7 --- /dev/null +++ b/app/Http/Controllers/GdprExportController.php @@ -0,0 +1,21 @@ + 'integer', ]; + protected $hidden = ['client_id', 'client_secret']; public function socialProfiles(): HasMany { return $this->hasMany(SocialLoginProfile::class, 'mastodon_server', 'id'); diff --git a/app/Models/OAuthClient.php b/app/Models/OAuthClient.php index ba6ec747b..d7c42cb99 100644 --- a/app/Models/OAuthClient.php +++ b/app/Models/OAuthClient.php @@ -29,6 +29,10 @@ class OAuthClient extends PassportClient { 'revoked' => 'bool', ]; + protected $hidden = [ + 'secret', + ]; + public static function newFactory() { return parent::newFactory(); } diff --git a/app/Models/Status.php b/app/Models/Status.php index 6099e6f03..b131deefe 100644 --- a/app/Models/Status.php +++ b/app/Models/Status.php @@ -107,6 +107,9 @@ public function getFavoritedAttribute(): ?bool { } public function getDescriptionAttribute(): string { + if ($this->checkin === null) { + return $this->body ?? ''; + } return __('description.status', [ 'username' => $this->user->name, 'origin' => $this->checkin->originStation->name . diff --git a/app/Models/User.php b/app/Models/User.php index 7c97a39da..6de6af745 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -6,6 +6,7 @@ use App\Enum\StatusVisibility; use App\Exceptions\RateLimitExceededException; use App\Http\Controllers\Backend\Social\MastodonProfileDetails; +use App\Http\Controllers\Backend\User\TokenController; use App\Jobs\SendVerificationEmail; use Carbon\Carbon; use Illuminate\Contracts\Auth\MustVerifyEmail; @@ -18,11 +19,14 @@ use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\DatabaseNotification; use Illuminate\Notifications\Notifiable; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\RateLimiter; use Laravel\Passport\HasApiTokens; use Mastodon; use Spatie\Permission\Traits\HasRoles; +use Spatie\PersonalDataExport\ExportsPersonalData; +use Spatie\PersonalDataExport\PersonalDataSelection; /** * @property int id @@ -52,7 +56,7 @@ * @todo remove "twitterUrl" (Twitter isn't used by traewelling anymore) * @mixin Builder */ -class User extends Authenticatable implements MustVerifyEmail +class User extends Authenticatable implements MustVerifyEmail, ExportsPersonalData { use Notifiable, HasApiTokens, HasFactory, HasRoles; @@ -284,4 +288,76 @@ public function preferredLocale(): string { protected function getDefaultGuardName(): string { return 'web'; } + + protected function oAuthClients(): HasMany { + return $this->hasMany(OAuthClient::class, 'user_id', 'id'); + } + + public function selectPersonalData(PersonalDataSelection $personalDataSelection): void { + $user = $this->toArray(); + $user['email'] = $this->email; + $user['email_verified_at'] = $this->email_verified_at; + $user['privacy_ack_at'] = $this->privacy_ack_at; + $user['last_login'] = $this->last_login; + $user['created_at'] = $this->created_at; + $user['updated_at'] = $this->updated_at; + + $webhooks = $this->webhooks()->with('events')->get(); + $webhooks = $webhooks->map(function($webhook) { + $webhook['created_at'] = $webhook->created_at; + $webhook['updated_at'] = $webhook->updated_at; + $webhook['client_id'] = (int) $webhook->oauth_client_id ?? null; + unset($webhook['url']); + return $webhook; + }); + + + if ($this->avatar && file_exists(public_path('/uploads/avatars/' . $this->avatar))) { + $personalDataSelection + ->addFile(public_path('/uploads/avatars/' . $this->avatar)); + } + + $personalDataSelection + ->add('user.json', $user) + ->add('statuses.json', $this->statuses()->with('tags')->get()) + ->add('notifications.json', $this->notifications()->get()->toJson()) + ->add('likes.json', $this->likes()->get()->toJson()) + ->add('social_profile.json', $this->socialProfile()->with('mastodonserver')->get()) + ->add('event_suggestions.json', EventSuggestion::where('user_id', $this->id)->get()->toJson()) + ->add('events.json', Event::where('approved_by', $this->id)->get()->toJson()) + ->add('webhooks.json', $webhooks) + ->add( + 'webhook_creation_requests.json', + WebhookCreationRequest::where('user_id', $this->id)->get()->toJson() + ) + ->add('tokens.json', TokenController::index($this)->toJson()) + ->add('ics_tokens.json', $this->icsTokens()->get()->toJson()) + ->add( + 'password_resets.json', + DB::table('password_resets')->select(['email','created_at'])->where('email', $this->email)->get() + ) + ->add('apps.json', $this->oAuthClients()->get()->toJson()) + ->add('follows.json', DB::table('follows')->where('user_id', $this->id)->get()) + ->add('followings.json', DB::table('follows')->where('follow_id', $this->id)->get()) + ->add('blocks.json', DB::table('user_blocks')->where('user_id', $this->id)->get()) + ->add('blocked_by.json', DB::table('user_blocks')->where('blocked_id', $this->id)->get()) + ->add('mutes.json', DB::table('user_mutes')->where('user_id', $this->id)->get()) + ->add('muted_by.json', DB::table('user_mutes')->where('muted_id', $this->id)->get()) + ->add('follow_requests.json', DB::table('follow_requests')->where('user_id', $this->id)->get()) + ->add('follows_requests.json', DB::table('follow_requests')->where('follow_id', $this->id)->get()) + ->add('sessions.json', $this->sessions()->get()->toJson()) + ->add('home.json', $this->home()->get()->toJson()) + ->add('hafas_trips.json', DB::table('hafas_trips')->where('user_id', $this->id)->get()) + ->add('mentions.json', Mention::where('user_id', $this->id)->get()->toJson()) + ->add('roles.json', $this->roles()->get()->toJson()) + ->add( + 'activity_log.json', + DB::table('activity_log')->where('causer_type', get_class($this))->where('causer_id', $this->id)->get() + ) + ->add('permissions.json', $this->permissions()->get()->toJson()); + } + + public function personalDataExportName(): string { + return $this->username; + } } diff --git a/app/Models/WebhookCreationRequest.php b/app/Models/WebhookCreationRequest.php index 4a88660a6..9624eec54 100644 --- a/app/Models/WebhookCreationRequest.php +++ b/app/Models/WebhookCreationRequest.php @@ -14,6 +14,7 @@ class WebhookCreationRequest extends Model { public $timestamps = false; protected $fillable = ['id', 'user_id', 'oauth_client_id', 'revoked', 'expires_at', 'events', 'url']; + protected $hidden = ['url']; protected $casts = [ 'id' => 'string', 'user_id' => 'integer', diff --git a/composer.json b/composer.json index 6459b068f..3fe9ec711 100644 --- a/composer.json +++ b/composer.json @@ -37,6 +37,7 @@ "spatie/icalendar-generator": "^2.0", "spatie/laravel-activitylog": "^4.7", "spatie/laravel-permission": "^6.1", + "spatie/laravel-personal-data-export": "^4.2", "spatie/laravel-prometheus": "^1.0", "spatie/laravel-sitemap": "^7.0", "spatie/laravel-validation-rules": "^3.2", diff --git a/composer.lock b/composer.lock index afa473319..c1f5ee4e3 100644 --- a/composer.lock +++ b/composer.lock @@ -6240,6 +6240,83 @@ ], "time": "2024-02-28T08:11:20+00:00" }, + { + "name": "spatie/laravel-personal-data-export", + "version": "4.2.2", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-personal-data-export.git", + "reference": "0004f98f4581811fa41d352b4dc594385a00c370" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-personal-data-export/zipball/0004f98f4581811fa41d352b4dc594385a00c370", + "reference": "0004f98f4581811fa41d352b4dc594385a00c370", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-zip": "*", + "illuminate/filesystem": "^9.0|^10.0", + "illuminate/queue": "^9.0|^10.0", + "illuminate/support": "^9.0|^10.0", + "nesbot/carbon": "^2.63", + "php": "^8.0", + "spatie/laravel-package-tools": "^1.9", + "spatie/temporary-directory": "^2.0" + }, + "require-dev": { + "laravel/legacy-factories": "^1.0.4", + "mockery/mockery": "^1.4", + "orchestra/testbench": "^7.0|^8.0", + "spatie/invade": "^1.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\PersonalDataExport\\PersonalDataExportServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Spatie\\PersonalDataExport\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Create personal data downloads in a Laravel app", + "homepage": "https://github.com/spatie/laravel-personal-data-export", + "keywords": [ + "gdpr", + "personal", + "personal-data-export", + "spatie", + "zip" + ], + "support": { + "issues": "https://github.com/spatie/laravel-personal-data-export/issues", + "source": "https://github.com/spatie/laravel-personal-data-export/tree/4.2.2" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + } + ], + "time": "2023-09-13T09:48:10+00:00" + }, { "name": "spatie/laravel-prometheus", "version": "1.1.0", diff --git a/config/filesystems.php b/config/filesystems.php index a8eb2319e..353871f91 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -43,6 +43,11 @@ 'disks' => [ + 'personal-data-exports' => [ + 'driver' => 'local', + 'root' => storage_path('app/personal-data-exports'), + ], + 'local' => [ 'driver' => 'local', 'root' => storage_path('app'), diff --git a/config/personal-data-export.php b/config/personal-data-export.php new file mode 100644 index 000000000..c5a25196b --- /dev/null +++ b/config/personal-data-export.php @@ -0,0 +1,39 @@ + 'personal-data-exports', + + /* + * If you want to keep the original directory structure for added files, + */ + 'keep_directory_structure' => true, + + /* + * The amount of days the exports will be available. + */ + 'delete_after_days' => 5, + + /* + * Determines whether the user should be logged in to be able + * to access the export. + */ + 'authentication_required' => true, + + /* + * The notification which will be sent to the user when the export + * has been created. + */ + 'notification' => \Spatie\PersonalDataExport\Notifications\PersonalDataExportedNotification::class, + + /* + * Configure the queue and connection used by `CreatePersonalDataExportJob` + * which will create the export. + */ + 'job' => [ + 'queue' => 'export', + 'connection' => null, + ], +]; From d23f3ce5f3242462d974d4b1d46ed62bef2c3cd4 Mon Sep 17 00:00:00 2001 From: Levin Date: Sat, 23 Mar 2024 16:31:34 +0100 Subject: [PATCH 02/19] Move statuses to end + fix mentions --- app/Models/User.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Models/User.php b/app/Models/User.php index 6de6af745..a6736020e 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -319,7 +319,6 @@ public function selectPersonalData(PersonalDataSelection $personalDataSelection) $personalDataSelection ->add('user.json', $user) - ->add('statuses.json', $this->statuses()->with('tags')->get()) ->add('notifications.json', $this->notifications()->get()->toJson()) ->add('likes.json', $this->likes()->get()->toJson()) ->add('social_profile.json', $this->socialProfile()->with('mastodonserver')->get()) @@ -348,13 +347,14 @@ public function selectPersonalData(PersonalDataSelection $personalDataSelection) ->add('sessions.json', $this->sessions()->get()->toJson()) ->add('home.json', $this->home()->get()->toJson()) ->add('hafas_trips.json', DB::table('hafas_trips')->where('user_id', $this->id)->get()) - ->add('mentions.json', Mention::where('user_id', $this->id)->get()->toJson()) + ->add('mentions.json', Mention::where('mentioned_id', $this->id)->get()->toJson()) ->add('roles.json', $this->roles()->get()->toJson()) ->add( 'activity_log.json', DB::table('activity_log')->where('causer_type', get_class($this))->where('causer_id', $this->id)->get() ) - ->add('permissions.json', $this->permissions()->get()->toJson()); + ->add('permissions.json', $this->permissions()->get()->toJson()) + ->add('statuses.json', $this->statuses()->with('tags')->get()); } public function personalDataExportName(): string { From bf8cbe412d4cd1fc8ab85b29b1c8e60912f5a62f Mon Sep 17 00:00:00 2001 From: Levin Date: Sun, 31 Mar 2024 18:46:45 +0200 Subject: [PATCH 03/19] Autoformatter --- config/personal-data-export.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/config/personal-data-export.php b/config/personal-data-export.php index c5a25196b..62d9131c9 100644 --- a/config/personal-data-export.php +++ b/config/personal-data-export.php @@ -4,7 +4,7 @@ /* * The disk where the exports will be stored by default. */ - 'disk' => 'personal-data-exports', + 'disk' => 'personal-data-exports', /* * If you want to keep the original directory structure for added files, @@ -14,26 +14,26 @@ /* * The amount of days the exports will be available. */ - 'delete_after_days' => 5, + 'delete_after_days' => 5, /* * Determines whether the user should be logged in to be able * to access the export. */ - 'authentication_required' => true, + 'authentication_required' => true, /* * The notification which will be sent to the user when the export * has been created. */ - 'notification' => \Spatie\PersonalDataExport\Notifications\PersonalDataExportedNotification::class, + 'notification' => \Spatie\PersonalDataExport\Notifications\PersonalDataExportedNotification::class, /* * Configure the queue and connection used by `CreatePersonalDataExportJob` * which will create the export. */ - 'job' => [ - 'queue' => 'export', + 'job' => [ + 'queue' => 'export', 'connection' => null, ], ]; From 9104b439cc09e5d7c6c792e104629e9da3e01514 Mon Sep 17 00:00:00 2001 From: Levin Date: Sun, 31 Mar 2024 19:38:01 +0200 Subject: [PATCH 04/19] Add Database Notification --- app/Jobs/MonitoredPersonalDataExportJob.php | 5 +++ app/Notifications/BaseNotification.php | 2 + .../PersonalDataExportedNotification.php | 37 +++++++++++++++++++ config/personal-data-export.php | 4 +- lang/de.json | 2 + lang/en.json | 2 + .../vue/components/NotificationEntry.vue | 2 + 7 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 app/Notifications/PersonalDataExportedNotification.php diff --git a/app/Jobs/MonitoredPersonalDataExportJob.php b/app/Jobs/MonitoredPersonalDataExportJob.php index f1605330b..d9a6a7397 100644 --- a/app/Jobs/MonitoredPersonalDataExportJob.php +++ b/app/Jobs/MonitoredPersonalDataExportJob.php @@ -3,6 +3,7 @@ namespace App\Jobs; use romanzipp\QueueMonitor\Traits\IsMonitored; +use Spatie\PersonalDataExport\ExportsPersonalData; use Spatie\PersonalDataExport\Jobs\CreatePersonalDataExportJob; class MonitoredPersonalDataExportJob extends CreatePersonalDataExportJob @@ -12,4 +13,8 @@ class MonitoredPersonalDataExportJob extends CreatePersonalDataExportJob public $timeout = 30 * 60; + + protected function ensureValidUser(ExportsPersonalData $user) { + // Do nothing since we are not enforcing the user to have an email property + } } diff --git a/app/Notifications/BaseNotification.php b/app/Notifications/BaseNotification.php index b7ca17966..484ec5579 100644 --- a/app/Notifications/BaseNotification.php +++ b/app/Notifications/BaseNotification.php @@ -13,4 +13,6 @@ public static function getNotice(array $data): ?string; * @return string|null optionally link to which the user should be redirected if clicked on the notification */ public static function getLink(array $data): ?string; + + public function toArray(): array; } diff --git a/app/Notifications/PersonalDataExportedNotification.php b/app/Notifications/PersonalDataExportedNotification.php new file mode 100644 index 000000000..d030ab293 --- /dev/null +++ b/app/Notifications/PersonalDataExportedNotification.php @@ -0,0 +1,37 @@ + userTime($date, __('datetime-format')), + ]); + } + + public static function getLink(array $data): ?string { + return route('personal-data-exports', $data['zipFilename']); + } + + public function toArray(): array + { + return [ + 'zipFilename' => $this->zipFilename, + 'deletionDatetime' => $this->deletionDatetime, + ]; + } +} diff --git a/config/personal-data-export.php b/config/personal-data-export.php index 62d9131c9..fd87947af 100644 --- a/config/personal-data-export.php +++ b/config/personal-data-export.php @@ -1,5 +1,7 @@ \Spatie\PersonalDataExport\Notifications\PersonalDataExportedNotification::class, + 'notification' => PersonalDataExportedNotification::class, /* * Configure the queue and connection used by `CreatePersonalDataExportJob` diff --git a/lang/de.json b/lang/de.json index 0ad77d2ca..0506305a2 100644 --- a/lang/de.json +++ b/lang/de.json @@ -276,6 +276,8 @@ "notifications.socialNotShared.mastodon.504": "Scheinbar war deine Instanz nicht verfügbar, als wir versucht haben, Deinen Check-In zu tooten. (504 Bad Gateway)", "notifications.userJoinedConnection.lead": "@:username ist auch in Deiner Verbindung!", "notifications.userJoinedConnection.notice": "@:username reist mit :linename von :origin nach :destination.|@:username reist mit Linie :linename von :origin nach :destination.", + "notifications.personalDataExported.lead": "Dein Export ist bereit!", + "notifications.personalDataExported.notice": "Die Daten stehen für dich bis :date zum Download bereit.", "pagination.next": "Nächste Seite »", "pagination.previous": "« Vorherige Seite", "pagination.back": "« Zurück", diff --git a/lang/en.json b/lang/en.json index 6bc459f96..d059d50ae 100644 --- a/lang/en.json +++ b/lang/en.json @@ -258,6 +258,8 @@ "notifications.socialNotShared.mastodon.504": "It looks like your Mastodon instance was not available when we tried to toot. (504 Bad Gateway)", "notifications.userJoinedConnection.lead": "@:username is in your connection!", "notifications.userJoinedConnection.notice": "@:username is on :linename from :origin to :destination.|@:username is on line :linename from :origin to :destination.", + "notifications.personalDataExported.lead": "Your export is ready!", + "notifications.personalDataExported.notice": "Your data will be available for download until :date.", "pagination.next": "Next »", "pagination.previous": "« Previous", "pagination.back": "« Back", diff --git a/resources/vue/components/NotificationEntry.vue b/resources/vue/components/NotificationEntry.vue index 4771258a2..c865d7649 100644 --- a/resources/vue/components/NotificationEntry.vue +++ b/resources/vue/components/NotificationEntry.vue @@ -42,6 +42,8 @@ export default { return 'fas fa-user-friends'; case 'UserJoinedConnection': return 'fa fa-train'; + case 'PersonalDataExportedNotification': + return 'fas fa-download'; default: return 'far fa-envelope'; } From 0c426826d6e8f696e00716cc1fcc156a32278d34 Mon Sep 17 00:00:00 2001 From: Levin Date: Sun, 31 Mar 2024 20:24:23 +0200 Subject: [PATCH 05/19] Remove useless comment --- app/Enum/ExportableColumn.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/Enum/ExportableColumn.php b/app/Enum/ExportableColumn.php index c25e47d02..753f1cd48 100644 --- a/app/Enum/ExportableColumn.php +++ b/app/Enum/ExportableColumn.php @@ -33,6 +33,4 @@ public function title(): string { } return $title; } - -// public } From 6ceb6776f7418f90c2e9607ab2f503e5627a7fb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20St=C3=B6ckel?= Date: Sat, 27 Apr 2024 14:33:57 +0200 Subject: [PATCH 06/19] fix composer errors --- .idea/trwl.iml | 2 +- composer.lock | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.idea/trwl.iml b/.idea/trwl.iml index b98c19383..53d2f109a 100644 --- a/.idea/trwl.iml +++ b/.idea/trwl.iml @@ -189,4 +189,4 @@ - + \ No newline at end of file diff --git a/composer.lock b/composer.lock index e755757be..0d2097bbf 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "168d45ed0964926f2c3a4d10281f9bfc", + "content-hash": "3a90e2dc82c8c8742996a8ee82e8fe28", "packages": [ { "name": "barryvdh/laravel-dompdf", @@ -6293,25 +6293,25 @@ }, { "name": "spatie/laravel-personal-data-export", - "version": "4.2.2", + "version": "4.3.0", "source": { "type": "git", "url": "https://github.com/spatie/laravel-personal-data-export.git", - "reference": "0004f98f4581811fa41d352b4dc594385a00c370" + "reference": "31bcfe10b9ea2d9c1b245f2279ba59c6f882dfdd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-personal-data-export/zipball/0004f98f4581811fa41d352b4dc594385a00c370", - "reference": "0004f98f4581811fa41d352b4dc594385a00c370", + "url": "https://api.github.com/repos/spatie/laravel-personal-data-export/zipball/31bcfe10b9ea2d9c1b245f2279ba59c6f882dfdd", + "reference": "31bcfe10b9ea2d9c1b245f2279ba59c6f882dfdd", "shasum": "" }, "require": { "ext-json": "*", "ext-zip": "*", - "illuminate/filesystem": "^9.0|^10.0", - "illuminate/queue": "^9.0|^10.0", - "illuminate/support": "^9.0|^10.0", - "nesbot/carbon": "^2.63", + "illuminate/filesystem": "^9.0|^10.0|^11.0", + "illuminate/queue": "^9.0|^10.0|^11.0", + "illuminate/support": "^9.0|^10.0|^11.0", + "nesbot/carbon": "^2.63|^3.0", "php": "^8.0", "spatie/laravel-package-tools": "^1.9", "spatie/temporary-directory": "^2.0" @@ -6319,8 +6319,8 @@ "require-dev": { "laravel/legacy-factories": "^1.0.4", "mockery/mockery": "^1.4", - "orchestra/testbench": "^7.0|^8.0", - "spatie/invade": "^1.0" + "orchestra/testbench": "^7.0|^8.0|^9.0", + "spatie/invade": "^1.0|^2.0" }, "type": "library", "extra": { @@ -6358,7 +6358,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-personal-data-export/issues", - "source": "https://github.com/spatie/laravel-personal-data-export/tree/4.2.2" + "source": "https://github.com/spatie/laravel-personal-data-export/tree/4.3.0" }, "funding": [ { @@ -6366,7 +6366,7 @@ "type": "custom" } ], - "time": "2023-09-13T09:48:10+00:00" + "time": "2024-02-28T10:07:01+00:00" }, { "name": "spatie/laravel-prometheus", From c2cb84a4a85a410dedd92d3e991d6b78378be146 Mon Sep 17 00:00:00 2001 From: Levin Date: Mon, 4 Nov 2024 21:19:13 +0100 Subject: [PATCH 07/19] composer fix --- composer.json | 2 +- composer.lock | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index e38fce13d..580242d20 100644 --- a/composer.json +++ b/composer.json @@ -39,7 +39,7 @@ "spatie/icalendar-generator": "^2.0", "spatie/laravel-activitylog": "^4.7", "spatie/laravel-permission": "^6.1", - "spatie/laravel-personal-data-export": "^4.2", + "spatie/laravel-personal-data-export": "^4.3", "spatie/laravel-prometheus": "^1.0", "spatie/laravel-sitemap": "^7.0", "spatie/laravel-validation-rules": "^3.2", diff --git a/composer.lock b/composer.lock index f26cc0258..bbf0fa58c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4f5d8537f86da4edf5b487297c1f33a4", + "content-hash": "07a309ea519918b05659731b084ca4ad", "packages": [ { "name": "barryvdh/laravel-dompdf", @@ -6432,6 +6432,83 @@ ], "time": "2024-06-22T23:04:52+00:00" }, + { + "name": "spatie/laravel-personal-data-export", + "version": "4.3.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-personal-data-export.git", + "reference": "31bcfe10b9ea2d9c1b245f2279ba59c6f882dfdd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-personal-data-export/zipball/31bcfe10b9ea2d9c1b245f2279ba59c6f882dfdd", + "reference": "31bcfe10b9ea2d9c1b245f2279ba59c6f882dfdd", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-zip": "*", + "illuminate/filesystem": "^9.0|^10.0|^11.0", + "illuminate/queue": "^9.0|^10.0|^11.0", + "illuminate/support": "^9.0|^10.0|^11.0", + "nesbot/carbon": "^2.63|^3.0", + "php": "^8.0", + "spatie/laravel-package-tools": "^1.9", + "spatie/temporary-directory": "^2.0" + }, + "require-dev": { + "laravel/legacy-factories": "^1.0.4", + "mockery/mockery": "^1.4", + "orchestra/testbench": "^7.0|^8.0|^9.0", + "spatie/invade": "^1.0|^2.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\PersonalDataExport\\PersonalDataExportServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Spatie\\PersonalDataExport\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Create personal data downloads in a Laravel app", + "homepage": "https://github.com/spatie/laravel-personal-data-export", + "keywords": [ + "gdpr", + "personal", + "personal-data-export", + "spatie", + "zip" + ], + "support": { + "issues": "https://github.com/spatie/laravel-personal-data-export/issues", + "source": "https://github.com/spatie/laravel-personal-data-export/tree/4.3.0" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + } + ], + "time": "2024-02-28T10:07:01+00:00" + }, { "name": "spatie/laravel-prometheus", "version": "1.2.0", From 2c7913fe68fcd84649f115aafb8b0a16f1563f30 Mon Sep 17 00:00:00 2001 From: Levin Date: Mon, 4 Nov 2024 22:28:29 +0100 Subject: [PATCH 08/19] Move UserPersonalData to custom class --- app/Models/User.php | 69 ++------------- .../UserGdprDataService.php | 83 +++++++++++++++++++ 2 files changed, 88 insertions(+), 64 deletions(-) create mode 100644 app/Services/PersonalDataSelection/UserGdprDataService.php diff --git a/app/Models/User.php b/app/Models/User.php index 77e6fb017..0fcd2eb4f 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -7,8 +7,8 @@ use App\Enum\User\FriendCheckinSetting; use App\Exceptions\RateLimitExceededException; use App\Http\Controllers\Backend\Social\MastodonProfileDetails; -use App\Http\Controllers\Backend\User\TokenController; use App\Jobs\SendVerificationEmail; +use App\Services\PersonalDataSelection\UserGdprDataService; use Carbon\Carbon; use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Builder; @@ -20,7 +20,6 @@ use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\DatabaseNotification; use Illuminate\Notifications\Notifiable; -use Illuminate\Support\Facades\DB; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\RateLimiter; @@ -82,6 +81,8 @@ * @property Collection statuses * @property Collection trustedUsers * @property Collection trustedByUsers + * @property Carbon created_at + * @property Carbon updated_at * * * @todo rename home_id to home_station_id @@ -334,72 +335,12 @@ protected function getDefaultGuardName(): string { return 'web'; } - protected function oAuthClients(): HasMany { + public function oAuthClients(): HasMany { return $this->hasMany(OAuthClient::class, 'user_id', 'id'); } public function selectPersonalData(PersonalDataSelection $personalDataSelection): void { - $user = $this->toArray(); - $user['email'] = $this->email; - $user['email_verified_at'] = $this->email_verified_at; - $user['privacy_ack_at'] = $this->privacy_ack_at; - $user['last_login'] = $this->last_login; - $user['created_at'] = $this->created_at; - $user['updated_at'] = $this->updated_at; - - $webhooks = $this->webhooks()->with('events')->get(); - $webhooks = $webhooks->map(function($webhook) { - $webhook['created_at'] = $webhook->created_at; - $webhook['updated_at'] = $webhook->updated_at; - $webhook['client_id'] = (int) $webhook->oauth_client_id ?? null; - unset($webhook['url']); - return $webhook; - }); - - - if ($this->avatar && file_exists(public_path('/uploads/avatars/' . $this->avatar))) { - $personalDataSelection - ->addFile(public_path('/uploads/avatars/' . $this->avatar)); - } - - $personalDataSelection - ->add('user.json', $user) - ->add('notifications.json', $this->notifications()->get()->toJson()) - ->add('likes.json', $this->likes()->get()->toJson()) - ->add('social_profile.json', $this->socialProfile()->with('mastodonserver')->get()) - ->add('event_suggestions.json', EventSuggestion::where('user_id', $this->id)->get()->toJson()) - ->add('events.json', Event::where('approved_by', $this->id)->get()->toJson()) - ->add('webhooks.json', $webhooks) - ->add( - 'webhook_creation_requests.json', - WebhookCreationRequest::where('user_id', $this->id)->get()->toJson() - ) - ->add('tokens.json', TokenController::index($this)->toJson()) - ->add('ics_tokens.json', $this->icsTokens()->get()->toJson()) - ->add( - 'password_resets.json', - DB::table('password_resets')->select(['email','created_at'])->where('email', $this->email)->get() - ) - ->add('apps.json', $this->oAuthClients()->get()->toJson()) - ->add('follows.json', DB::table('follows')->where('user_id', $this->id)->get()) - ->add('followings.json', DB::table('follows')->where('follow_id', $this->id)->get()) - ->add('blocks.json', DB::table('user_blocks')->where('user_id', $this->id)->get()) - ->add('blocked_by.json', DB::table('user_blocks')->where('blocked_id', $this->id)->get()) - ->add('mutes.json', DB::table('user_mutes')->where('user_id', $this->id)->get()) - ->add('muted_by.json', DB::table('user_mutes')->where('muted_id', $this->id)->get()) - ->add('follow_requests.json', DB::table('follow_requests')->where('user_id', $this->id)->get()) - ->add('follows_requests.json', DB::table('follow_requests')->where('follow_id', $this->id)->get()) - ->add('sessions.json', $this->sessions()->get()->toJson()) - ->add('home.json', $this->home()->get()->toJson()) - ->add('hafas_trips.json', DB::table('hafas_trips')->where('user_id', $this->id)->get()) - ->add('mentions.json', Mention::where('mentioned_id', $this->id)->get()->toJson()) - ->add('roles.json', $this->roles()->get()->toJson()) - ->add( - 'activity_log.json', - DB::table('activity_log')->where('causer_type', get_class($this))->where('causer_id', $this->id)->get() - ) - ->add('permissions.json', $this->permissions()->get()->toJson()) - ->add('statuses.json', $this->statuses()->with('tags')->get()); + (new UserGdprDataService())($personalDataSelection, $this); } public function personalDataExportName(): string { diff --git a/app/Services/PersonalDataSelection/UserGdprDataService.php b/app/Services/PersonalDataSelection/UserGdprDataService.php new file mode 100644 index 000000000..9496ae006 --- /dev/null +++ b/app/Services/PersonalDataSelection/UserGdprDataService.php @@ -0,0 +1,83 @@ +addUserPersonalData($personalDataSelection, $data); + } + + private function addUserPersonalData(PersonalDataSelection $personalDataSelection, User $data): void { + $user = $data->toArray(); + $user['email'] = $data->email; + $user['email_verified_at'] = $data->email_verified_at; + $user['privacy_ack_at'] = $data->privacy_ack_at; + $user['last_login'] = $data->last_login; + $user['created_at'] = $data->created_at; + $user['updated_at'] = $data->updated_at; + + $webhooks = $data->webhooks()->with('events')->get(); + $webhooks = $webhooks->map(function($webhook) { + $webhook['created_at'] = $webhook->created_at; + $webhook['updated_at'] = $webhook->updated_at; + $webhook['client_id'] = (int) $webhook->oauth_client_id ?? null; + unset($webhook['url']); + return $webhook; + }); + + + if ($data->avatar && file_exists(public_path('/uploads/avatars/' . $data->avatar))) { + $personalDataSelection + ->addFile(public_path('/uploads/avatars/' . $data->avatar)); + } + + $personalDataSelection + ->add('user.json', $user) + ->add('notifications.json', $data->notifications()->get()->toJson()) + ->add('likes.json', $data->likes()->get()->toJson()) + ->add('social_profile.json', $data->socialProfile()->with('mastodonserver')->get()) + ->add('event_suggestions.json', EventSuggestion::where('user_id', $data->id)->get()->toJson()) + ->add('events.json', Event::where('approved_by', $data->id)->get()->toJson()) + ->add('webhooks.json', $webhooks) + ->add( + 'webhook_creation_requests.json', + WebhookCreationRequest::where('user_id', $data->id)->get()->toJson() + ) + ->add('tokens.json', TokenController::index($data)->toJson()) + ->add('ics_tokens.json', $data->icsTokens()->get()->toJson()) + ->add( + 'password_resets.json', + DB::table('password_resets')->select(['email', 'created_at'])->where('email', $data->email)->get() + ) + ->add('apps.json', $data->oAuthClients()->get()->toJson()) + ->add('follows.json', DB::table('follows')->where('user_id', $data->id)->get()) + ->add('followings.json', DB::table('follows')->where('follow_id', $data->id)->get()) + ->add('blocks.json', DB::table('user_blocks')->where('user_id', $data->id)->get()) + ->add('blocked_by.json', DB::table('user_blocks')->where('blocked_id', $data->id)->get()) + ->add('mutes.json', DB::table('user_mutes')->where('user_id', $data->id)->get()) + ->add('muted_by.json', DB::table('user_mutes')->where('muted_id', $data->id)->get()) + ->add('follow_requests.json', DB::table('follow_requests')->where('user_id', $data->id)->get()) + ->add('follows_requests.json', DB::table('follow_requests')->where('follow_id', $data->id)->get()) + ->add('sessions.json', $data->sessions()->get()->toJson()) + ->add('home.json', $data->home()->get()->toJson()) + ->add('hafas_trips.json', DB::table('hafas_trips')->where('user_id', $data->id)->get()) + ->add('mentions.json', Mention::where('mentioned_id', $data->id)->get()->toJson()) + ->add('roles.json', $data->roles()->get()->toJson()) + ->add( + 'activity_log.json', + DB::table('activity_log')->where('causer_type', get_class($data))->where('causer_id', $data->id)->get() + ) + ->add('permissions.json', $data->permissions()->get()->toJson()) + ->add('statuses.json', $data->statuses()->with('tags')->get()); + } +} From 321a09dba3f9d0ca2efe4d9ea9e8d77359146915 Mon Sep 17 00:00:00 2001 From: Levin Date: Mon, 4 Nov 2024 22:36:00 +0100 Subject: [PATCH 09/19] Add reports to GdprDataService --- .../PersonalDataSelection/UserGdprDataService.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/Services/PersonalDataSelection/UserGdprDataService.php b/app/Services/PersonalDataSelection/UserGdprDataService.php index 9496ae006..d850a5fbc 100644 --- a/app/Services/PersonalDataSelection/UserGdprDataService.php +++ b/app/Services/PersonalDataSelection/UserGdprDataService.php @@ -78,6 +78,13 @@ private function addUserPersonalData(PersonalDataSelection $personalDataSelectio DB::table('activity_log')->where('causer_type', get_class($data))->where('causer_id', $data->id)->get() ) ->add('permissions.json', $data->permissions()->get()->toJson()) - ->add('statuses.json', $data->statuses()->with('tags')->get()); + ->add('statuses.json', $data->statuses()->with('tags')->get()) + ->add( + 'reports.json', + DB::table('reports') + ->select('subject_type', 'subject_id', 'reason', 'description', 'reporter_id') + ->where('reporter_id', $data->id) + ->get() + ); } } From 44673e92d6707fd08a8ba7fa2df62f9b75d7d60c Mon Sep 17 00:00:00 2001 From: Levin Date: Mon, 4 Nov 2024 22:42:39 +0100 Subject: [PATCH 10/19] remove blocked_by --- app/Services/PersonalDataSelection/UserGdprDataService.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Services/PersonalDataSelection/UserGdprDataService.php b/app/Services/PersonalDataSelection/UserGdprDataService.php index d850a5fbc..a811a3dcf 100644 --- a/app/Services/PersonalDataSelection/UserGdprDataService.php +++ b/app/Services/PersonalDataSelection/UserGdprDataService.php @@ -63,7 +63,6 @@ private function addUserPersonalData(PersonalDataSelection $personalDataSelectio ->add('follows.json', DB::table('follows')->where('user_id', $data->id)->get()) ->add('followings.json', DB::table('follows')->where('follow_id', $data->id)->get()) ->add('blocks.json', DB::table('user_blocks')->where('user_id', $data->id)->get()) - ->add('blocked_by.json', DB::table('user_blocks')->where('blocked_id', $data->id)->get()) ->add('mutes.json', DB::table('user_mutes')->where('user_id', $data->id)->get()) ->add('muted_by.json', DB::table('user_mutes')->where('muted_id', $data->id)->get()) ->add('follow_requests.json', DB::table('follow_requests')->where('user_id', $data->id)->get()) From 3256bf0edab61fcc6001c8a017ed4d75d96bdd56 Mon Sep 17 00:00:00 2001 From: Levin Date: Mon, 4 Nov 2024 23:14:30 +0100 Subject: [PATCH 11/19] Add trusted_users, remove muted_by --- app/Services/PersonalDataSelection/UserGdprDataService.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/Services/PersonalDataSelection/UserGdprDataService.php b/app/Services/PersonalDataSelection/UserGdprDataService.php index a811a3dcf..fda101651 100644 --- a/app/Services/PersonalDataSelection/UserGdprDataService.php +++ b/app/Services/PersonalDataSelection/UserGdprDataService.php @@ -64,7 +64,6 @@ private function addUserPersonalData(PersonalDataSelection $personalDataSelectio ->add('followings.json', DB::table('follows')->where('follow_id', $data->id)->get()) ->add('blocks.json', DB::table('user_blocks')->where('user_id', $data->id)->get()) ->add('mutes.json', DB::table('user_mutes')->where('user_id', $data->id)->get()) - ->add('muted_by.json', DB::table('user_mutes')->where('muted_id', $data->id)->get()) ->add('follow_requests.json', DB::table('follow_requests')->where('user_id', $data->id)->get()) ->add('follows_requests.json', DB::table('follow_requests')->where('follow_id', $data->id)->get()) ->add('sessions.json', $data->sessions()->get()->toJson()) @@ -84,6 +83,6 @@ private function addUserPersonalData(PersonalDataSelection $personalDataSelectio ->select('subject_type', 'subject_id', 'reason', 'description', 'reporter_id') ->where('reporter_id', $data->id) ->get() - ); + )->add('trusted_users.json', DB::table('trusted_users')->where('user_id', $data->id)->get()); } } From 0648d2551e539a4fa516950a8895a18cd9c3b6ef Mon Sep 17 00:00:00 2001 From: Levin Date: Tue, 5 Nov 2024 00:39:54 +0100 Subject: [PATCH 12/19] Add frontend to gdpr request --- .../Controllers/API/v1/ExportController.php | 36 +++++++++++++++++-- app/Http/Controllers/GdprExportController.php | 21 ----------- app/Models/User.php | 8 +++-- ...223623_add_recent_gdpr_export_to_users.php | 20 +++++++++++ lang/de.json | 5 +++ lang/en.json | 5 +++ resources/views/export.blade.php | 36 +++++++++++++++++++ routes/api.php | 9 ++--- routes/web.php | 1 + 9 files changed, 111 insertions(+), 30 deletions(-) delete mode 100644 app/Http/Controllers/GdprExportController.php create mode 100644 database/migrations/2024_11_04_223623_add_recent_gdpr_export_to_users.php diff --git a/app/Http/Controllers/API/v1/ExportController.php b/app/Http/Controllers/API/v1/ExportController.php index 4c08b2f2b..ddc83fa01 100644 --- a/app/Http/Controllers/API/v1/ExportController.php +++ b/app/Http/Controllers/API/v1/ExportController.php @@ -5,6 +5,7 @@ use App\Enum\ExportableColumn; use App\Exceptions\DataOverflowException; use App\Http\Controllers\Backend\Export\ExportController as ExportBackend; +use App\Jobs\MonitoredPersonalDataExportJob; use Carbon\Carbon; use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; @@ -15,18 +16,37 @@ class ExportController extends Controller { + public function requestGdprExport(Request $request): JsonResponse|Response|RedirectResponse { + $validated = $request->validate([ + 'frontend' => ['nullable', 'boolean'], + ]); + + $user = $request->user(); + + if ($user->recent_gdpr_export && $user->recent_gdpr_export->diffInDays(now()) < 30) { + return $this->frontendOrJson($validated, ['error' => __('export.error.gdpr-time', ['date' => userTime($user->recent_gdpr_export)])]); + } + + $user->update(['recent_gdpr_export' => now()]); + + dispatch(new MonitoredPersonalDataExportJob($user)); + + return $this->frontendOrJson($validated, ['message' => __('export.requested')], 200); + } + public function generateStatusExport(Request $request): JsonResponse|StreamedResponse|Response|RedirectResponse { $validated = $request->validate([ 'from' => ['required', 'date', 'before_or_equal:until'], 'until' => ['required', 'date', 'after_or_equal:from'], 'columns.*' => ['required', Rule::enum(ExportableColumn::class)], 'filetype' => ['required', Rule::in(['pdf', 'csv_human', 'csv_machine', 'json'])], + 'frontend' => ['nullable', 'boolean'], ]); $from = Carbon::parse($validated['from']); $until = Carbon::parse($validated['until']); if ($from->diffInDays($until) > 365) { - return back()->with('error', __('export.error.time')); + return $this->frontendOrJson($validated, ['error' => __('export.error.time')]); } if ($validated['filetype'] === 'json') { @@ -49,7 +69,19 @@ public function generateStatusExport(Request $request): JsonResponse|StreamedRes filetype: $validated['filetype'] ); } catch (DataOverflowException) { - return back()->with('error', __('export.error.amount')); + return $this->frontendOrJson($validated, ['error' => __('export.error.amount')], 406); + } + } + + private function frontendOrJson(array $validated, array $data, int $status = 400): RedirectResponse|JsonResponse { + if (empty($validated['frontend'])) { + return response()->json($data, $status); + } + + if (array_key_exists('error', $data)) { + return redirect('export')->with($data); } + + return redirect('export')->with('success', $data['message']); } } diff --git a/app/Http/Controllers/GdprExportController.php b/app/Http/Controllers/GdprExportController.php deleted file mode 100644 index af3badbe7..000000000 --- a/app/Http/Controllers/GdprExportController.php +++ /dev/null @@ -1,21 +0,0 @@ - MapProvider::class, 'timezone' => 'string', 'friend_checkin' => FriendCheckinSetting::class, + 'recent_gdpr_export' => 'datetime', ]; public function getTrainDistanceAttribute(): float { diff --git a/database/migrations/2024_11_04_223623_add_recent_gdpr_export_to_users.php b/database/migrations/2024_11_04_223623_add_recent_gdpr_export_to_users.php new file mode 100644 index 000000000..f5d71c7d1 --- /dev/null +++ b/database/migrations/2024_11_04_223623_add_recent_gdpr_export_to_users.php @@ -0,0 +1,20 @@ +timestamp('recent_gdpr_export')->nullable()->after('last_login'); + }); + } + + public function down(): void { + Schema::table('users', function(Blueprint $table) { + $table->dropColumn('recent_gdpr_export'); + }); + } +}; diff --git a/lang/de.json b/lang/de.json index a0d161b46..4b8329976 100644 --- a/lang/de.json +++ b/lang/de.json @@ -158,8 +158,13 @@ "export.end": "Bis", "export.lead": "Hier kannst du deine Fahrten aus der Datenbank als CSV, JSON und als PDF exportieren.", "export.error.time": "Du kannst nur Fahrten über einen Zeitraum von maximal 365 Tagen exportieren.", + "export.error.gdpr-time": "Du kannst nur einmal alle 30 Tage deine gesamten Daten exportieren. Dein letzter Export war am :date.", "export.error.amount": "Du hast mehr als 2000 Fahrten angefragt. Bitte versuche den Zeitraum einzuschränken.", + "export.gdpr": "DSGVO-Export", + "export.gdpr.description": "Hier kannst du deine gesamten Daten exportieren. Dieser Vorgang kann bis zu 48 Stunden dauern.", + "export.gdpr.recent": "Dein letzter Export war am :date. Du kannst deine Daten alle 30 Tage exportieren.", "export.submit": "Exportieren als", + "export.request": "Export anfordern", "export.title": "Exportieren", "export.type": "Zugart", "export.number": "Zugnummer", diff --git a/lang/en.json b/lang/en.json index 3824ac5d9..815694b7a 100644 --- a/lang/en.json +++ b/lang/en.json @@ -137,8 +137,13 @@ "export.end": "End", "export.lead": "You can export your journeys into a CSV, JSON or PDF file here.", "export.submit": "Export as", + "export.request": "Request", "export.error.time": "You can only export trips over a maximum period of 365 days.", + "export.error.gdpr-time": "You can only export your full data once every 30 days. Your last export was on :date.", "export.error.amount": "You have requested more than 2000 trips. Please try to limit the period.", + "export.gdpr": "GDPR-Export", + "export.gdpr.description": "Here you can export all your data. This process can take up to 48 hours.", + "export.gdpr.recent": "Your last export was on :date. You can export your data every 30 days.", "export.title": "Export data", "export.type": "Type", "export.number": "Number", diff --git a/resources/views/export.blade.php b/resources/views/export.blade.php index 7e4a825c8..7bf659096 100644 --- a/resources/views/export.blade.php +++ b/resources/views/export.blade.php @@ -12,6 +12,7 @@
+ @csrf
@@ -135,6 +136,7 @@ class="form-control"/>
+ @csrf
@@ -165,6 +167,40 @@ class="form-control"/>
+
+
+

+   + {{ __('export.gdpr') }} +

+ + {{__('export.gdpr.description')}} +
+ @php + $recent = auth()->user()->recent_gdpr_export; + @endphp + + @if($recent) + {{ __('export.gdpr.recent', ['date' => userTime($recent, __('datetime-format'))]) }} + @endif + +
+ +
+ + @csrf +
+
+ +
+
+
+
+
diff --git a/routes/api.php b/routes/api.php index 4e2ee772f..fcfb4ac72 100644 --- a/routes/api.php +++ b/routes/api.php @@ -106,6 +106,7 @@ }); Route::group(['prefix' => 'export', 'middleware' => 'scope:write-exports'], static function() { Route::post('statuses', [ExportController::class, 'generateStatusExport']); //TODO: undocumented endpoint - document when stable + Route::post('gdpr', [ExportController::class, 'requestGdprExport']); //TODO: undocumented endpoint - document when stable }); Route::group(['prefix' => 'user'], static function() { Route::group(['middleware' => ['scope:write-follows']], static function() { @@ -113,9 +114,9 @@ Route::delete('/{userId}/follow', [FollowController::class, 'destroyFollow']); }); Route::group(['middleware' => ['scope:write-followers']], static function() { - Route::delete('removeFollower', [FollowController::class, 'removeFollower']); // TODO remove after 2024-10 + Route::delete('removeFollower', [FollowController::class, 'removeFollower']); // TODO remove after 2024-10 Route::delete('rejectFollowRequest', [FollowController::class, 'rejectFollowRequest']); // TODO remove after 2024-10 - Route::put('approveFollowRequest', [FollowController::class, 'approveFollowRequest']); // TODO remove after 2024-10 + Route::put('approveFollowRequest', [FollowController::class, 'approveFollowRequest']); // TODO remove after 2024-10 }); Route::group(['middleware' => ['scope:write-blocks']], static function() { Route::post('/{userId}/block', [UserController::class, 'createBlock']); @@ -160,9 +161,9 @@ Route::delete('token', [TokenController::class, 'revokeToken']); //TODO: undocumented endpoint - document when stable }); Route::group(['middleware' => ['scope:read-settings-followers']], static function() { - Route::get('followers', [FollowController::class, 'getFollowers']); // TODO remove after 2024-10 + Route::get('followers', [FollowController::class, 'getFollowers']); // TODO remove after 2024-10 Route::get('follow-requests', [FollowController::class, 'getFollowRequests']); // TODO remove after 2024-10 - Route::get('followings', [FollowController::class, 'getFollowings']); // TODO remove after 2024-10 + Route::get('followings', [FollowController::class, 'getFollowings']); // TODO remove after 2024-10 }); }); Route::group(['prefix' => 'webhooks'], static function() { diff --git a/routes/web.php b/routes/web.php index 6b59c0ec5..faddeb94a 100644 --- a/routes/web.php +++ b/routes/web.php @@ -89,6 +89,7 @@ * These routes can be used by logged in users although they have not signed the privacy policy yet. */ Route::middleware(['auth'])->group(function() { + Route::personalDataExports('personal-data-exports'); Route::get('/gdpr-intercept', [PrivacyAgreementController::class, 'intercept']) ->name('gdpr.intercept'); From d0182fc8dec1f42898dfcb800106e35ae2bca4c6 Mon Sep 17 00:00:00 2001 From: Levin Date: Tue, 5 Nov 2024 00:43:21 +0100 Subject: [PATCH 13/19] Limit to 7 days + cleanup --- app/Console/Kernel.php | 2 ++ config/personal-data-export.php | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 7e0ac1908..e5e33aa62 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -12,6 +12,7 @@ use App\Console\Commands\WikidataFetcher; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; +use Spatie\PersonalDataExport\Commands\CleanOldPersonalDataExportsCommand; class Kernel extends ConsoleKernel { @@ -44,6 +45,7 @@ protected function schedule(Schedule $schedule): void { //daily tasks $schedule->command(DatabaseCleaner::class)->daily(); $schedule->command(CleanUpProfilePictures::class)->daily(); + $schedule->command(CleanOldPersonalDataExportsCommand::class)->daily(); //weekly tasks $schedule->command(MastodonServers::class)->weekly(); diff --git a/config/personal-data-export.php b/config/personal-data-export.php index fd87947af..2db8925f7 100644 --- a/config/personal-data-export.php +++ b/config/personal-data-export.php @@ -16,7 +16,7 @@ /* * The amount of days the exports will be available. */ - 'delete_after_days' => 5, + 'delete_after_days' => 7, /* * Determines whether the user should be logged in to be able From a5ba02b35f01b1a24350f6527e7ae0f98203c4d8 Mon Sep 17 00:00:00 2001 From: Levin Date: Tue, 5 Nov 2024 00:53:22 +0100 Subject: [PATCH 14/19] soft-launching --- config/trwl.php | 7 ++- .../seeders/Constants/PermissionSeeder.php | 3 +- resources/views/export.blade.php | 59 ++++++++++--------- 3 files changed, 39 insertions(+), 30 deletions(-) diff --git a/config/trwl.php b/config/trwl.php index 6d8de23ba..52244a6ad 100644 --- a/config/trwl.php +++ b/config/trwl.php @@ -12,7 +12,7 @@ 'mastodon_timeout_seconds' => env("MASTODON_TIMEOUT_SECONDS", 5), # Brouter - 'brouter' => env('BROUTER', true), + 'brouter' => env('BROUTER', true), 'brouter_url' => env('BROUTER_URL', 'https://brouter.de/'), 'brouter_timeout' => env('BROUTER_TIMEOUT', 10), @@ -63,4 +63,9 @@ ], 'webhooks_active' => env('WEBHOOKS_ACTIVE', false), 'webfinger_active' => env('WEBFINGER_ACTIVE', false), + + # A/B Testing + 'ab_testing' => [ + 'gdpr_export' => env('AB_TESTING_GDPR_EXPORT', false), + ] ]; diff --git a/database/seeders/Constants/PermissionSeeder.php b/database/seeders/Constants/PermissionSeeder.php index 821820eca..239b56596 100644 --- a/database/seeders/Constants/PermissionSeeder.php +++ b/database/seeders/Constants/PermissionSeeder.php @@ -20,6 +20,7 @@ public function run(): void { $roleClosedBeta = Role::updateOrCreate(['name' => 'closed-beta']); $roleDisallowManualTrips = Role::updateOrCreate(['name' => 'disallow-manual-trips']); $roleDeactivateAccountUsage = Role::updateOrCreate(['name' => 'deactivate-account-usage']); + $roleTestGdprExport = Role::updateOrCreate(['name' => 'test-gdpr-export']); //TODO: remove this permission when GDPR export is no longer in testing //Create permissions $permissionViewBackend = Permission::updateOrCreate(['name' => 'view-backend']); @@ -72,7 +73,7 @@ public function run(): void { $roleEventModerator->givePermissionTo($permissionUpdateEvents); //Revoke permissions from closed-beta role - $roleClosedBeta->revokePermissionTo($permissionCreateManualTrip); //now in open-beta + $roleClosedBeta->revokePermissionTo($permissionCreateManualTrip); //now in open-beta //Assign permissions to open-beta role $roleOpenBeta->givePermissionTo($permissionCreateManualTrip); diff --git a/resources/views/export.blade.php b/resources/views/export.blade.php index 7bf659096..38c43f7a5 100644 --- a/resources/views/export.blade.php +++ b/resources/views/export.blade.php @@ -167,40 +167,43 @@ class="form-control"/> -
-
-

-   - {{ __('export.gdpr') }} -

+ @if(config('trwl.ab_testing.gdpr_export') || auth()->user()->hasRole('test-gdpr-export')) + +
+
+

+   + {{ __('export.gdpr') }} +

- {{__('export.gdpr.description')}} -
- @php - $recent = auth()->user()->recent_gdpr_export; - @endphp + {{__('export.gdpr.description')}} +
+ @php + $recent = auth()->user()->recent_gdpr_export; + @endphp - @if($recent) - {{ __('export.gdpr.recent', ['date' => userTime($recent, __('datetime-format'))]) }} - @endif + @if($recent) + {{ __('export.gdpr.recent', ['date' => userTime($recent, __('datetime-format'))]) }} + @endif -
+
-
- - @csrf -
-
- + + + @csrf +
+
+ +
-
- + +
-
+ @endif
From 690a105c156a7d704be51952a6abbfda7f31ec2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20St=C3=B6ckel?= Date: Fri, 15 Nov 2024 16:25:48 +0100 Subject: [PATCH 15/19] rename `data` -> `userModel` --- .../UserGdprDataService.php | 74 +++++++++---------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/app/Services/PersonalDataSelection/UserGdprDataService.php b/app/Services/PersonalDataSelection/UserGdprDataService.php index fda101651..ecbebf55c 100644 --- a/app/Services/PersonalDataSelection/UserGdprDataService.php +++ b/app/Services/PersonalDataSelection/UserGdprDataService.php @@ -17,16 +17,16 @@ public function __invoke(PersonalDataSelection $personalDataSelection, User $dat $this->addUserPersonalData($personalDataSelection, $data); } - private function addUserPersonalData(PersonalDataSelection $personalDataSelection, User $data): void { - $user = $data->toArray(); - $user['email'] = $data->email; - $user['email_verified_at'] = $data->email_verified_at; - $user['privacy_ack_at'] = $data->privacy_ack_at; - $user['last_login'] = $data->last_login; - $user['created_at'] = $data->created_at; - $user['updated_at'] = $data->updated_at; + private function addUserPersonalData(PersonalDataSelection $personalDataSelection, User $userModel): void { + $user = $userModel->toArray(); + $user['email'] = $userModel->email; + $user['email_verified_at'] = $userModel->email_verified_at; + $user['privacy_ack_at'] = $userModel->privacy_ack_at; + $user['last_login'] = $userModel->last_login; + $user['created_at'] = $userModel->created_at; + $user['updated_at'] = $userModel->updated_at; - $webhooks = $data->webhooks()->with('events')->get(); + $webhooks = $userModel->webhooks()->with('events')->get(); $webhooks = $webhooks->map(function($webhook) { $webhook['created_at'] = $webhook->created_at; $webhook['updated_at'] = $webhook->updated_at; @@ -36,53 +36,53 @@ private function addUserPersonalData(PersonalDataSelection $personalDataSelectio }); - if ($data->avatar && file_exists(public_path('/uploads/avatars/' . $data->avatar))) { + if ($userModel->avatar && file_exists(public_path('/uploads/avatars/' . $userModel->avatar))) { $personalDataSelection - ->addFile(public_path('/uploads/avatars/' . $data->avatar)); + ->addFile(public_path('/uploads/avatars/' . $userModel->avatar)); } $personalDataSelection ->add('user.json', $user) - ->add('notifications.json', $data->notifications()->get()->toJson()) - ->add('likes.json', $data->likes()->get()->toJson()) - ->add('social_profile.json', $data->socialProfile()->with('mastodonserver')->get()) - ->add('event_suggestions.json', EventSuggestion::where('user_id', $data->id)->get()->toJson()) - ->add('events.json', Event::where('approved_by', $data->id)->get()->toJson()) + ->add('notifications.json', $userModel->notifications()->get()->toJson()) + ->add('likes.json', $userModel->likes()->get()->toJson()) + ->add('social_profile.json', $userModel->socialProfile()->with('mastodonserver')->get()) + ->add('event_suggestions.json', EventSuggestion::where('user_id', $userModel->id)->get()->toJson()) + ->add('events.json', Event::where('approved_by', $userModel->id)->get()->toJson()) ->add('webhooks.json', $webhooks) ->add( 'webhook_creation_requests.json', - WebhookCreationRequest::where('user_id', $data->id)->get()->toJson() + WebhookCreationRequest::where('user_id', $userModel->id)->get()->toJson() ) - ->add('tokens.json', TokenController::index($data)->toJson()) - ->add('ics_tokens.json', $data->icsTokens()->get()->toJson()) + ->add('tokens.json', TokenController::index($userModel)->toJson()) + ->add('ics_tokens.json', $userModel->icsTokens()->get()->toJson()) ->add( 'password_resets.json', - DB::table('password_resets')->select(['email', 'created_at'])->where('email', $data->email)->get() + DB::table('password_resets')->select(['email', 'created_at'])->where('email', $userModel->email)->get() ) - ->add('apps.json', $data->oAuthClients()->get()->toJson()) - ->add('follows.json', DB::table('follows')->where('user_id', $data->id)->get()) - ->add('followings.json', DB::table('follows')->where('follow_id', $data->id)->get()) - ->add('blocks.json', DB::table('user_blocks')->where('user_id', $data->id)->get()) - ->add('mutes.json', DB::table('user_mutes')->where('user_id', $data->id)->get()) - ->add('follow_requests.json', DB::table('follow_requests')->where('user_id', $data->id)->get()) - ->add('follows_requests.json', DB::table('follow_requests')->where('follow_id', $data->id)->get()) - ->add('sessions.json', $data->sessions()->get()->toJson()) - ->add('home.json', $data->home()->get()->toJson()) - ->add('hafas_trips.json', DB::table('hafas_trips')->where('user_id', $data->id)->get()) - ->add('mentions.json', Mention::where('mentioned_id', $data->id)->get()->toJson()) - ->add('roles.json', $data->roles()->get()->toJson()) + ->add('apps.json', $userModel->oAuthClients()->get()->toJson()) + ->add('follows.json', DB::table('follows')->where('user_id', $userModel->id)->get()) + ->add('followings.json', DB::table('follows')->where('follow_id', $userModel->id)->get()) + ->add('blocks.json', DB::table('user_blocks')->where('user_id', $userModel->id)->get()) + ->add('mutes.json', DB::table('user_mutes')->where('user_id', $userModel->id)->get()) + ->add('follow_requests.json', DB::table('follow_requests')->where('user_id', $userModel->id)->get()) + ->add('follows_requests.json', DB::table('follow_requests')->where('follow_id', $userModel->id)->get()) + ->add('sessions.json', $userModel->sessions()->get()->toJson()) + ->add('home.json', $userModel->home()->get()->toJson()) + ->add('hafas_trips.json', DB::table('hafas_trips')->where('user_id', $userModel->id)->get()) + ->add('mentions.json', Mention::where('mentioned_id', $userModel->id)->get()->toJson()) + ->add('roles.json', $userModel->roles()->get()->toJson()) ->add( 'activity_log.json', - DB::table('activity_log')->where('causer_type', get_class($data))->where('causer_id', $data->id)->get() + DB::table('activity_log')->where('causer_type', get_class($userModel))->where('causer_id', $userModel->id)->get() ) - ->add('permissions.json', $data->permissions()->get()->toJson()) - ->add('statuses.json', $data->statuses()->with('tags')->get()) + ->add('permissions.json', $userModel->permissions()->get()->toJson()) + ->add('statuses.json', $userModel->statuses()->with('tags')->get()) ->add( 'reports.json', DB::table('reports') ->select('subject_type', 'subject_id', 'reason', 'description', 'reporter_id') - ->where('reporter_id', $data->id) + ->where('reporter_id', $userModel->id) ->get() - )->add('trusted_users.json', DB::table('trusted_users')->where('user_id', $data->id)->get()); + )->add('trusted_users.json', DB::table('trusted_users')->where('user_id', $userModel->id)->get()); } } From f28056e42c2e9f33407fb77c1fcb0239d31b1ee3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20St=C3=B6ckel?= Date: Fri, 15 Nov 2024 16:37:31 +0100 Subject: [PATCH 16/19] use only --- .../PersonalDataSelection/UserGdprDataService.php | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/app/Services/PersonalDataSelection/UserGdprDataService.php b/app/Services/PersonalDataSelection/UserGdprDataService.php index ecbebf55c..8b4d0d087 100644 --- a/app/Services/PersonalDataSelection/UserGdprDataService.php +++ b/app/Services/PersonalDataSelection/UserGdprDataService.php @@ -18,13 +18,10 @@ public function __invoke(PersonalDataSelection $personalDataSelection, User $dat } private function addUserPersonalData(PersonalDataSelection $personalDataSelection, User $userModel): void { - $user = $userModel->toArray(); - $user['email'] = $userModel->email; - $user['email_verified_at'] = $userModel->email_verified_at; - $user['privacy_ack_at'] = $userModel->privacy_ack_at; - $user['last_login'] = $userModel->last_login; - $user['created_at'] = $userModel->created_at; - $user['updated_at'] = $userModel->updated_at; + $userData = $userModel->only([ + 'email', 'email_verified_at', 'privacy_ack_at', + 'last_login', 'created_at', 'updated_at' + ]); $webhooks = $userModel->webhooks()->with('events')->get(); $webhooks = $webhooks->map(function($webhook) { @@ -42,7 +39,7 @@ private function addUserPersonalData(PersonalDataSelection $personalDataSelectio } $personalDataSelection - ->add('user.json', $user) + ->add('user.json', $userData) ->add('notifications.json', $userModel->notifications()->get()->toJson()) ->add('likes.json', $userModel->likes()->get()->toJson()) ->add('social_profile.json', $userModel->socialProfile()->with('mastodonserver')->get()) From 94f8b1ee6db8d4c0c04c00fd42c1b677f3653afb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20St=C3=B6ckel?= Date: Fri, 15 Nov 2024 16:42:12 +0100 Subject: [PATCH 17/19] add attributes from user model --- app/Services/PersonalDataSelection/UserGdprDataService.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/Services/PersonalDataSelection/UserGdprDataService.php b/app/Services/PersonalDataSelection/UserGdprDataService.php index 8b4d0d087..267fd5ba3 100644 --- a/app/Services/PersonalDataSelection/UserGdprDataService.php +++ b/app/Services/PersonalDataSelection/UserGdprDataService.php @@ -19,6 +19,9 @@ public function __invoke(PersonalDataSelection $personalDataSelection, User $dat private function addUserPersonalData(PersonalDataSelection $personalDataSelection, User $userModel): void { $userData = $userModel->only([ + 'name', 'username', 'home_id', 'private_profile', 'default_status_visibility', + 'default_status_sensitivity', 'prevent_index', 'privacy_hide_days', 'language', + 'timezone', 'friend_checkin', 'likes_enabled', 'points_enabled', 'mapprovider', 'email', 'email_verified_at', 'privacy_ack_at', 'last_login', 'created_at', 'updated_at' ]); From dff7f2fb89965f2fb89c4f9b6a14be4582bae5b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20St=C3=B6ckel?= Date: Fri, 15 Nov 2024 16:42:59 +0100 Subject: [PATCH 18/19] use only --- .../PersonalDataSelection/UserGdprDataService.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/Services/PersonalDataSelection/UserGdprDataService.php b/app/Services/PersonalDataSelection/UserGdprDataService.php index 267fd5ba3..846814a6c 100644 --- a/app/Services/PersonalDataSelection/UserGdprDataService.php +++ b/app/Services/PersonalDataSelection/UserGdprDataService.php @@ -28,11 +28,9 @@ private function addUserPersonalData(PersonalDataSelection $personalDataSelectio $webhooks = $userModel->webhooks()->with('events')->get(); $webhooks = $webhooks->map(function($webhook) { - $webhook['created_at'] = $webhook->created_at; - $webhook['updated_at'] = $webhook->updated_at; - $webhook['client_id'] = (int) $webhook->oauth_client_id ?? null; - unset($webhook['url']); - return $webhook; + return $webhook->only([ + 'oauth_client_id', 'created_at', 'updated_at' + ]); }); From f0e7b10b8756bb89b84db7b474e01282301ae391 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20St=C3=B6ckel?= Date: Fri, 15 Nov 2024 16:44:17 +0100 Subject: [PATCH 19/19] add todo --- .../UserGdprDataService.php | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/app/Services/PersonalDataSelection/UserGdprDataService.php b/app/Services/PersonalDataSelection/UserGdprDataService.php index 846814a6c..a2e442401 100644 --- a/app/Services/PersonalDataSelection/UserGdprDataService.php +++ b/app/Services/PersonalDataSelection/UserGdprDataService.php @@ -41,46 +41,47 @@ private function addUserPersonalData(PersonalDataSelection $personalDataSelectio $personalDataSelection ->add('user.json', $userData) - ->add('notifications.json', $userModel->notifications()->get()->toJson()) - ->add('likes.json', $userModel->likes()->get()->toJson()) - ->add('social_profile.json', $userModel->socialProfile()->with('mastodonserver')->get()) - ->add('event_suggestions.json', EventSuggestion::where('user_id', $userModel->id)->get()->toJson()) - ->add('events.json', Event::where('approved_by', $userModel->id)->get()->toJson()) + ->add('notifications.json', $userModel->notifications()->get()->toJson()) //TODO: columns definieren + ->add('likes.json', $userModel->likes()->get()->toJson()) //TODO: columns definieren + ->add('social_profile.json', $userModel->socialProfile()->with('mastodonserver')->get()) //TODO: columns definieren + ->add('event_suggestions.json', EventSuggestion::where('user_id', $userModel->id)->get()->toJson()) //TODO: columns definieren + ->add('events.json', Event::where('approved_by', $userModel->id)->get()->toJson()) //TODO: columns definieren ->add('webhooks.json', $webhooks) ->add( 'webhook_creation_requests.json', - WebhookCreationRequest::where('user_id', $userModel->id)->get()->toJson() + WebhookCreationRequest::where('user_id', $userModel->id)->get()->toJson() //TODO: columns definieren ) - ->add('tokens.json', TokenController::index($userModel)->toJson()) - ->add('ics_tokens.json', $userModel->icsTokens()->get()->toJson()) + ->add('tokens.json', TokenController::index($userModel)->toJson()) //TODO: columns definieren + ->add('ics_tokens.json', $userModel->icsTokens()->get()->toJson()) //TODO: columns definieren ->add( 'password_resets.json', - DB::table('password_resets')->select(['email', 'created_at'])->where('email', $userModel->email)->get() + DB::table('password_resets')->select(['email', 'created_at'])->where('email', $userModel->email)->get() //TODO: columns definieren ) - ->add('apps.json', $userModel->oAuthClients()->get()->toJson()) - ->add('follows.json', DB::table('follows')->where('user_id', $userModel->id)->get()) - ->add('followings.json', DB::table('follows')->where('follow_id', $userModel->id)->get()) - ->add('blocks.json', DB::table('user_blocks')->where('user_id', $userModel->id)->get()) - ->add('mutes.json', DB::table('user_mutes')->where('user_id', $userModel->id)->get()) - ->add('follow_requests.json', DB::table('follow_requests')->where('user_id', $userModel->id)->get()) - ->add('follows_requests.json', DB::table('follow_requests')->where('follow_id', $userModel->id)->get()) - ->add('sessions.json', $userModel->sessions()->get()->toJson()) - ->add('home.json', $userModel->home()->get()->toJson()) - ->add('hafas_trips.json', DB::table('hafas_trips')->where('user_id', $userModel->id)->get()) - ->add('mentions.json', Mention::where('mentioned_id', $userModel->id)->get()->toJson()) - ->add('roles.json', $userModel->roles()->get()->toJson()) + ->add('apps.json', $userModel->oAuthClients()->get()->toJson()) //TODO: columns definieren + ->add('follows.json', DB::table('follows')->where('user_id', $userModel->id)->get()) //TODO: columns definieren + ->add('followings.json', DB::table('follows')->where('follow_id', $userModel->id)->get()) //TODO: columns definieren + ->add('blocks.json', DB::table('user_blocks')->where('user_id', $userModel->id)->get()) //TODO: columns definieren + ->add('mutes.json', DB::table('user_mutes')->where('user_id', $userModel->id)->get()) //TODO: columns definieren + ->add('follow_requests.json', DB::table('follow_requests')->where('user_id', $userModel->id)->get()) //TODO: columns definieren + ->add('follows_requests.json', DB::table('follow_requests')->where('follow_id', $userModel->id)->get()) //TODO: columns definieren + ->add('sessions.json', $userModel->sessions()->get()->toJson()) //TODO: columns definieren + ->add('home.json', $userModel->home()->get()->toJson()) //TODO: columns definieren + ->add('hafas_trips.json', DB::table('hafas_trips')->where('user_id', $userModel->id)->get()) //TODO: columns definieren + ->add('mentions.json', Mention::where('mentioned_id', $userModel->id)->get()->toJson()) //TODO: columns definieren + ->add('roles.json', $userModel->roles()->get()->toJson()) //TODO: columns definieren ->add( 'activity_log.json', - DB::table('activity_log')->where('causer_type', get_class($userModel))->where('causer_id', $userModel->id)->get() + DB::table('activity_log')->where('causer_type', get_class($userModel))->where('causer_id', $userModel->id)->get() //TODO: columns definieren ) - ->add('permissions.json', $userModel->permissions()->get()->toJson()) - ->add('statuses.json', $userModel->statuses()->with('tags')->get()) + ->add('permissions.json', $userModel->permissions()->get()->toJson()) //TODO: columns definieren + ->add('statuses.json', $userModel->statuses()->with('tags')->get()) //TODO: columns definieren ->add( 'reports.json', DB::table('reports') ->select('subject_type', 'subject_id', 'reason', 'description', 'reporter_id') ->where('reporter_id', $userModel->id) - ->get() - )->add('trusted_users.json', DB::table('trusted_users')->where('user_id', $userModel->id)->get()); + ->get() //TODO: columns definieren + ) + ->add('trusted_users.json', DB::table('trusted_users')->where('user_id', $userModel->id)->get()); //TODO: columns definieren } }