From a4681766f878d9717b9462eb5998fce4b67e8f02 Mon Sep 17 00:00:00 2001 From: Spitfire Date: Wed, 24 Jan 2024 16:51:00 -0600 Subject: [PATCH 1/9] Email Validation. --- .../Settings/SubscriptionController.php | 2 +- .../User/EmailValidationController.php | 32 +++++++++++ .../Subscriptions/EmailValidationJob.php | 53 +++++++++++++++++++ .../Subscription/User/ValidationEmail.php | 49 +++++++++++++++++ app/Models/UserFlag.php | 1 + app/Models/UserValidation.php | 36 +++++++++++++ app/User.php | 38 +++++++++++++ ...2_203110_create_user_validations_table.php | 35 ++++++++++++ database/seeders/DatabaseSeeder.php | 1 + lang/en/emails/validation.php | 7 +++ .../validation/user-html.blade.php | 22 ++++++++ .../settings/subscription/change.blade.php | 9 ++-- routes/web.php | 2 + 13 files changed, 283 insertions(+), 4 deletions(-) create mode 100644 app/Http/Controllers/User/EmailValidationController.php create mode 100644 app/Jobs/Emails/Subscriptions/EmailValidationJob.php create mode 100644 app/Mail/Subscription/User/ValidationEmail.php create mode 100644 app/Models/UserValidation.php create mode 100644 database/migrations/2024_01_22_203110_create_user_validations_table.php create mode 100644 lang/en/emails/validation.php create mode 100644 resources/views/emails/subscriptions/validation/user-html.blade.php diff --git a/app/Http/Controllers/Settings/SubscriptionController.php b/app/Http/Controllers/Settings/SubscriptionController.php index 149f1d2533..bd94b5c7cf 100644 --- a/app/Http/Controllers/Settings/SubscriptionController.php +++ b/app/Http/Controllers/Settings/SubscriptionController.php @@ -72,7 +72,7 @@ public function index() )); } - public function change(Request $request) + public function change(Request $request, Tier $tier) { $user = $request->user(); $period = $request->get('period', 'monthly'); diff --git a/app/Http/Controllers/User/EmailValidationController.php b/app/Http/Controllers/User/EmailValidationController.php new file mode 100644 index 0000000000..57eeb203d4 --- /dev/null +++ b/app/Http/Controllers/User/EmailValidationController.php @@ -0,0 +1,32 @@ +get('token'); + + /** @var UserValidation $validation */ + $validation = UserValidation::where('user_id', $user->id) + ->where('token', $token) + ->first(); + + if ($validation->exists) { + $validation->is_valid = true; + $validation->saveQuietly(); + } else { + response()->redirectTo(route('settings.subscription'))->withError(__('emails/validation.error')); + } + + return response()->redirectTo(route('settings.subscription'))->withSuccess(__('emails/validation.success')); + } +} diff --git a/app/Jobs/Emails/Subscriptions/EmailValidationJob.php b/app/Jobs/Emails/Subscriptions/EmailValidationJob.php new file mode 100644 index 0000000000..6711f35309 --- /dev/null +++ b/app/Jobs/Emails/Subscriptions/EmailValidationJob.php @@ -0,0 +1,53 @@ +user = $user->id; + $this->token = $token; + } + + /** + * @throws \Illuminate\Contracts\Container\BindingResolutionException + */ + public function handle() + { + // User deleted their account already? Sure thing + /** @var User|null $user */ + $user = User::find($this->user); + if (empty($user)) { + return; + } + + // Send an email to the user + Mail::to($user->email) + ->locale($user->locale) + ->send( + new ValidationEmail($user, $this->token) + ); + } +} diff --git a/app/Mail/Subscription/User/ValidationEmail.php b/app/Mail/Subscription/User/ValidationEmail.php new file mode 100644 index 0000000000..cffa1f9677 --- /dev/null +++ b/app/Mail/Subscription/User/ValidationEmail.php @@ -0,0 +1,49 @@ +user = $user; + $this->token = $token; + } + + /** + * Build the message. + * + * @return $this + */ + public function build() + { + return $this + ->from(['address' => config('app.email'), 'name' => 'Kanka Team']) + ->subject(__('emails/subscriptions/validation.title')) + ->view('emails.subscriptions.validation.user-html') + ->tag('validation'); + } +} diff --git a/app/Models/UserFlag.php b/app/Models/UserFlag.php index f82c2ed228..87d139e5af 100644 --- a/app/Models/UserFlag.php +++ b/app/Models/UserFlag.php @@ -16,6 +16,7 @@ class UserFlag extends Model public const FLAG_INACTIVE_1 = 'inactive_1'; public const FLAG_INACTIVE_2 = 'inactive_2'; + public const FLAG_EMAIL = 'email'; public function user() { diff --git a/app/Models/UserValidation.php b/app/Models/UserValidation.php new file mode 100644 index 0000000000..2d74232793 --- /dev/null +++ b/app/Models/UserValidation.php @@ -0,0 +1,36 @@ +belongsTo(User::class); + } + + /** + * Automatically prune old elements from the db + * @return \Illuminate\Database\Eloquent\Builder + */ + public function prunable() + { + return static::where('is_valid', false)->where('created_at', '<=', now()->subDays(1)); + } +} diff --git a/app/User.php b/app/User.php index 30f5d4ee20..176a7a239d 100644 --- a/app/User.php +++ b/app/User.php @@ -17,8 +17,11 @@ use App\Models\Scopes\UserScope; use App\Models\UserLog; use App\Models\UserSetting; +use App\Models\UserFlag; +use App\Models\UserValidation; use App\Models\Relations\UserRelations; use Carbon\Carbon; +use App\Jobs\Emails\Subscriptions\EmailValidationJob; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Notifications\Notifiable; use Illuminate\Support\Collection; @@ -433,6 +436,12 @@ public function isFrauding(): bool if (!empty($this->provider)) { return false; } + + $validation = UserValidation::where('user_id', $this->id)->where('is_valid', true)->first(); + if ($validation) { + return false; + } + // If the account was created recently, add some small checks /*if ($this->created_at->isAfter(Carbon::now()->subHour())) { // User's name is directly in the campaign name @@ -453,6 +462,35 @@ public function isFrauding(): bool ->count() >= 2; } + public function requiresEmail(): self + { + $token = UserValidation::where('user_id', $this->id)->first(); + if ($token && $token->is_valid) { + return $this; + } + //Check for existing token + $flag = UserFlag::where('user_id', $this->id)->where('flag', UserFlag::FLAG_EMAIL)->first(); + + if (!$flag) { + $flag = new UserFlag(); + $flag->user_id = $this->id; + $flag->flag = UserFlag::FLAG_EMAIL; + $flag->save(); + } + + if (!$token) { + $token = new UserValidation(); + $token->token = Str::uuid(); + $token->user_id = $this->id; + $token->is_valid = false; + $token->save(); + } + + EmailValidationJob::dispatch($this, $token->token); + + return $this; + } + /** * List of campaigns the user is the only admin of. This is used for the automatic purge warning emails */ diff --git a/database/migrations/2024_01_22_203110_create_user_validations_table.php b/database/migrations/2024_01_22_203110_create_user_validations_table.php new file mode 100644 index 0000000000..0d09e9d31a --- /dev/null +++ b/database/migrations/2024_01_22_203110_create_user_validations_table.php @@ -0,0 +1,35 @@ +id(); + $table->uuid('token'); + $table->unsignedInteger('user_id'); + $table->boolean('is_valid'); + $table->timestamps(); + + $table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete(); + $table->index(['is_valid']); + + + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('user_validations'); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 0918ef9a8b..5b62c76603 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -25,5 +25,6 @@ public function run() $this->call(PostLayoutTableSeeder::class); $this->call(FeatureCategorySeeder::class); $this->call(FeatureStatusSeeder::class); + $this->call(TierSeeder::class); } } diff --git a/lang/en/emails/validation.php b/lang/en/emails/validation.php new file mode 100644 index 0000000000..53a784175e --- /dev/null +++ b/lang/en/emails/validation.php @@ -0,0 +1,7 @@ + 'Email validation required for subscription, please check your inbox to continue the validation process.', + 'success' => 'Email succesfully validated.', + 'error' => 'Validation failed, please try again.', +]; diff --git a/resources/views/emails/subscriptions/validation/user-html.blade.php b/resources/views/emails/subscriptions/validation/user-html.blade.php new file mode 100644 index 0000000000..c6e49893ff --- /dev/null +++ b/resources/views/emails/subscriptions/validation/user-html.blade.php @@ -0,0 +1,22 @@ +@extends('emails.base', [ + 'utmSource' => 'subscription', + 'utmCampaign' => 'failed-charge' +]) + +@section('content') +

+ Email Validation +

+

+ {{ __('emails/subscriptions/upcoming.dear', ['name' => $user->name]) }}, +

+ +

This is an automatic notification.

+

To validate the email for your Kanka account click here. This link will expire in 24 hours.

+

If the above link doesnt work, open the following URL in your web browser {{ 'https://app.kanka.io/users/' . $user->id . '/validation?token=' . $token }}

+

+ {{ __('emails/subscriptions/upcoming.closing') }}
+ The Kanka Team +

+ +@endsection diff --git a/resources/views/settings/subscription/change.blade.php b/resources/views/settings/subscription/change.blade.php index 665f7eeddf..d8aba330d3 100644 --- a/resources/views/settings/subscription/change.blade.php +++ b/resources/views/settings/subscription/change.blade.php @@ -5,9 +5,12 @@
- @if ($user->isFrauding()) + @if (!$user->isFrauding()) + @php + $user->requiresEmail(); + @endphp - {{ __('settings.subscription.errors.failed', ['email' => config('app.email')]) }} + {{ __('emails/validation.modal') }} @endif @@ -76,7 +79,7 @@
  • - giropay + Giropay
  • @endif diff --git a/routes/web.php b/routes/web.php index 51264ab619..b1222b5cb9 100644 --- a/routes/web.php +++ b/routes/web.php @@ -34,3 +34,5 @@ Route::get('roadmap/{feature}', [App\Http\Controllers\Roadmap\FeatureController::class, 'show'])->name('roadmap.feature.show'); Route::post('roadmap/{feature}/upvote', [App\Http\Controllers\Roadmap\FeatureController::class, 'upvote'])->name('roadmap.upvote'); Route::post('roadmap/submit', [App\Http\Controllers\Roadmap\FeatureController::class, 'store'])->name('roadmap.store'); + +Route::get('/users/{user}/validation', [App\Http\Controllers\User\EmailValidationController::class, 'validateEmail'])->name('validation.email'); From 018bc97160a0d442ff28c0e7ecfe2d21d0e6fbf9 Mon Sep 17 00:00:00 2001 From: spitfire305 Date: Wed, 24 Jan 2024 22:52:41 +0000 Subject: [PATCH 2/9] Fix styling --- app/Http/Controllers/User/EmailValidationController.php | 4 +--- app/Jobs/Emails/Subscriptions/EmailValidationJob.php | 1 - app/Mail/Subscription/User/ValidationEmail.php | 1 - app/Observers/CampaignObserver.php | 2 +- app/Renderers/DatagridRenderer2.php | 2 +- app/User.php | 2 +- .../2024_01_22_203110_create_user_validations_table.php | 3 +-- 7 files changed, 5 insertions(+), 10 deletions(-) diff --git a/app/Http/Controllers/User/EmailValidationController.php b/app/Http/Controllers/User/EmailValidationController.php index 57eeb203d4..f2c6a9ce0f 100644 --- a/app/Http/Controllers/User/EmailValidationController.php +++ b/app/Http/Controllers/User/EmailValidationController.php @@ -7,10 +7,8 @@ use App\User; use Illuminate\Http\Request; - class EmailValidationController extends Controller { - public function validateEmail(Request $request, User $user) { $token = $request->get('token'); @@ -26,7 +24,7 @@ public function validateEmail(Request $request, User $user) } else { response()->redirectTo(route('settings.subscription'))->withError(__('emails/validation.error')); } - + return response()->redirectTo(route('settings.subscription'))->withSuccess(__('emails/validation.success')); } } diff --git a/app/Jobs/Emails/Subscriptions/EmailValidationJob.php b/app/Jobs/Emails/Subscriptions/EmailValidationJob.php index 6711f35309..e9889ae710 100644 --- a/app/Jobs/Emails/Subscriptions/EmailValidationJob.php +++ b/app/Jobs/Emails/Subscriptions/EmailValidationJob.php @@ -3,7 +3,6 @@ namespace App\Jobs\Emails\Subscriptions; use App\Mail\Subscription\User\ValidationEmail; -use App\Models\UserValidation; use App\User; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; diff --git a/app/Mail/Subscription/User/ValidationEmail.php b/app/Mail/Subscription/User/ValidationEmail.php index cffa1f9677..3de9ab7f08 100644 --- a/app/Mail/Subscription/User/ValidationEmail.php +++ b/app/Mail/Subscription/User/ValidationEmail.php @@ -2,7 +2,6 @@ namespace App\Mail\Subscription\User; -use App\Models\UserValidation; use App\User; use Illuminate\Bus\Queueable; use Illuminate\Mail\Mailable; diff --git a/app/Observers/CampaignObserver.php b/app/Observers/CampaignObserver.php index 2f15d9f5d2..95f36278be 100644 --- a/app/Observers/CampaignObserver.php +++ b/app/Observers/CampaignObserver.php @@ -53,7 +53,7 @@ public function saving(Campaign $campaign) $isPublic = request()->get('is_public', null); if (!empty($isPublic) && $previousVisibility == Campaign::VISIBILITY_PRIVATE) { $campaign->visibility_id = Campaign::VISIBILITY_PUBLIC; - // Default to public for now. Later will have REVIEW mode. + // Default to public for now. Later will have REVIEW mode. } elseif (empty($isPublic) && $previousVisibility != Campaign::VISIBILITY_PRIVATE) { $campaign->visibility_id = Campaign::VISIBILITY_PRIVATE; } diff --git a/app/Renderers/DatagridRenderer2.php b/app/Renderers/DatagridRenderer2.php index d475ea333a..3e9ccc320c 100644 --- a/app/Renderers/DatagridRenderer2.php +++ b/app/Renderers/DatagridRenderer2.php @@ -158,7 +158,7 @@ public function bulks(): array } continue; } - // More specific use cases? + // More specific use cases? } elseif ($bulk === Layout::ACTION_DELETE) { if (auth()->check() && auth()->user()->isAdmin()) { $this->bulks[] = $bulk; diff --git a/app/User.php b/app/User.php index 176a7a239d..20da4ec5f9 100644 --- a/app/User.php +++ b/app/User.php @@ -441,7 +441,7 @@ public function isFrauding(): bool if ($validation) { return false; } - + // If the account was created recently, add some small checks /*if ($this->created_at->isAfter(Carbon::now()->subHour())) { // User's name is directly in the campaign name diff --git a/database/migrations/2024_01_22_203110_create_user_validations_table.php b/database/migrations/2024_01_22_203110_create_user_validations_table.php index 0d09e9d31a..b558a10a04 100644 --- a/database/migrations/2024_01_22_203110_create_user_validations_table.php +++ b/database/migrations/2024_01_22_203110_create_user_validations_table.php @@ -4,8 +4,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class () extends Migration { /** * Run the migrations. */ From 14ef8766b091dc11c4788b553ee1eb7e1c093a8d Mon Sep 17 00:00:00 2001 From: Spitfire Date: Thu, 1 Feb 2024 17:10:40 -0600 Subject: [PATCH 3/9] Implemented requested changes. --- .../User/EmailValidationController.php | 4 ++ .../Subscriptions/EmailValidationJob.php | 9 ++-- .../Subscription/User/ValidationEmail.php | 11 ++--- app/Models/Relations/UserRelations.php | 6 +++ app/Models/Scopes/UserScope.php | 7 +++ app/Services/Users/EmailValidationService.php | 43 +++++++++++++++++++ app/User.php | 34 +-------------- ...2_203110_create_user_validations_table.php | 2 +- .../validation/user-html.blade.php | 6 +-- .../settings/subscription/change.blade.php | 6 ++- 10 files changed, 77 insertions(+), 51 deletions(-) create mode 100644 app/Services/Users/EmailValidationService.php diff --git a/app/Http/Controllers/User/EmailValidationController.php b/app/Http/Controllers/User/EmailValidationController.php index f2c6a9ce0f..9e4dc50fbc 100644 --- a/app/Http/Controllers/User/EmailValidationController.php +++ b/app/Http/Controllers/User/EmailValidationController.php @@ -11,6 +11,10 @@ class EmailValidationController extends Controller { public function validateEmail(Request $request, User $user) { + if (!(auth()->check() && auth()->user()->id == $user->id)) { + return response()->redirectTo(route('settings.subscription'))->withError(__('emails/validation.error')); + } + $token = $request->get('token'); /** @var UserValidation $validation */ diff --git a/app/Jobs/Emails/Subscriptions/EmailValidationJob.php b/app/Jobs/Emails/Subscriptions/EmailValidationJob.php index e9889ae710..8ac4cf6b32 100644 --- a/app/Jobs/Emails/Subscriptions/EmailValidationJob.php +++ b/app/Jobs/Emails/Subscriptions/EmailValidationJob.php @@ -18,9 +18,8 @@ class EmailValidationJob implements ShouldQueue use Queueable; use SerializesModels; - /** @var int user id */ - protected $user; - protected $token; + protected int $user; + protected string $token; /** */ @@ -41,12 +40,12 @@ public function handle() if (empty($user)) { return; } - + $url = route('validation.email', ['user' => $user, 'token' => $this->token]); // Send an email to the user Mail::to($user->email) ->locale($user->locale) ->send( - new ValidationEmail($user, $this->token) + new ValidationEmail($user, $url) ); } } diff --git a/app/Mail/Subscription/User/ValidationEmail.php b/app/Mail/Subscription/User/ValidationEmail.php index 3de9ab7f08..c29a958c90 100644 --- a/app/Mail/Subscription/User/ValidationEmail.php +++ b/app/Mail/Subscription/User/ValidationEmail.php @@ -12,11 +12,8 @@ class ValidationEmail extends Mailable use Queueable; use SerializesModels; - /** - * @var User - */ - public $user; - public $token; + public User $user; + public string $url; public $date; @@ -26,10 +23,10 @@ class ValidationEmail extends Mailable * * @return void */ - public function __construct(User $user, string $token) + public function __construct(User $user, string $url) { $this->user = $user; - $this->token = $token; + $this->url = $url; } /** diff --git a/app/Models/Relations/UserRelations.php b/app/Models/Relations/UserRelations.php index f7bd50e259..22ae7fac6f 100644 --- a/app/Models/Relations/UserRelations.php +++ b/app/Models/Relations/UserRelations.php @@ -18,6 +18,7 @@ use App\Models\UserApp; use App\Models\UserFlag; use App\Models\Users\Tutorial; +use App\Models\UserValidation; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -199,4 +200,9 @@ public function upvotes(): HasMany { return $this->hasMany(FeatureVote::class); } + + public function userValidation(): HasMany + { + return $this->hasOne(UserValidation::class, 'user_id', 'id'); + } } diff --git a/app/Models/Scopes/UserScope.php b/app/Models/Scopes/UserScope.php index db2c38a2a0..5b1d2763d2 100644 --- a/app/Models/Scopes/UserScope.php +++ b/app/Models/Scopes/UserScope.php @@ -1,6 +1,7 @@ where(['is_valid' => $valid]); + } } diff --git a/app/Services/Users/EmailValidationService.php b/app/Services/Users/EmailValidationService.php new file mode 100644 index 0000000000..b97963f84d --- /dev/null +++ b/app/Services/Users/EmailValidationService.php @@ -0,0 +1,43 @@ +user->id)->first(); + if ($token && $token->is_valid) { + return; + } + //Check for existing token + $flag = UserFlag::where('user_id', $this->user->id)->where('flag', UserFlag::FLAG_EMAIL)->first(); + + if (!$flag) { + $flag = new UserFlag(); + $flag->user_id = $this->user->id; + $flag->flag = UserFlag::FLAG_EMAIL; + $flag->save(); + } + + if (!$token) { + $token = new UserValidation(); + $token->token = Str::uuid(); + $token->user_id = $this->user->id; + $token->is_valid = false; + $token->save(); + } + + EmailValidationJob::dispatch($this->user, $token->token); + + return; + } +} diff --git a/app/User.php b/app/User.php index 20da4ec5f9..149b446c04 100644 --- a/app/User.php +++ b/app/User.php @@ -17,11 +17,8 @@ use App\Models\Scopes\UserScope; use App\Models\UserLog; use App\Models\UserSetting; -use App\Models\UserFlag; -use App\Models\UserValidation; use App\Models\Relations\UserRelations; use Carbon\Carbon; -use App\Jobs\Emails\Subscriptions\EmailValidationJob; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Notifications\Notifiable; use Illuminate\Support\Collection; @@ -437,7 +434,7 @@ public function isFrauding(): bool return false; } - $validation = UserValidation::where('user_id', $this->id)->where('is_valid', true)->first(); + $validation = $this->userValidation->valid()->first(); if ($validation) { return false; } @@ -462,35 +459,6 @@ public function isFrauding(): bool ->count() >= 2; } - public function requiresEmail(): self - { - $token = UserValidation::where('user_id', $this->id)->first(); - if ($token && $token->is_valid) { - return $this; - } - //Check for existing token - $flag = UserFlag::where('user_id', $this->id)->where('flag', UserFlag::FLAG_EMAIL)->first(); - - if (!$flag) { - $flag = new UserFlag(); - $flag->user_id = $this->id; - $flag->flag = UserFlag::FLAG_EMAIL; - $flag->save(); - } - - if (!$token) { - $token = new UserValidation(); - $token->token = Str::uuid(); - $token->user_id = $this->id; - $token->is_valid = false; - $token->save(); - } - - EmailValidationJob::dispatch($this, $token->token); - - return $this; - } - /** * List of campaigns the user is the only admin of. This is used for the automatic purge warning emails */ diff --git a/database/migrations/2024_01_22_203110_create_user_validations_table.php b/database/migrations/2024_01_22_203110_create_user_validations_table.php index b558a10a04..8e9d5d04a1 100644 --- a/database/migrations/2024_01_22_203110_create_user_validations_table.php +++ b/database/migrations/2024_01_22_203110_create_user_validations_table.php @@ -14,7 +14,7 @@ public function up(): void $table->id(); $table->uuid('token'); $table->unsignedInteger('user_id'); - $table->boolean('is_valid'); + $table->boolean('is_valid')->default(false); $table->timestamps(); $table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete(); diff --git a/resources/views/emails/subscriptions/validation/user-html.blade.php b/resources/views/emails/subscriptions/validation/user-html.blade.php index c6e49893ff..3ff9514665 100644 --- a/resources/views/emails/subscriptions/validation/user-html.blade.php +++ b/resources/views/emails/subscriptions/validation/user-html.blade.php @@ -1,5 +1,5 @@ @extends('emails.base', [ - 'utmSource' => 'subscription', + 'utmSource' => 'validation', 'utmCampaign' => 'failed-charge' ]) @@ -12,8 +12,8 @@

    This is an automatic notification.

    -

    To validate the email for your Kanka account click here. This link will expire in 24 hours.

    -

    If the above link doesnt work, open the following URL in your web browser {{ 'https://app.kanka.io/users/' . $user->id . '/validation?token=' . $token }}

    +

    To validate the email for your Kanka account click here. This link will expire in 24 hours.

    +

    If the above link doesnt work, open the following URL in your web browser {{ $url }}

    {{ __('emails/subscriptions/upcoming.closing') }}
    The Kanka Team diff --git a/resources/views/settings/subscription/change.blade.php b/resources/views/settings/subscription/change.blade.php index d8aba330d3..ca127a6ac8 100644 --- a/resources/views/settings/subscription/change.blade.php +++ b/resources/views/settings/subscription/change.blade.php @@ -6,8 +6,10 @@ @if (!$user->isFrauding()) + @inject('emailService', 'App\Services\Users\EmailValidationService') @php - $user->requiresEmail(); + /** @var \App\Services\Users\EmailValidationService $emailService */ + $emailService->user($user)->requiresEmail(); @endphp {{ __('emails/validation.modal') }} @@ -79,7 +81,7 @@

  • - Giropay + giropay
  • @endif From c8cf5c629b475aac490996dd80ffff815ef77f61 Mon Sep 17 00:00:00 2001 From: spitfire305 Date: Thu, 1 Feb 2024 23:11:42 +0000 Subject: [PATCH 4/9] Fix styling --- app/Models/Scopes/UserScope.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Models/Scopes/UserScope.php b/app/Models/Scopes/UserScope.php index 5b1d2763d2..1b3c06b681 100644 --- a/app/Models/Scopes/UserScope.php +++ b/app/Models/Scopes/UserScope.php @@ -1,6 +1,7 @@ Date: Tue, 6 Feb 2024 08:58:10 -0600 Subject: [PATCH 5/9] Implemented requested changes --- .../Controllers/User/EmailValidationController.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/User/EmailValidationController.php b/app/Http/Controllers/User/EmailValidationController.php index 9e4dc50fbc..65b80ef965 100644 --- a/app/Http/Controllers/User/EmailValidationController.php +++ b/app/Http/Controllers/User/EmailValidationController.php @@ -9,9 +9,20 @@ class EmailValidationController extends Controller { + + /** + * Create a new controller instance. + * + * @return void + */ + public function __construct() + { + $this->middleware('auth'); + } + public function validateEmail(Request $request, User $user) { - if (!(auth()->check() && auth()->user()->id == $user->id)) { + if (auth()->user()->id != $user->id) { return response()->redirectTo(route('settings.subscription'))->withError(__('emails/validation.error')); } From 5cdcf0fd6b512fe4e7e4a6f2204faceaced1a7d5 Mon Sep 17 00:00:00 2001 From: Spitfire Date: Wed, 7 Feb 2024 10:56:52 -0600 Subject: [PATCH 6/9] Implemented requested changes --- .../Controllers/Settings/SubscriptionController.php | 8 +++++++- app/Models/Relations/UserRelations.php | 3 ++- app/Models/Scopes/UserScope.php | 9 +-------- app/Models/UserValidation.php | 12 ++++++++++++ app/User.php | 4 ++-- lang/en/emails/subscriptions/validation.php | 5 +++++ .../views/settings/subscription/change.blade.php | 5 ----- 7 files changed, 29 insertions(+), 17 deletions(-) create mode 100644 lang/en/emails/subscriptions/validation.php diff --git a/app/Http/Controllers/Settings/SubscriptionController.php b/app/Http/Controllers/Settings/SubscriptionController.php index bd94b5c7cf..b97236a831 100644 --- a/app/Http/Controllers/Settings/SubscriptionController.php +++ b/app/Http/Controllers/Settings/SubscriptionController.php @@ -10,6 +10,7 @@ use App\Models\Tier; use App\Services\SubscriptionService; use App\Services\SubscriptionUpgradeService; +use App\Services\Users\EmailValidationService; use App\User; use Exception; use Illuminate\Http\Request; @@ -22,14 +23,17 @@ class SubscriptionController extends Controller protected SubscriptionUpgradeService $subscriptionUpgrade; + protected EmailValidationService $emailValidation; + /** * SubscriptionController constructor. */ - public function __construct(SubscriptionService $service, SubscriptionUpgradeService $subscriptionUpgradeService) + public function __construct(SubscriptionService $service, SubscriptionUpgradeService $subscriptionUpgradeService, EmailValidationService $validationService) { $this->middleware(['auth', 'identity', 'subscriptions']); $this->subscription = $service; $this->subscriptionUpgrade = $subscriptionUpgradeService; + $this->emailValidation = $validationService; } public function index() @@ -93,6 +97,8 @@ public function change(Request $request, Tier $tier) } $upgrade = $this->subscriptionUpgrade->user($user)->tier($tier)->upgradePrice($period); $currency = $user->currencySymbol(); + + $this->emailValidation->user($user)->requiresEmail(); return view('settings.subscription.change', compact( 'tier', diff --git a/app/Models/Relations/UserRelations.php b/app/Models/Relations/UserRelations.php index 22ae7fac6f..64c130a269 100644 --- a/app/Models/Relations/UserRelations.php +++ b/app/Models/Relations/UserRelations.php @@ -21,6 +21,7 @@ use App\Models\UserValidation; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasOne; /** * Trait UserRelations @@ -201,7 +202,7 @@ public function upvotes(): HasMany return $this->hasMany(FeatureVote::class); } - public function userValidation(): HasMany + public function userValidation(): HasOne { return $this->hasOne(UserValidation::class, 'user_id', 'id'); } diff --git a/app/Models/Scopes/UserScope.php b/app/Models/Scopes/UserScope.php index 1b3c06b681..a7d8e4aed1 100644 --- a/app/Models/Scopes/UserScope.php +++ b/app/Models/Scopes/UserScope.php @@ -2,18 +2,11 @@ namespace App\Models\Scopes; -use Illuminate\Database\Eloquent\Builder; - /** * Trait UserScope * @package App\Models\Scopes */ trait UserScope { - /** - */ - public function scopeValid(Builder $query, bool $valid = true): Builder - { - return $query->where(['is_valid' => $valid]); - } + } diff --git a/app/Models/UserValidation.php b/app/Models/UserValidation.php index 2d74232793..23ba6e58be 100644 --- a/app/Models/UserValidation.php +++ b/app/Models/UserValidation.php @@ -5,10 +5,12 @@ use App\User; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Prunable; +use Illuminate\Database\Eloquent\Builder; /** * @property int $user_id * @property bool $is_valid + * @property User $user */ class UserValidation extends Model { @@ -20,6 +22,9 @@ class UserValidation extends Model 'token' ]; + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ public function user() { return $this->belongsTo(User::class); @@ -33,4 +38,11 @@ public function prunable() { return static::where('is_valid', false)->where('created_at', '<=', now()->subDays(1)); } + + /** + */ + public function scopeValid(Builder $query, bool $valid = true): Builder + { + return $query->where(['is_valid' => $valid]); + } } diff --git a/app/User.php b/app/User.php index 149b446c04..af764ad523 100644 --- a/app/User.php +++ b/app/User.php @@ -426,7 +426,7 @@ public function hasUnread(): bool public function isFrauding(): bool { // Fraud detection can be turned on or off - if (!config('subscription.fraud_detection')) { + if (config('subscription.fraud_detection')) { return false; } // Someone with a provider (twitter, fb) login is always considered safe @@ -434,7 +434,7 @@ public function isFrauding(): bool return false; } - $validation = $this->userValidation->valid()->first(); + $validation = $this->userValidation()->valid()->first(); if ($validation) { return false; } diff --git a/lang/en/emails/subscriptions/validation.php b/lang/en/emails/subscriptions/validation.php new file mode 100644 index 0000000000..979f8ae0ec --- /dev/null +++ b/lang/en/emails/subscriptions/validation.php @@ -0,0 +1,5 @@ + 'Kanka account email validation.', +]; diff --git a/resources/views/settings/subscription/change.blade.php b/resources/views/settings/subscription/change.blade.php index ca127a6ac8..1b0eb20871 100644 --- a/resources/views/settings/subscription/change.blade.php +++ b/resources/views/settings/subscription/change.blade.php @@ -6,11 +6,6 @@ @if (!$user->isFrauding()) - @inject('emailService', 'App\Services\Users\EmailValidationService') - @php - /** @var \App\Services\Users\EmailValidationService $emailService */ - $emailService->user($user)->requiresEmail(); - @endphp {{ __('emails/validation.modal') }} From 7c156badfaa7717304b4d0210fa65fc8afd88d89 Mon Sep 17 00:00:00 2001 From: Spitfire Date: Thu, 8 Feb 2024 13:18:51 -0600 Subject: [PATCH 7/9] Implemented requested changes. --- app/Http/Controllers/Settings/SubscriptionController.php | 7 +++++-- app/User.php | 2 +- resources/views/settings/subscription/change.blade.php | 5 ++++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/Settings/SubscriptionController.php b/app/Http/Controllers/Settings/SubscriptionController.php index b97236a831..11c5092135 100644 --- a/app/Http/Controllers/Settings/SubscriptionController.php +++ b/app/Http/Controllers/Settings/SubscriptionController.php @@ -97,8 +97,10 @@ public function change(Request $request, Tier $tier) } $upgrade = $this->subscriptionUpgrade->user($user)->tier($tier)->upgradePrice($period); $currency = $user->currencySymbol(); - - $this->emailValidation->user($user)->requiresEmail(); + $validationService = false; + if ($user->isFrauding()) { + $validationService = $this->emailValidation; + } return view('settings.subscription.change', compact( 'tier', @@ -114,6 +116,7 @@ public function change(Request $request, Tier $tier) 'hasPromo', 'limited', 'isYearly', + 'validationService', )); } diff --git a/app/User.php b/app/User.php index af764ad523..c7284968d0 100644 --- a/app/User.php +++ b/app/User.php @@ -426,7 +426,7 @@ public function hasUnread(): bool public function isFrauding(): bool { // Fraud detection can be turned on or off - if (config('subscription.fraud_detection')) { + if (!config('subscription.fraud_detection')) { return false; } // Someone with a provider (twitter, fb) login is always considered safe diff --git a/resources/views/settings/subscription/change.blade.php b/resources/views/settings/subscription/change.blade.php index 1b0eb20871..b9df844529 100644 --- a/resources/views/settings/subscription/change.blade.php +++ b/resources/views/settings/subscription/change.blade.php @@ -5,7 +5,10 @@
    - @if (!$user->isFrauding()) + @if ($validationService) + @php + $validationService->user($user)->requiresEmail(); + @endphp {{ __('emails/validation.modal') }} From 7b874073d7fd4a466db38db048107b7b3de38c5e Mon Sep 17 00:00:00 2001 From: Spitfire Date: Mon, 12 Feb 2024 20:04:59 -0600 Subject: [PATCH 8/9] Corrected bad changes --- app/Http/Controllers/Settings/SubscriptionController.php | 4 ++-- resources/views/settings/subscription/change.blade.php | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/app/Http/Controllers/Settings/SubscriptionController.php b/app/Http/Controllers/Settings/SubscriptionController.php index 11c5092135..e5e83ed97f 100644 --- a/app/Http/Controllers/Settings/SubscriptionController.php +++ b/app/Http/Controllers/Settings/SubscriptionController.php @@ -97,9 +97,9 @@ public function change(Request $request, Tier $tier) } $upgrade = $this->subscriptionUpgrade->user($user)->tier($tier)->upgradePrice($period); $currency = $user->currencySymbol(); - $validationService = false; + if ($user->isFrauding()) { - $validationService = $this->emailValidation; + $this->emailValidation->user($user)->requiresEmail(); } return view('settings.subscription.change', compact( diff --git a/resources/views/settings/subscription/change.blade.php b/resources/views/settings/subscription/change.blade.php index b9df844529..02fe7fea1c 100644 --- a/resources/views/settings/subscription/change.blade.php +++ b/resources/views/settings/subscription/change.blade.php @@ -5,10 +5,7 @@
    - @if ($validationService) - @php - $validationService->user($user)->requiresEmail(); - @endphp + @if ($user->isFrauding()) {{ __('emails/validation.modal') }} From e9e0d4fdeb41a1ebc8c2a14f12f9877fbb070235 Mon Sep 17 00:00:00 2001 From: ilestis Date: Mon, 19 Feb 2024 09:42:31 -0600 Subject: [PATCH 9/9] Change logic, bind token to routing to avoid exposing user ids --- .../Settings/SubscriptionController.php | 1 - .../User/EmailValidationController.php | 19 +++-------- .../Subscriptions/EmailValidationJob.php | 12 ++++--- app/Models/Relations/UserRelations.php | 2 +- app/Models/UserValidation.php | 9 +++++ app/Providers/RouteServiceProvider.php | 2 ++ app/Services/Users/EmailValidationService.php | 34 +++++++++---------- app/User.php | 2 ++ lang/en/emails/validation.php | 4 +-- .../views/components/dialog/close.blade.php | 7 +--- .../settings/subscription/change.blade.php | 2 +- .../settings/subscription/index.blade.php | 2 +- routes/web.php | 2 +- 13 files changed, 48 insertions(+), 50 deletions(-) diff --git a/app/Http/Controllers/Settings/SubscriptionController.php b/app/Http/Controllers/Settings/SubscriptionController.php index e5e83ed97f..e4b80c2f89 100644 --- a/app/Http/Controllers/Settings/SubscriptionController.php +++ b/app/Http/Controllers/Settings/SubscriptionController.php @@ -116,7 +116,6 @@ public function change(Request $request, Tier $tier) 'hasPromo', 'limited', 'isYearly', - 'validationService', )); } diff --git a/app/Http/Controllers/User/EmailValidationController.php b/app/Http/Controllers/User/EmailValidationController.php index 65b80ef965..dafbd0f383 100644 --- a/app/Http/Controllers/User/EmailValidationController.php +++ b/app/Http/Controllers/User/EmailValidationController.php @@ -20,25 +20,14 @@ public function __construct() $this->middleware('auth'); } - public function validateEmail(Request $request, User $user) + public function validateEmail(Request $request, UserValidation $userValidation) { - if (auth()->user()->id != $user->id) { + if (auth()->user()->id != $userValidation->user_id) { return response()->redirectTo(route('settings.subscription'))->withError(__('emails/validation.error')); } - $token = $request->get('token'); - - /** @var UserValidation $validation */ - $validation = UserValidation::where('user_id', $user->id) - ->where('token', $token) - ->first(); - - if ($validation->exists) { - $validation->is_valid = true; - $validation->saveQuietly(); - } else { - response()->redirectTo(route('settings.subscription'))->withError(__('emails/validation.error')); - } + $userValidation->is_valid = true; + $userValidation->saveQuietly(); return response()->redirectTo(route('settings.subscription'))->withSuccess(__('emails/validation.success')); } diff --git a/app/Jobs/Emails/Subscriptions/EmailValidationJob.php b/app/Jobs/Emails/Subscriptions/EmailValidationJob.php index 8ac4cf6b32..636f4bfd5c 100644 --- a/app/Jobs/Emails/Subscriptions/EmailValidationJob.php +++ b/app/Jobs/Emails/Subscriptions/EmailValidationJob.php @@ -3,6 +3,7 @@ namespace App\Jobs\Emails\Subscriptions; use App\Mail\Subscription\User\ValidationEmail; +use App\Models\UserValidation; use App\User; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; @@ -23,10 +24,10 @@ class EmailValidationJob implements ShouldQueue /** */ - public function __construct(User $user, string $token) + public function __construct(User $user, UserValidation $token) { $this->user = $user->id; - $this->token = $token; + $this->token = $token->id; } /** @@ -34,14 +35,15 @@ public function __construct(User $user, string $token) */ public function handle() { - // User deleted their account already? Sure thing + // Small check in case the user deleted their account before the queue could get to them /** @var User|null $user */ $user = User::find($this->user); if (empty($user)) { return; } - $url = route('validation.email', ['user' => $user, 'token' => $this->token]); - // Send an email to the user + $userValidation = UserValidation::find($this->token); + $url = route('validation.email', ['userValidation' => $userValidation]); + Mail::to($user->email) ->locale($user->locale) ->send( diff --git a/app/Models/Relations/UserRelations.php b/app/Models/Relations/UserRelations.php index 64c130a269..7aeddfc4dd 100644 --- a/app/Models/Relations/UserRelations.php +++ b/app/Models/Relations/UserRelations.php @@ -202,7 +202,7 @@ public function upvotes(): HasMany return $this->hasMany(FeatureVote::class); } - public function userValidation(): HasOne + public function userValidation(): HasOne|UserValidation { return $this->hasOne(UserValidation::class, 'user_id', 'id'); } diff --git a/app/Models/UserValidation.php b/app/Models/UserValidation.php index 23ba6e58be..dc04a7740b 100644 --- a/app/Models/UserValidation.php +++ b/app/Models/UserValidation.php @@ -8,9 +8,13 @@ use Illuminate\Database\Eloquent\Builder; /** + * @property int $id * @property int $user_id + * @property string $token * @property bool $is_valid * @property User $user + * + * @method static self|Builder valid() */ class UserValidation extends Model { @@ -22,6 +26,11 @@ class UserValidation extends Model 'token' ]; + public function getRouteKeyName() + { + return 'token'; + } + /** * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 9c0c497465..0a42098e29 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -10,6 +10,7 @@ use App\Models\EntityType; use App\Models\Plugin; use App\Models\Tier; +use App\Models\UserValidation; use Illuminate\Support\Facades\Route; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; @@ -42,6 +43,7 @@ public function boot(): void return Campaign::acl($value)->firstOrFail(); }); Route::model('entityType', EntityType::class); + Route::model('userValidation', UserValidation::class); } /** diff --git a/app/Services/Users/EmailValidationService.php b/app/Services/Users/EmailValidationService.php index b97963f84d..4e0e2106bb 100644 --- a/app/Services/Users/EmailValidationService.php +++ b/app/Services/Users/EmailValidationService.php @@ -18,26 +18,26 @@ public function requiresEmail(): void if ($token && $token->is_valid) { return; } - //Check for existing token - $flag = UserFlag::where('user_id', $this->user->id)->where('flag', UserFlag::FLAG_EMAIL)->first(); - - if (!$flag) { - $flag = new UserFlag(); - $flag->user_id = $this->user->id; - $flag->flag = UserFlag::FLAG_EMAIL; - $flag->save(); - } - if (!$token) { - $token = new UserValidation(); - $token->token = Str::uuid(); - $token->user_id = $this->user->id; - $token->is_valid = false; - $token->save(); + $flag = UserFlag::where('user_id', $this->user->id) + ->where('flag', UserFlag::FLAG_EMAIL) + ->first(); + // If we've already notified the user, no need to notify them again + if ($flag) { + return; } - EmailValidationJob::dispatch($this->user, $token->token); + $flag = new UserFlag(); + $flag->user_id = $this->user->id; + $flag->flag = UserFlag::FLAG_EMAIL; + $flag->save(); + + $token = new UserValidation(); + $token->token = Str::uuid(); + $token->user_id = $this->user->id; + $token->is_valid = false; + $token->save(); - return; + EmailValidationJob::dispatch($this->user, $token); } } diff --git a/app/User.php b/app/User.php index c7284968d0..d00f9df2ab 100644 --- a/app/User.php +++ b/app/User.php @@ -439,6 +439,8 @@ public function isFrauding(): bool return false; } + return true; + // If the account was created recently, add some small checks /*if ($this->created_at->isAfter(Carbon::now()->subHour())) { // User's name is directly in the campaign name diff --git a/lang/en/emails/validation.php b/lang/en/emails/validation.php index 53a784175e..e93d2aa636 100644 --- a/lang/en/emails/validation.php +++ b/lang/en/emails/validation.php @@ -1,7 +1,7 @@ 'Email validation required for subscription, please check your inbox to continue the validation process.', - 'success' => 'Email succesfully validated.', + 'modal' => 'An email validation is required for subscriptions. Please check your inbox for a link to confirm your email before continuing the subscription process.', + 'success' => 'Email successfully validated.', 'error' => 'Validation failed, please try again.', ]; diff --git a/resources/views/components/dialog/close.blade.php b/resources/views/components/dialog/close.blade.php index 6d0143a196..d66fdb8d70 100644 --- a/resources/views/components/dialog/close.blade.php +++ b/resources/views/components/dialog/close.blade.php @@ -1,9 +1,4 @@ -@if ($modal || $dismiss == 'alert')@else - -@endif diff --git a/resources/views/settings/subscription/change.blade.php b/resources/views/settings/subscription/change.blade.php index 02fe7fea1c..c49b86533a 100644 --- a/resources/views/settings/subscription/change.blade.php +++ b/resources/views/settings/subscription/change.blade.php @@ -1,4 +1,4 @@ - + {{ __('settings.subscription.change.title') }} diff --git a/resources/views/settings/subscription/index.blade.php b/resources/views/settings/subscription/index.blade.php index 11ef420218..e8e50f36ae 100644 --- a/resources/views/settings/subscription/index.blade.php +++ b/resources/views/settings/subscription/index.blade.php @@ -160,7 +160,7 @@ - + @endsection diff --git a/routes/web.php b/routes/web.php index b1222b5cb9..a56e289dc4 100644 --- a/routes/web.php +++ b/routes/web.php @@ -35,4 +35,4 @@ Route::post('roadmap/{feature}/upvote', [App\Http\Controllers\Roadmap\FeatureController::class, 'upvote'])->name('roadmap.upvote'); Route::post('roadmap/submit', [App\Http\Controllers\Roadmap\FeatureController::class, 'store'])->name('roadmap.store'); -Route::get('/users/{user}/validation', [App\Http\Controllers\User\EmailValidationController::class, 'validateEmail'])->name('validation.email'); +Route::get('/validation/{userValidation}', [App\Http\Controllers\User\EmailValidationController::class, 'validateEmail'])->name('validation.email');