Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Email Validation. #807

Merged
merged 10 commits into from
Feb 19, 2024
10 changes: 9 additions & 1 deletion app/Http/Controllers/Settings/SubscriptionController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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()
Expand Down Expand Up @@ -94,6 +98,10 @@ public function change(Request $request, Tier $tier)
$upgrade = $this->subscriptionUpgrade->user($user)->tier($tier)->upgradePrice($period);
$currency = $user->currencySymbol();

if ($user->isFrauding()) {
$this->emailValidation->user($user)->requiresEmail();
}

return view('settings.subscription.change', compact(
'tier',
'period',
Expand Down
34 changes: 34 additions & 0 deletions app/Http/Controllers/User/EmailValidationController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace App\Http\Controllers\User;

use App\Http\Controllers\Controller;
use App\Models\UserValidation;
use App\User;
use Illuminate\Http\Request;

class EmailValidationController extends Controller
{

/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('auth');
}

public function validateEmail(Request $request, UserValidation $userValidation)
{
if (auth()->user()->id != $userValidation->user_id) {
return 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'));
}
}
53 changes: 53 additions & 0 deletions app/Jobs/Emails/Subscriptions/EmailValidationJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

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;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;

class EmailValidationJob implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;

protected int $user;
protected string $token;

/**
*/
public function __construct(User $user, UserValidation $token)
{
$this->user = $user->id;
$this->token = $token->id;

Check failure on line 30 in app/Jobs/Emails/Subscriptions/EmailValidationJob.php

View workflow job for this annotation

GitHub Actions / PHP 8.3

Property App\Jobs\Emails\Subscriptions\EmailValidationJob::$token (string) does not accept int.
}

/**
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function handle()
{
// 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;
}
$userValidation = UserValidation::find($this->token);
$url = route('validation.email', ['userValidation' => $userValidation]);

Mail::to($user->email)
->locale($user->locale)
->send(
new ValidationEmail($user, $url)
);
}
}
45 changes: 45 additions & 0 deletions app/Mail/Subscription/User/ValidationEmail.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace App\Mail\Subscription\User;

use App\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;

class ValidationEmail extends Mailable
{
use Queueable;
use SerializesModels;

public User $user;
public string $url;


public $date;

/**
* Create a new message instance.
*
* @return void
*/
public function __construct(User $user, string $url)
{
$this->user = $user;
$this->url = $url;
}

/**
* 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');
}
}
7 changes: 7 additions & 0 deletions app/Models/Relations/UserRelations.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@
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;
use Illuminate\Database\Eloquent\Relations\HasOne;

/**
* Trait UserRelations
Expand Down Expand Up @@ -199,4 +201,9 @@ public function upvotes(): HasMany
{
return $this->hasMany(FeatureVote::class);
}

public function userValidation(): HasOne|UserValidation
{
return $this->hasOne(UserValidation::class, 'user_id', 'id');
}
}
1 change: 1 addition & 0 deletions app/Models/Scopes/UserScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@
*/
trait UserScope
{

}
1 change: 1 addition & 0 deletions app/Models/UserFlag.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
57 changes: 57 additions & 0 deletions app/Models/UserValidation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

namespace App\Models;

use App\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Prunable;
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
{
use Prunable;

/** @var string[] */
protected $fillable = [
'is_valid',
'token'
];

public function getRouteKeyName()
{
return 'token';
}

/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
spitfire305 marked this conversation as resolved.
Show resolved Hide resolved
{
return $this->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));
}

/**
*/
public function scopeValid(Builder $query, bool $valid = true): Builder
{
return $query->where(['is_valid' => $valid]);
}
}
2 changes: 2 additions & 0 deletions app/Providers/RouteServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -42,6 +43,7 @@ public function boot(): void
return Campaign::acl($value)->firstOrFail();
});
Route::model('entityType', EntityType::class);
Route::model('userValidation', UserValidation::class);
}

/**
Expand Down
43 changes: 43 additions & 0 deletions app/Services/Users/EmailValidationService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace App\Services\Users;

use App\Traits\UserAware;
use App\Models\UserFlag;
use App\Models\UserValidation;
use App\Jobs\Emails\Subscriptions\EmailValidationJob;
use Illuminate\Support\Str;

class EmailValidationService
{
use UserAware;

public function requiresEmail(): void
{
$token = UserValidation::where('user_id', $this->user->id)->first();
if ($token && $token->is_valid) {
return;
}

$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;
}

$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();

EmailValidationJob::dispatch($this->user, $token);
}
}
8 changes: 8 additions & 0 deletions app/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,14 @@
if (!empty($this->provider)) {
return false;
}

$validation = $this->userValidation()->valid()->first();
if ($validation) {
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
Expand All @@ -447,7 +455,7 @@
}
}*/
// Recent fails are a clear indicator of someone cycling through cards
return $this->logs()

Check failure on line 458 in app/User.php

View workflow job for this annotation

GitHub Actions / PHP 8.3

Unreachable statement - code above always terminates.
->where('type_id', UserLog::TYPE_FAILED_CHARGE_EMAIL)
->whereDate('created_at', '>=', Carbon::now()->subHour()->toDateString())
->count() >= 2;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class () extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('user_validations', function (Blueprint $table) {
$table->id();
$table->uuid('token');
$table->unsignedInteger('user_id');
$table->boolean('is_valid')->default(false);
$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');
}
};
5 changes: 5 additions & 0 deletions lang/en/emails/subscriptions/validation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

return [
'title' => 'Kanka account email validation.',
];
7 changes: 7 additions & 0 deletions lang/en/emails/validation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

return [
'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.',
];
Loading
Loading