Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# https://github.com/jeromecoloma/sessionx
.sessionx.yaml

.DS_Store
.idea
.phpunit.result.cache
Expand All @@ -11,4 +14,4 @@ phpunit.xml
phpstan.neon
testbench.yaml
vendor
!docs
!docs
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,18 +60,23 @@ Optionally, you can publish the views using
php artisan vendor:publish --tag="mails-views"
```

Add the routes to the PanelProvider using the `routes()` method, like this:
Add the routes to the PanelProvider using the `authenticatedRoutes()` method, like this:

```php
use Backstage\Mails\Facades\Mails;

public function panel(Panel $panel): Panel
{
return $panel
->routes(fn () => Mails::routes());
->authenticatedRoutes(fn () => Mails::routes());
}
```

The preview and attachment routes also enforce panel authentication and the
`canManageMails()` check themselves, so they remain protected if they are
registered with `routes()`. `authenticatedRoutes()` is still the recommended
registration method.

Then add the plugin to your `PanelProvider`

```php
Expand Down Expand Up @@ -116,9 +121,13 @@ $panel

This example demonstrates how to combine role-based and permission-based access control, providing a more robust and flexible approach to managing access to mail resources.

The same `canManageMails()` check protects mail previews and attachment
downloads. Attachments can only be downloaded through the mail record they
belong to, and previews run in a sandboxed iframe.

### Tenant middleware and route protection

If you want to protect the mail routes with your (tenant) middleware, you can do so by adding the routes to the `tenantRoutes`:
If you want to protect the mail routes with your tenant middleware, add them to `authenticatedTenantRoutes()`:

```php
use Backstage\Mails\MailsPlugin;
Expand All @@ -128,7 +137,7 @@ public function panel(Panel $panel): Panel
{
return $panel
->plugin(MailsPlugin::make())
->tenantRoutes(fn() => Mails::routes());
->authenticatedTenantRoutes(fn () => Mails::routes());
}
```

Expand Down
5 changes: 4 additions & 1 deletion resources/views/mails/preview.blade.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<div class="w-full h-screen">
<iframe
src="{{ route('filament.' . Filament\Facades\Filament::getCurrentPanel()->getId() . '.mails.preview', ['tenant' => Filament\Facades\Filament::getTenant(), 'mail' => $mail->id]) }}"
srcdoc="{{ $html }}"
sandbox
referrerpolicy="no-referrer"
title="{{ __('Email preview') }}"
class="w-full h-full max-w-full" style="width: 100vw; height: 100vh; border: none;">
</iframe>
</div>
18 changes: 7 additions & 11 deletions src/Controllers/MailDownloadController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,20 @@

namespace Backstage\Mails\Controllers;

use Backstage\Mails\Laravel\Models\MailAttachment;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Config;
use Symfony\Component\HttpFoundation\StreamedResponse;

class MailDownloadController extends Controller
{
public function __invoke(...$arguments)
public function __invoke(Request $request): StreamedResponse
{
if (count($arguments) === 4) {
[$tenant, $mail, $attachment, $filename] = $arguments;
} else {
[$mail, $attachment, $filename] = $arguments;
$tenant = null;
}
$mailModel = Config::string('mails.models.mail');

$attachmentModel = Config::get('mails.models.attachment');
/** @var MailAttachment $attachment */
$attachment = $attachmentModel::find($attachment);
$mail = $mailModel::findOrFail($request->route('mail'));

$attachment = $mail->attachments()->findOrFail($request->route('attachment'));

return $attachment->downloadFileFromStorage();
}
Expand Down
18 changes: 13 additions & 5 deletions src/Controllers/MailPreviewController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,25 @@

namespace Backstage\Mails\Controllers;

use Backstage\Mails\Laravel\Models\Mail;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Config;
use Symfony\Component\HttpFoundation\Response;

class MailPreviewController extends Controller
{
public function __invoke(Request $request)
public function __invoke(Request $request): Response
{
/** @var Mail $mail */
$mail = Mail::find($request->mail);
$mailModel = Config::string('mails.models.mail');

return response($mail->html);
$mail = $mailModel::findOrFail($request->route('mail'));

return response($mail->html, 200, [
'Content-Type' => 'text/html; charset=UTF-8',
'Content-Security-Policy' => "sandbox; frame-ancestors 'self'",
'Referrer-Policy' => 'no-referrer',
'X-Content-Type-Options' => 'nosniff',
'X-Frame-Options' => 'SAMEORIGIN',
]);
}
}
23 changes: 23 additions & 0 deletions src/Http/Middleware/EnsureUserCanManageMails.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace Backstage\Mails\Http\Middleware;

use Backstage\Mails\MailsPlugin;
use Closure;
use Filament\Facades\Filament;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class EnsureUserCanManageMails
{
public function handle(Request $request, Closure $next): Response
{
$panel = Filament::getCurrentPanel();

abort_if($panel === null || ! $panel->hasPlugin('mails'), 403);

abort_unless(MailsPlugin::get()->userCanManageMails(), 403);

return $next($request);
}
}
14 changes: 12 additions & 2 deletions src/Mails.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,23 @@

use Backstage\Mails\Controllers\MailDownloadController;
use Backstage\Mails\Controllers\MailPreviewController;
use Backstage\Mails\Http\Middleware\EnsureUserCanManageMails;
use Filament\Http\Middleware\Authenticate;
use Illuminate\Support\Facades\Route;

class Mails
{
public static function routes(): void
{
Route::get('mails/{mail}/preview', MailPreviewController::class)->name('mails.preview');
Route::get('mails/{mail}/attachment/{attachment}/{filename}', MailDownloadController::class)->name('mails.attachment.download');
Route::middleware([
Authenticate::class,
EnsureUserCanManageMails::class,
])->group(function (): void {
Route::get('mails/{mail}/preview', MailPreviewController::class)
->name('mails.preview');

Route::get('mails/{mail}/attachment/{attachment}/{filename}', MailDownloadController::class)
->name('mails.attachment.download');
});
}
}
4 changes: 4 additions & 0 deletions tests/Fixtures/TestPanelProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Backstage\Mails\Tests\Fixtures;

use Backstage\Mails\Mails;
use Backstage\Mails\MailsPlugin;
use Filament\Panel;
use Filament\PanelProvider;
Expand All @@ -14,7 +15,10 @@ public function panel(Panel $panel): Panel
->default()
->id('admin')
->path('admin')
->login()
->authGuard('web')
->topNavigation()
->routes(fn () => Mails::routes())
->plugin(MailsPlugin::make());
}
}
23 changes: 23 additions & 0 deletions tests/Fixtures/User.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace Backstage\Mails\Tests\Fixtures;

use Filament\Models\Contracts\FilamentUser;
use Filament\Panel;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable implements FilamentUser
{
protected $table = 'users';

protected $fillable = [
'name',
'email',
'password',
];

public function canAccessPanel(Panel $panel): bool
{
return true;
}
}
Loading