From 6db25f43697c7530d36c49fbc23f4676b627d1a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Drunen?= Date: Sun, 23 Aug 2020 01:17:31 +0200 Subject: [PATCH 1/6] Create DeleteUserButton component and show it in settings --- app/Http/Controllers/SettingsController.php | 7 +++ resources/assets/js/app.js | 2 + .../assets/js/components/DeleteUserButton.vue | 55 +++++++++++++++++++ resources/assets/sass/components/input.scss | 16 ++++++ resources/views/settings/account.blade.php | 6 ++ routes/web.php | 1 + 6 files changed, 87 insertions(+) create mode 100644 resources/assets/js/components/DeleteUserButton.vue diff --git a/app/Http/Controllers/SettingsController.php b/app/Http/Controllers/SettingsController.php index cbfd2dea..8e9178fd 100644 --- a/app/Http/Controllers/SettingsController.php +++ b/app/Http/Controllers/SettingsController.php @@ -104,6 +104,13 @@ public function getAccount() return view('settings.account'); } + public function postAccountDelete() + { + // TODO DELETE + + return redirect()->route('login'); + } + public function getSpaces() { return view('settings.spaces.index', [ diff --git a/resources/assets/js/app.js b/resources/assets/js/app.js index b4570f75..6f93345f 100644 --- a/resources/assets/js/app.js +++ b/resources/assets/js/app.js @@ -6,6 +6,7 @@ import { Chrome } from 'vue-color'; import ButtonDropdown from './components/ButtonDropdown.vue'; import DatePicker from './components/DatePicker.vue'; import BarChart from './components/BarChart.vue'; +import DeleteUserButton from './components/DeleteUserButton.vue'; import Dropdown from './components/Dropdown.vue'; import TransactionWizard from './components/TransactionWizard.vue'; import ValidationError from './components/ValidationError.vue'; @@ -21,6 +22,7 @@ Vue.component('button-dropdown', ButtonDropdown); Vue.component('datepicker', DatePicker); // TODO DEPRECATE Vue.component('date-picker', DatePicker); Vue.component('barchart', BarChart); +Vue.component('delete-user-button', DeleteUserButton); Vue.component('dropdown', Dropdown); Vue.component('transaction-wizard', TransactionWizard); Vue.component('validation-error', ValidationError); diff --git a/resources/assets/js/components/DeleteUserButton.vue b/resources/assets/js/components/DeleteUserButton.vue new file mode 100644 index 00000000..8fd088f8 --- /dev/null +++ b/resources/assets/js/components/DeleteUserButton.vue @@ -0,0 +1,55 @@ + + + diff --git a/resources/assets/sass/components/input.scss b/resources/assets/sass/components/input.scss index cf8e3ab9..6db5e8ca 100644 --- a/resources/assets/sass/components/input.scss +++ b/resources/assets/sass/components/input.scss @@ -140,10 +140,26 @@ button.button { background: none; } + &.button--link-danger { + padding: 0; + font-size: 16px; + color: $colors-red; + background: none; + } + &.button--wide { padding: 12.5px 20px; width: 100%; } + + &.button--light { + background: #EEE; + color: #000; + } + + &.button--danger { + background: $colors-red; + } } .button, .button-dropdown { diff --git a/resources/views/settings/account.blade.php b/resources/views/settings/account.blade.php index 314fe5ff..c56a2e9f 100644 --- a/resources/views/settings/account.blade.php +++ b/resources/views/settings/account.blade.php @@ -27,3 +27,9 @@ @endsection + +@section('settings_body_formless') + +@endsection diff --git a/routes/web.php b/routes/web.php index 92723f06..01283e8c 100644 --- a/routes/web.php +++ b/routes/web.php @@ -120,6 +120,7 @@ Route::post('/settings', [SettingsController::class, 'postIndex']); Route::get('/settings/profile', [SettingsController::class, 'getProfile'])->name('profile'); Route::get('/settings/account', [SettingsController::class, 'getAccount'])->name('account'); + Route::post('/settings/account/delete', [SettingsController::class, 'postAccountDelete'])->name('account.delete'); Route::get('/settings/preferences', [SettingsController::class, 'getPreferences'])->name('preferences'); Route::get('/settings/billing', [SettingsController::class, 'getBilling'])->name('billing')->middleware('stripe'); Route::post('/settings/billing/upgrade', [SettingsController::class, 'postUpgrade'])->name('billing.upgrade')->middleware('stripe'); From 57d0a03355abf99f81c3cdc445e27a4af91ee8ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Drunen?= Date: Sun, 23 Aug 2020 02:06:04 +0200 Subject: [PATCH 2/6] Create action that handles user deletion --- app/Actions/DeleteUserAction.php | 64 +++++++++++++++++++ .../UserActiveStripeSubscriptionException.php | 10 +++ app/Http/Controllers/SettingsController.php | 5 +- ...7_support_anonymization_of_users_table.php | 24 +++++++ 4 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 app/Actions/DeleteUserAction.php create mode 100644 app/Exceptions/UserActiveStripeSubscriptionException.php create mode 100644 database/migrations/2020_08_22_232957_support_anonymization_of_users_table.php diff --git a/app/Actions/DeleteUserAction.php b/app/Actions/DeleteUserAction.php new file mode 100644 index 00000000..1d48e148 --- /dev/null +++ b/app/Actions/DeleteUserAction.php @@ -0,0 +1,64 @@ +stripe_customer_id) { + $stripe = new StripeClient(config('stripe.secret')); + + $stripeSubscriptions = $stripe->subscriptions->all([ + 'customer' => $user->stripe_customer_id, + 'status' => 'active' + ]); + + $activeStripeSubscriptions = 0; + + foreach ($stripeSubscriptions as $subscription) { + // Determine whether or not subscription is active by looking at "ended_at" + if (!$subscription->ended_at) { + $activeStripeSubscriptions++; + } + } + + if ($activeStripeSubscriptions > 0) { + throw new UserActiveStripeSubscriptionException(); + } + } + + // Commit to deleting + if ($user->avatar) { + Storage::delete('public/avatars/' . $user->avatar); + } + + if (Helper::arePlansEnabled() && $user->stripe_customer_id) { + $stripe = new StripeClient(config('stripe.secret')); + + $stripe->customers->delete($user->stripe_customer_id); + } + + $user->fill([ + 'avatar' => null, + 'name' => null, + 'email' => null, + 'stripe_customer_id' => null + ])->save(); + } +} diff --git a/app/Exceptions/UserActiveStripeSubscriptionException.php b/app/Exceptions/UserActiveStripeSubscriptionException.php new file mode 100644 index 00000000..4c780ab5 --- /dev/null +++ b/app/Exceptions/UserActiveStripeSubscriptionException.php @@ -0,0 +1,10 @@ +execute(Auth::id()); + + Auth::logout(); return redirect()->route('login'); } diff --git a/database/migrations/2020_08_22_232957_support_anonymization_of_users_table.php b/database/migrations/2020_08_22_232957_support_anonymization_of_users_table.php new file mode 100644 index 00000000..7cacdc95 --- /dev/null +++ b/database/migrations/2020_08_22_232957_support_anonymization_of_users_table.php @@ -0,0 +1,24 @@ +string('name')->nullable()->change(); + $table->string('email')->nullable()->change(); + }); + } + + public function down(): void + { + Schema::table('users', function ($table) { + $table->string('name')->nullable(false)->change(); + $table->string('email')->nullable(false)->change(); + }); + } +} From d9447f98b00f160d43880e86c3c0830d8f138bb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Drunen?= Date: Mon, 24 Aug 2020 11:20:26 +0200 Subject: [PATCH 3/6] Show errors during deletion of user on settings page --- app/Http/Controllers/SettingsController.php | 11 +++++++++-- resources/views/settings/account.blade.php | 9 ++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/app/Http/Controllers/SettingsController.php b/app/Http/Controllers/SettingsController.php index a2325dd1..7f9812ce 100644 --- a/app/Http/Controllers/SettingsController.php +++ b/app/Http/Controllers/SettingsController.php @@ -6,6 +6,7 @@ use App\Actions\CreateStripeCustomerAction; use App\Actions\DeleteUserAction; use App\Actions\FetchStripeSubscriptionAction; +use App\Exceptions\UserActiveStripeSubscriptionException; use App\Exceptions\UserStripelessException; use Illuminate\Http\Request; use App\Mail\PasswordChanged; @@ -105,9 +106,15 @@ public function getAccount() return view('settings.account'); } - public function postAccountDelete() + public function postAccountDelete(Request $request) { - (new DeleteUserAction())->execute(Auth::id()); + try { + (new DeleteUserAction())->execute(Auth::id()); + } catch (UserActiveStripeSubscriptionException $e) { + $request->session()->flash('delete_user_error', 'active_stripe_subscription'); + + return redirect()->route('settings.account'); + } Auth::logout(); diff --git a/resources/views/settings/account.blade.php b/resources/views/settings/account.blade.php index c56a2e9f..fd5d734e 100644 --- a/resources/views/settings/account.blade.php +++ b/resources/views/settings/account.blade.php @@ -29,7 +29,10 @@ @endsection @section('settings_body_formless') - +
+ @if (session('delete_user_error') === 'active_stripe_subscription') +
Unable to delete user, you still have a premium plan (and would continue to be billed otherwise)
+ @endif + +
@endsection From 50d888fe3f5b63e727f1f535ada2e4ce657a31f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Drunen?= Date: Mon, 24 Aug 2020 13:58:04 +0200 Subject: [PATCH 4/6] Test DeleteUserAction --- tests/Unit/Actions/DeleteUserTest.php | 54 +++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 tests/Unit/Actions/DeleteUserTest.php diff --git a/tests/Unit/Actions/DeleteUserTest.php b/tests/Unit/Actions/DeleteUserTest.php new file mode 100644 index 00000000..2d3b31b7 --- /dev/null +++ b/tests/Unit/Actions/DeleteUserTest.php @@ -0,0 +1,54 @@ +expectException(UserNotFoundException::class); + + (new DeleteUserAction())->execute(999); + } + + public function testSuccessfulAvatarCleared(): void + { + $image = \Image::canvas(500, 500, '#CCC'); + + Storage::put('public/avatars/yabadabadoo.png', (string) $image->encode()); + + $user = factory(User::class)->create([ + 'avatar' => 'yabadabadoo.png' + ]); + + $this->assertFileExists(storage_path() . '/app/public/avatars/yabadabadoo.png'); + + (new DeleteUserAction())->execute($user->id); + + $this->assertFileNotExists(storage_path() . '/app/public/avatars/yabadabadoo.png'); + } + + public function testSuccessfulColumnsCleared(): void + { + $user = factory(User::class)->create([ + 'name' => 'John Doe', + 'email' => 'jdoe@gmail.com' + ]); + + $this->assertNotNull($user->name); + $this->assertNotNull($user->email); + + (new DeleteUserAction())->execute($user->id); + + $user->refresh(); + + $this->assertNull($user->name); + $this->assertNull($user->email); + } +} From 88ca164ba5797ca377effe34217f68ee89722dc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Drunen?= Date: Mon, 24 Aug 2020 14:16:41 +0200 Subject: [PATCH 5/6] Refactor avatar accessor in User model --- app/Models/User.php | 2 +- resources/views/activities/index.blade.php | 2 +- resources/views/layout.blade.php | 2 +- resources/views/settings/profile.blade.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Models/User.php b/app/Models/User.php index a0f87c8c..b68afbfe 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -28,7 +28,7 @@ class User extends Authenticatable ]; // Accessors - public function getAvatarAttribute($avatar) + public function getAvatarPathAttribute($avatar) { return $avatar ? '/storage/avatars/' . $avatar : 'https://via.placeholder.com/250'; } diff --git a/resources/views/activities/index.blade.php b/resources/views/activities/index.blade.php index 0b5b13b1..9ab1bd98 100644 --- a/resources/views/activities/index.blade.php +++ b/resources/views/activities/index.blade.php @@ -10,7 +10,7 @@
@if ($activity->user) - + @endif
{{ __('activities.' . $activity->action) }} #{{ $activity->entity_id }}
diff --git a/resources/views/layout.blade.php b/resources/views/layout.blade.php index e75fa64c..8073de4c 100644 --- a/resources/views/layout.blade.php +++ b/resources/views/layout.blade.php @@ -100,7 +100,7 @@
  • - +
    • diff --git a/resources/views/settings/profile.blade.php b/resources/views/settings/profile.blade.php index e33d31ea..90d7961d 100644 --- a/resources/views/settings/profile.blade.php +++ b/resources/views/settings/profile.blade.php @@ -9,7 +9,7 @@
      - + @include('partials.validation_error', ['payload' => 'avatar'])
      From 9694d363fe35d5526fd3b42497246833fb1869b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20van=20Drunen?= Date: Mon, 24 Aug 2020 14:50:22 +0200 Subject: [PATCH 6/6] Prevent sending of weekly reports to deleted users --- app/Jobs/SendWeeklyReports.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/Jobs/SendWeeklyReports.php b/app/Jobs/SendWeeklyReports.php index 43a0edb4..8f40bfa6 100644 --- a/app/Jobs/SendWeeklyReports.php +++ b/app/Jobs/SendWeeklyReports.php @@ -49,6 +49,10 @@ public function handle() ORDER BY spendings.amount DESC LIMIT 1', [$space->id, $lastWeekDate, $currentDate]); foreach ($space->users as $user) { + if (!$user->email) { + continue; + } + // Only send if user wants to receive report if ($user->weekly_report) { Mail::to($user->email)->queue(new WeeklyReport(