Skip to content

Commit

Permalink
Add email settings
Browse files Browse the repository at this point in the history
  • Loading branch information
rawilk committed Nov 14, 2023
1 parent 28a57b4 commit 77d901d
Show file tree
Hide file tree
Showing 34 changed files with 1,236 additions and 13 deletions.
70 changes: 67 additions & 3 deletions config/profile-filament.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@
'delete_passkey' => \Rawilk\ProfileFilament\Actions\Passkeys\DeletePasskeyAction::class,
'register_passkey' => \Rawilk\ProfileFilament\Actions\Passkeys\RegisterPasskeyAction::class,
'upgrade_to_passkey' => \Rawilk\ProfileFilament\Actions\Passkeys\UpgradeToPasskeyAction::class,

// Pending user emails
'store_old_user_email' => \Rawilk\ProfileFilament\Actions\PendingUserEmails\StoreOldUserEmailAction::class,
'update_user_email' => \Rawilk\ProfileFilament\Actions\PendingUserEmails\UpdateUserEmailAction::class,
],

/*
Expand All @@ -65,6 +69,8 @@
'table_names' => [
'authenticator_app' => 'authenticator_apps',
'webauthn_key' => 'webauthn_keys',
'pending_user_email' => 'pending_user_emails',
'old_user_email' => 'old_user_emails',
],

/*
Expand All @@ -74,24 +80,55 @@
|
| Here you may override the models provided by this package.
|
| Note: Any custom models you define MUST extend the package's models.
|
*/
'models' => [
/**
* Authenticator App
*
* This model is responsible for storing a user's verified authenticator apps
* for 2fa. Your model must extend the AuthenticatorApp model.
* for 2fa.
*/
'authenticator_app' => \Rawilk\ProfileFilament\Models\AuthenticatorApp::class,

/**
* Webauthn Key
*
* This model is responsible for storing webauthn keys for a user, such
* as hardware security keys or passkeys. Your model must extend
* the WebauthnKey model.
* as hardware security keys or passkeys.
*/
'webauthn_key' => \Rawilk\ProfileFilament\Models\WebauthnKey::class,

/**
* Pending User Email
*
* This model is responsible for storing tokens for when a user wants to
* change their email address.
*/
'pending_user_email' => \Rawilk\ProfileFilament\Models\PendingUserEmail::class,

/**
* Old User Email
*
* This model is responsible for storing a user's old email addresses, which
* can be used to revert a change if it wasn't made by the user.
*/
'old_user_email' => \Rawilk\ProfileFilament\Models\OldUserEmail::class,
],

/*
|--------------------------------------------------------------------------
| Mailables
|--------------------------------------------------------------------------
|
| Here you may define which mailables are used for the notifications from
| this package.
|
*/
'mail' => [
'pending_email_verification' => \Rawilk\ProfileFilament\Mail\PendingEmailVerificationMail::class,
'pending_email_verified' => \Rawilk\ProfileFilament\Mail\PendingEmailVerifiedMail::class,
],

/*
Expand Down Expand Up @@ -126,6 +163,33 @@
'expires' => DateInterval::createFromDateString('2 hours'),
],

/*
|--------------------------------------------------------------------------
| Pending Email Changes
|--------------------------------------------------------------------------
|
| Here you may define some constraints for when a user changes their email
| address.
|
*/
'pending_email_changes' => [
/**
* The amount of time a revert change link should be valid for, in the case
* a user needs to revert their email change when it wasn't made by them.
*/
'revert_expiration' => DateInterval::createFromDateString('5 days'),

/**
* Log users in after they verify their new emails. (not recommended)
*/
'login_after_verification' => false,

/**
* Set the "remember" cookie after login from verification.
*/
'login_remember' => true,
],

