diff --git a/app/Actions/Fortify/CreateNewUser.php b/app/Actions/Fortify/CreateNewUser.php index 77ae73fce..7950bd4f7 100644 --- a/app/Actions/Fortify/CreateNewUser.php +++ b/app/Actions/Fortify/CreateNewUser.php @@ -58,6 +58,11 @@ class CreateNewUser implements CreatesNewUsers 'password' => Hash::make($input['password']), ]); $team = $user->teams()->first(); + if (isCloud()) { + $user->sendVerificationEmail(); + } else { + $user->markEmailAsVerified(); + } } // Set session variable session(['currentTeam' => $user->currentTeam = $team]); diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index be63b9694..d8cba40b6 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -38,8 +38,7 @@ class Kernel extends HttpKernel \App\Http\Middleware\VerifyCsrfToken::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, \App\Http\Middleware\CheckForcePasswordReset::class, - \App\Http\Middleware\IsSubscriptionValid::class, - \App\Http\Middleware\IsBoardingFlow::class, + \App\Http\Middleware\DecideWhatToDoWithUser::class, ], diff --git a/app/Http/Livewire/Upgrade.php b/app/Http/Livewire/Upgrade.php index d5ff62a64..ca5f7df30 100644 --- a/app/Http/Livewire/Upgrade.php +++ b/app/Http/Livewire/Upgrade.php @@ -5,10 +5,11 @@ namespace App\Http\Livewire; use App\Actions\Server\UpdateCoolify; use App\Models\InstanceSettings; use Livewire\Component; -use Masmerise\Toaster\Toaster; +use DanHarrin\LivewireRateLimiting\WithRateLimiting; class Upgrade extends Component { + use WithRateLimiting; public bool $showProgress = false; public bool $isUpgradeAvailable = false; public string $latestVersion = ''; @@ -31,6 +32,7 @@ class Upgrade extends Component public function upgrade() { try { + $this->rateLimit(1, 30); if ($this->showProgress) { return; } diff --git a/app/Http/Livewire/VerifyEmail.php b/app/Http/Livewire/VerifyEmail.php new file mode 100644 index 000000000..e485102cb --- /dev/null +++ b/app/Http/Livewire/VerifyEmail.php @@ -0,0 +1,26 @@ +rateLimit(1, 300); + auth()->user()->sendVerificationEmail(); + $this->emit('success', 'Email verification link sent!'); + + } catch(\Exception $e) { + ray($e); + return handleError($e,$this); + } + } + public function render() + { + return view('livewire.verify-email'); + } +} diff --git a/app/Http/Middleware/DecideWhatToDoWithUser.php b/app/Http/Middleware/DecideWhatToDoWithUser.php new file mode 100644 index 000000000..a8db3a823 --- /dev/null +++ b/app/Http/Middleware/DecideWhatToDoWithUser.php @@ -0,0 +1,45 @@ +user() || !isCloud()) { + return $next($request); + } + if (!auth()->user()->hasVerifiedEmail()) { + if ($request->path() === 'verify' || in_array($request->path(), allowedPathsForInvalidAccounts()) || $request->routeIs('verify.verify')) { + return $next($request); + } + return redirect('/verify'); + } + if (!isSubscriptionActive() && !isSubscriptionOnGracePeriod()) { + if (!in_array($request->path(), allowedPathsForUnsubscribedAccounts())) { + if (Str::startsWith($request->path(), 'invitations')) { + return $next($request); + } + return redirect('subscription'); + } + } + if (showBoarding() && !in_array($request->path(), allowedPathsForBoardingAccounts())) { + if (Str::startsWith($request->path(), 'invitations')) { + return $next($request); + } + return redirect('boarding'); + } + if (auth()->user()->hasVerifiedEmail() && $request->path() === 'verify') { + return redirect('/'); + } + if (isSubscriptionActive() && $request->path() === 'subscription') { + return redirect('/'); + } + return $next($request); + } +} diff --git a/app/Http/Middleware/IsBoardingFlow.php b/app/Http/Middleware/NOTUSEDIsBoardingFlow.php similarity index 100% rename from app/Http/Middleware/IsBoardingFlow.php rename to app/Http/Middleware/NOTUSEDIsBoardingFlow.php diff --git a/app/Http/Middleware/IsSubscriptionValid.php b/app/Http/Middleware/NOTUSEDIsSubscriptionValid.php similarity index 100% rename from app/Http/Middleware/IsSubscriptionValid.php rename to app/Http/Middleware/NOTUSEDIsSubscriptionValid.php diff --git a/app/Models/Subscription.php b/app/Models/Subscription.php index d69d95981..23ed70f8f 100644 --- a/app/Models/Subscription.php +++ b/app/Models/Subscription.php @@ -33,7 +33,7 @@ class Subscription extends Model } if (isStripe()) { if (!$this->stripe_plan_id) { - return 'zero'; + return 'zero'; } $subscription = Subscription::where('id', $this->id)->first(); if (!$subscription) { diff --git a/app/Models/User.php b/app/Models/User.php index aba05acf3..51ac0aa14 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -6,8 +6,12 @@ use App\Notifications\Channels\SendsEmail; use App\Notifications\TransactionalEmails\ResetPassword as TransactionalEmailsResetPassword; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; +use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notifiable; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\Config; +use Illuminate\Support\Facades\URL; use Laravel\Fortify\TwoFactorAuthenticatable; use Laravel\Sanctum\HasApiTokens; @@ -54,6 +58,23 @@ class User extends Authenticatable implements SendsEmail return $this->email; } + public function sendVerificationEmail() + { + $mail = new MailMessage(); + $url = Url::temporarySignedRoute( + 'verify.verify', + Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)), + [ + 'id' => $this->getKey(), + 'hash' => sha1($this->getEmailForVerification()), + ] + ); + $mail->view('emails.email-verification', [ + 'url' => $url, + ]); + $mail->subject('Coolify Cloud: Verify your email.'); + send_user_an_email($mail, $this->email); + } public function sendPasswordResetNotification($token): void { $this->notify(new TransactionalEmailsResetPassword($token)); @@ -61,7 +82,7 @@ class User extends Authenticatable implements SendsEmail public function isAdmin() { - return data_get($this->pivot,'role') === 'admin' || data_get($this->pivot,'role') === 'owner'; + return data_get($this->pivot, 'role') === 'admin' || data_get($this->pivot, 'role') === 'owner'; } public function isAdminFromSession() @@ -79,7 +100,7 @@ class User extends Authenticatable implements SendsEmail return true; } $team = $teams->where('id', session('currentTeam')->id)->first(); - $role = data_get($team,'pivot.role'); + $role = data_get($team, 'pivot.role'); return $role === 'admin' || $role === 'owner'; } @@ -96,7 +117,7 @@ class User extends Authenticatable implements SendsEmail public function currentTeam() { - return Cache::remember('team:' . auth()->user()->id, 3600, function() { + return Cache::remember('team:' . auth()->user()->id, 3600, function () { return Team::find(session('currentTeam')->id); }); } diff --git a/bootstrap/helpers/subscriptions.php b/bootstrap/helpers/subscriptions.php index 9ea76ec9b..cd1f79fb4 100644 --- a/bootstrap/helpers/subscriptions.php +++ b/bootstrap/helpers/subscriptions.php @@ -122,14 +122,13 @@ function allowedPathsForUnsubscribedAccounts() return [ 'subscription', 'login', - 'register', + 'logout', 'waitlist', 'force-password-reset', - 'logout', 'livewire/message/force-password-reset', 'livewire/message/check-license', 'livewire/message/switch-team', - 'livewire/message/subscription.pricing-plans' + 'livewire/message/subscription.pricing-plans', ]; } function allowedPathsForBoardingAccounts() @@ -141,3 +140,10 @@ function allowedPathsForBoardingAccounts() 'livewire/message/activity-monitor' ]; } +function allowedPathsForInvalidAccounts() { + return [ + 'logout', + 'verify', + 'livewire/message/verify-email', + ]; +} diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 47ac188f2..d2e00f58f 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -9,12 +9,12 @@ @if ($is_registration_enabled) @if (config('coolify.waitlist')) + class="text-xs text-center text-white normal-case bg-transparent border-none rounded no-animation hover:no-underline btn btn-sm bg-coollabs-gradient"> Join the waitlist @else + class="text-xs text-center text-white normal-case bg-transparent border-none rounded no-animation hover:no-underline btn btn-sm bg-coollabs-gradient"> {{ __('auth.register_now') }} @endif diff --git a/resources/views/auth/verify-email.blade.php b/resources/views/auth/verify-email.blade.php new file mode 100644 index 000000000..dca6a4c6a --- /dev/null +++ b/resources/views/auth/verify-email.blade.php @@ -0,0 +1,12 @@ + +
+
+