/*
|--------------------------------------------------------------------------
| Webauthn
Expand Down
29 changes: 29 additions & 0 deletions database/migrations/create_pending_user_emails_table.php.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

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

return new class extends Migration
{
public function up(): void
{
Schema::create(config('profile-filament.table_names.pending_user_email'), function (Blueprint $table) {
$table->id();
$table->morphs('user');
$table->string('email')->index();
$table->string('token');
$table->dateTime('created_at')->nullable();
});

Schema::create(config('profile-filament.table_names.old_user_email'), function (Blueprint $table) {
$table->id();
$table->morphs('user');
$table->string('email')->index();
$table->string('token');
$table->dateTime('created_at')->nullable();
});
}
};
33 changes: 33 additions & 0 deletions resources/lang/en/mail.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

return [

'pending_email_verification' => [
'subject' => 'Verify your email address',
'greeting' => 'Hello,',
'line1' => 'A request has been made on your account to change your email address to :email. Please click the button below to verify your new email address.',
'button' => 'Verify new email address',
'line2' => 'Note: This link will expire in :minutes minutes.',
'line3' => 'If you did not update your email address, no further action is required.',
'salutation' => 'Thanks,<br>:app_name',
],

'email_updated' => [
'subject' => 'Email address updated',
'greeting' => 'Hello,',
'line1' => 'You are receiving this email because your :app_name account email address was recently updated.',
'line2' => 'From now on, you will need to use ":email" to sign into your account.',
'line3' => 'If this was you, no further action is required.',
'line4' => 'If you did not initiate this change, [click this link](:url) to revert the change. This link will expire in :days days.',
'salutation' => 'Thanks,<br>:app_name',
],

'request_details' => [
'heading' => '**Request details**',
'ip' => 'IP address: :ip',
'date' => 'Date: :date',
],

];
47 changes: 47 additions & 0 deletions resources/lang/en/pages/settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,51 @@

return [
'title' => 'Account',

'account_security_link' => 'Looking to manage account security settings? You can find them in the [Password and authentication](:url) page.',

'email' => [
'invalid_verification_link' => 'This verification link has already been consumed or is expired. Please request a new one to verify your email address.',
'email_already_taken' => 'The email address from your link has already been taken.',
'email_verified' => 'Your new email address has been verified and can now be used to sign-in.',
'invalid_revert_link' => 'This link has already been consumed or is expired. Please contact our support for further assistance.',
'email_reverted' => 'Your email address has been reverted back to what it was and can now be used to sign-in with.',

'heading' => 'Email address',
'change_pending_badge' => 'Change pending',
'email_description' => 'This email will be used for sign-in, account-related notifications and can also be used for password resets.',

'pending_heading' => 'Confirm your email',
'pending_description' => "We just need you to check your email **:email** and click the verification link we sent you to verify it's you and complete the update. Your change will not take effect until you've confirmed your new email.",

'actions' => [

'edit' => [
'trigger' => 'Change email',
'modal_title' => 'Edit email address',
'email_label' => 'New email address',
'email_placeholder' => 'example@:host',
'email_help' => 'We will send an email to this address to verify you have access to it. Your changes will not take effect until you verify the new email address.',
'success_title' => 'Success!',
'success_body' => 'Your email address has been updated.',
'success_body_pending' => 'Check your new email address for a verification link.',
],

'resend' => [
'trigger' => 'Resend email',
'success_title' => 'Success!',
'success_body' => 'A new verification link has been sent to your new email address.',

'throttled' => [
'title' => 'Too many requests',
'body' => 'Please try again in :minutes minutes.',
],
],

'cancel' => [
'trigger' => 'Undo email change',
],

],
],
];
73 changes: 73 additions & 0 deletions resources/views/livewire/emails/user-email.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<div>
<x-profile-filament::component-section>
<x-slot:title>
<span class="flex items-center gap-x-2" id="current-email-heading">
<span>{{ __('profile-filament::pages/settings.email.heading') }}</span>

@if ($pendingEmail)
<x-filament::badge color="warning">
{{ __('profile-filament::pages/settings.email.change_pending_badge') }}
</x-filament::badge>
@endif
</span>
</x-slot:title>

<div>
@if ($pendingEmail)
<div class="mb-4 px-4 py-3 rounded-md border border-gray-300 dark:border-gray-600">
<div class="flex gap-x-2 items-start">
<div class="shrink-0">
<x-filament::icon
alias="profile-filament::pending-email-info"
icon="heroicon-o-information-circle"
class="h-5 w-5 text-primary-500 dark:text-primary-400"
/>
</div>

<div class="flex-1">
<div class="text-sm font-bold">{{ __('profile-filament::pages/settings.email.pending_heading') }}</div>

<p class="mt-1 text-sm">
{{ \Rawilk\ProfileFilament\renderMarkdown(__('profile-filament::pages/settings.email.pending_description', ['email' => $pendingEmail->email])) }}
</p>

<div class="mt-3 flex items-center gap-x-2">
{{ $this->resendAction }}
<span class="inline-block rounded-full h-1 w-1 bg-gray-600" aria-hidden="true"></span>
{{ $this->cancelAction }}
</div>
</div>
</div>
</div>
@endif
</div>

<div class="px-4 py-3 rounded-md bg-gray-200 dark:bg-gray-600" aria-labelledby="current-email-heading" aria-describedby="current-email-description">
{{ $this->user->email }}
</div>

<p class="text-sm mt-2" id="current-email-description">{{ __('profile-filament::pages/settings.email.email_description') }}</p>

@unless ($pendingEmail)
<div class="mt-4">
{{ $this->editAction }}
</div>
@endunless

@if ($this->securityUrl)
<div class="mt-4 text-xs gap-x-1 flex items-center [&_a]:text-primary-600 dark:[&_a]:text-primary-400 [&_a:hover]:underline">
<div>
<x-filament::icon
alias="profile-filament::help"
icon="heroicon-o-question-mark-circle"
class="h-5 w-5"
/>
</div>

<span>{{ \Rawilk\ProfileFilament\renderMarkdown(__('profile-filament::pages/settings.account_security_link', ['url' => $this->securityUrl])) }}</span>
</div>
@endif

<x-filament-actions::modals />
</x-profile-filament::component-section>
</div>
15 changes: 15 additions & 0 deletions resources/views/mail/email-updated.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<x-mail::message>
# {{ __('profile-filament::mail.email_updated.greeting') }}

{{ __('profile-filament::mail.email_updated.line1', ['app_name' => config('app.name')]) }}

{{ \Rawilk\ProfileFilament\renderMarkdown(__('profile-filament::mail.email_updated.line2', ['email' => $maskedEmail])) }}

{{ \Rawilk\ProfileFilament\renderMarkdown(__('profile-filament::mail.email_updated.line3')) }}

{{ \Rawilk\ProfileFilament\renderMarkdown(__('profile-filament::mail.email_updated.line4', ['url' => $url, 'days' => $linkExpirationDays])) }}

{{ \Rawilk\ProfileFilament\renderMarkdown($requestDetails) }}

{{ \Rawilk\ProfileFilament\renderMarkdown(__('profile-filament::mail.email_updated.salutation', ['app_name' => config('app.name')])) }}
</x-mail::message>
15 changes: 15 additions & 0 deletions resources/views/mail/pending-email-verification.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<x-mail::message>
# {{ __('profile-filament::mail.pending_email_verification.greeting') }}

{{ __('profile-filament::mail.pending_email_verification.line1', ['email' => $email]) }}

<x-mail::button :url="$url">
{{ __('profile-filament::mail.pending_email_verification.button') }}
</x-mail::button>

{{ \Rawilk\ProfileFilament\renderMarkdown(__('profile-filament::mail.pending_email_verification.line2', ['minutes' => config('auth.verification.expire', 60)])) }}

{{ \Rawilk\ProfileFilament\renderMarkdown(__('profile-filament::mail.pending_email_verification.line3')) }}

{{ \Rawilk\ProfileFilament\renderMarkdown(__('profile-filament::mail.pending_email_verification.salutation', ['app_name' => config('app.name')])) }}
</x-mail::message>
6 changes: 5 additions & 1 deletion resources/views/pages/settings.blade.php
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
<x-profile-filament::layout>
Settings content here.
<div class="flex flex-col gap-y-6 lg:gap-y-10">
@foreach ($this->registeredComponents as $component)
@livewire($component)
@endforeach
</div>
</x-profile-filament::layout>
11 changes: 11 additions & 0 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
use Filament\Facades\Filament;
use Illuminate\Support\Facades\Route;
use Rawilk\ProfileFilament\Http\Controllers\PasskeysController;
use Rawilk\ProfileFilament\Http\Controllers\RevertEmailController;
use Rawilk\ProfileFilament\Http\Controllers\VerifyPendingEmailController;
use Rawilk\ProfileFilament\Http\Controllers\WebauthnPublicKeysController;

Route::name('filament.')
Expand Down Expand Up @@ -41,6 +43,15 @@
->name('auth.sudo-challenge')
->middleware($panel->getAuthMiddleware());
}

// Email verification
Route::get('/pending-emails/verify/{token}', VerifyPendingEmailController::class)
->name('pending_email.verify')
->middleware(['throttle:6,1', 'signed']);

Route::get('/pending-emails/revert/{token}', RevertEmailController::class)
->name('pending_email.revert')
->middleware(['throttle:6,1', 'signed']);
});
}
}
Expand Down
Loading

0 comments on commit 77d901d

Please sign in to comment.