Verification Email Sent

+
+
To activate your account, please open the email and follow the + instructions. +
+ +
+
+
diff --git a/resources/views/emails/email-verification.blade.php b/resources/views/emails/email-verification.blade.php new file mode 100644 index 000000000..ec09f024b --- /dev/null +++ b/resources/views/emails/email-verification.blade.php @@ -0,0 +1,3 @@ + + Verify your email [here]({{ $url }}). + diff --git a/resources/views/livewire/verify-email.blade.php b/resources/views/livewire/verify-email.blade.php new file mode 100644 index 000000000..51c48eb65 --- /dev/null +++ b/resources/views/livewire/verify-email.blade.php @@ -0,0 +1,3 @@ +
+ Send Verification Email Again +
diff --git a/routes/web.php b/routes/web.php index f2df41b74..89b37fdce 100644 --- a/routes/web.php +++ b/routes/web.php @@ -26,6 +26,7 @@ use App\Models\PrivateKey; use App\Models\Server; use App\Models\StandaloneDocker; use App\Models\SwarmDocker; +use Illuminate\Foundation\Auth\EmailVerificationRequest; use Illuminate\Http\Request; use Illuminate\Support\Facades\Password; use Illuminate\Support\Facades\Route; @@ -61,7 +62,19 @@ Route::post('/forgot-password', function (Request $request) { } return response()->json(['message' => 'Transactional emails are not active'], 400); })->name('password.forgot'); + + Route::get('/waitlist', WaitlistIndex::class)->name('waitlist.index'); + +Route::get('/verify', function () { + return view('auth.verify-email'); +})->middleware('auth')->name('verify.email'); + +Route::get('/email/verify/{id}/{hash}', function (EmailVerificationRequest $request) { + $request->fulfill(); + return redirect('/'); +})->middleware(['auth'])->name('verify.verify'); + Route::middleware(['throttle:login'])->group(function () { Route::get('/auth/link', [Controller::class, 'link'])->name('auth.link'); }); @@ -74,7 +87,7 @@ Route::prefix('magic')->middleware(['auth'])->group(function () { Route::get('/environment/new', [MagicController::class, 'newEnvironment']); }); -Route::middleware(['auth'])->group(function () { +Route::middleware(['auth', 'verified'])->group(function () { Route::get('/projects', [ProjectController::class, 'all'])->name('projects'); Route::get('/project/{project_uuid}/edit', [ProjectController::class, 'edit'])->name('project.edit'); Route::get('/project/{project_uuid}', [ProjectController::class, 'show'])->name('project.show'); @@ -114,7 +127,7 @@ Route::middleware(['auth'])->group(function () { }); -Route::middleware(['auth'])->group(function () { +Route::middleware(['auth', 'verified'])->group(function () { Route::get('/', Dashboard::class)->name('dashboard'); Route::get('/boarding', BoardingIndex::class)->name('boarding'); Route::middleware(['throttle:force-password-reset'])->group(function () {