diff --git a/app/Console/Commands/Install.php b/app/Console/Commands/Install.php index 8570367c..5b274307 100644 --- a/app/Console/Commands/Install.php +++ b/app/Console/Commands/Install.php @@ -57,6 +57,7 @@ protected function createUser() $user = User::create($this->getUserData()); $user->role = UserRole::Admin; + $user->email_verified_at = now(); $user->save(); $this->info('User created!'); diff --git a/app/Filament/Pages/Settings.php b/app/Filament/Pages/Settings.php index 1d991077..6bf09d8b 100644 --- a/app/Filament/Pages/Settings.php +++ b/app/Filament/Pages/Settings.php @@ -109,6 +109,10 @@ protected function getFormSchema(): array ->hidden(fn (Closure $get) => $get('select_board_when_creating_item') === false) ->columnSpan(2), + Toggle::make('users_must_verify_email') + ->label('Users must verify their email before they can submit items, or reply to items.') + ->columnSpan(2), + Grid::make()->schema([ Select::make('inbox_workflow') ->options(InboxWorkflow::getSelectOptions()) diff --git a/app/Filament/Pages/System.php b/app/Filament/Pages/System.php index 2985cc55..d0d31a43 100644 --- a/app/Filament/Pages/System.php +++ b/app/Filament/Pages/System.php @@ -2,8 +2,8 @@ namespace App\Filament\Pages; -use App\Filament\Pages\Widgets\System\SystemInfo; use Filament\Pages\Page; +use App\Filament\Pages\Widgets\System\SystemInfo; class System extends Page { diff --git a/app/Filament/Pages/Widgets/System/SystemInfo.php b/app/Filament/Pages/Widgets/System/SystemInfo.php index af85918d..e4a200af 100644 --- a/app/Filament/Pages/Widgets/System/SystemInfo.php +++ b/app/Filament/Pages/Widgets/System/SystemInfo.php @@ -2,8 +2,8 @@ namespace App\Filament\Pages\Widgets\System; -use App\Services\SystemChecker; use Filament\Widgets\Widget; +use App\Services\SystemChecker; class SystemInfo extends Widget { diff --git a/app/Filament/Resources/CommentResource/Pages/EditComment.php b/app/Filament/Resources/CommentResource/Pages/EditComment.php index cc847053..2c4461bb 100644 --- a/app/Filament/Resources/CommentResource/Pages/EditComment.php +++ b/app/Filament/Resources/CommentResource/Pages/EditComment.php @@ -2,10 +2,22 @@ namespace App\Filament\Resources\CommentResource\Pages; +use Filament\Pages\Actions\Action; use Filament\Resources\Pages\EditRecord; use App\Filament\Resources\CommentResource; class EditComment extends EditRecord { protected static string $resource = CommentResource::class; + + public function getActions(): array + { + return [ + Action::make('view_public') + ->color('secondary') + ->openUrlInNewTab() + ->url(fn () => route('items.show', $this->record->item) . '#comment-' . $this->record->id), + ...parent::getActions() + ]; + } } diff --git a/app/Http/Controllers/Auth/VerificationController.php b/app/Http/Controllers/Auth/VerificationController.php index 5e749af8..e2ccdcd2 100644 --- a/app/Http/Controllers/Auth/VerificationController.php +++ b/app/Http/Controllers/Auth/VerificationController.php @@ -8,35 +8,23 @@ class VerificationController extends Controller { - /* - |-------------------------------------------------------------------------- - | Email Verification Controller - |-------------------------------------------------------------------------- - | - | This controller is responsible for handling email verification for any - | user that recently registered with the application. Emails may also - | be re-sent if the user didn't receive the original email message. - | - */ - use VerifiesEmails; - /** - * Where to redirect users after verification. - * - * @var string - */ protected $redirectTo = RouteServiceProvider::HOME; - /** - * Create a new controller instance. - * - * @return void - */ public function __construct() { $this->middleware('auth'); $this->middleware('signed')->only('verify'); $this->middleware('throttle:6,1')->only('verify', 'resend'); } + + public function show() + { + if (auth()->user()->hasVerifiedEmail()) { + return redirect($this->redirectPath()); + } + + return view('auth.verify-email'); + } } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 0035379b..7bc3f508 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -39,6 +39,10 @@ class Kernel extends HttpKernel \App\Http\Middleware\PasswordProtected::class, ], + 'authed' => [ + 'auth', + ], + 'api' => [ // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, 'throttle:api', diff --git a/app/Http/Livewire/Item/Comments.php b/app/Http/Livewire/Item/Comments.php index 8572593a..0836526f 100644 --- a/app/Http/Livewire/Item/Comments.php +++ b/app/Http/Livewire/Item/Comments.php @@ -3,6 +3,8 @@ namespace App\Http\Livewire\Item; use App\Models\Item; +use App\Settings\GeneralSettings; +use Filament\Http\Livewire\Concerns\CanNotify; use Livewire\Component; use Filament\Forms\Components\Tabs; use Filament\Forms\Contracts\HasForms; @@ -11,7 +13,7 @@ class Comments extends Component implements HasForms { - use InteractsWithForms; + use CanNotify, InteractsWithForms; public Item $item; public $comments; @@ -32,6 +34,12 @@ public function submit() return redirect()->route('login'); } + if (app(GeneralSettings::class)->users_must_verify_email && !auth()->user()->hasVerifiedEmail()) { + $this->notify('primary', 'Please verify your email before replying to items.'); + + return redirect()->route('verification.notice'); + } + $formState = array_merge($this->form->getState(), [ 'parent_id' => $this->reply, 'user_id' => auth()->id(), diff --git a/app/Http/Livewire/Modals/Item/CreateItemModal.php b/app/Http/Livewire/Modals/Item/CreateItemModal.php index 27c3e225..9d034035 100644 --- a/app/Http/Livewire/Modals/Item/CreateItemModal.php +++ b/app/Http/Livewire/Modals/Item/CreateItemModal.php @@ -83,6 +83,12 @@ public function submit() return redirect()->route('login'); } + if (app(GeneralSettings::class)->users_must_verify_email && !auth()->user()->hasVerifiedEmail()) { + $this->notify('primary', 'Please verify your email before submitting items.'); + + return redirect()->route('verification.notice'); + } + $data = $this->form->getState(); $item = Item::create([ diff --git a/app/Models/User.php b/app/Models/User.php index c47dbb71..88e09468 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -9,11 +9,12 @@ use Filament\Models\Contracts\HasAvatar; use Illuminate\Notifications\Notifiable; use Filament\Models\Contracts\FilamentUser; +use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; -class User extends Authenticatable implements FilamentUser, HasAvatar +class User extends Authenticatable implements FilamentUser, HasAvatar, MustVerifyEmail { use HasApiTokens, HasFactory, Notifiable; diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 1bfeb7f2..7d6e15ff 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,7 +2,9 @@ namespace App\Providers; +use App\Http\Kernel; use Filament\Facades\Filament; +use App\Settings\GeneralSettings; use App\Services\OgImageGenerator; use Illuminate\Support\Collection; use App\SocialProviders\SsoProvider; @@ -13,17 +15,17 @@ class AppServiceProvider extends ServiceProvider { - public function boot(): void + public function boot(Kernel $kernel): void { View::composer('partials.meta', static function ($view) { $view->with( 'defaultImage', OgImageGenerator::make(config('app.name')) - ->withSubject('Roadmap') - ->withPolygonDecoration() - ->withFilename('og.jpg') - ->generate() - ->getPublicUrl() + ->withSubject('Roadmap') + ->withPolygonDecoration() + ->withFilename('og.jpg') + ->generate() + ->getPublicUrl() ); }); @@ -33,12 +35,12 @@ public function boot(): void Filament::registerNavigationItems([ NavigationItem::make() - ->group('External') - ->sort(101) - ->label('Public view') - ->icon('heroicon-o-rewind') - ->isActiveWhen(fn (): bool => false) - ->url('/'), + ->group('External') + ->sort(101) + ->label('Public view') + ->icon('heroicon-o-rewind') + ->isActiveWhen(fn (): bool => false) + ->url('/'), ]); if (file_exists($favIcon = storage_path('app/public/favicon.png'))) { @@ -46,8 +48,11 @@ public function boot(): void } $this->bootSsoSocialite(); - $this->bootCollectionMacros(); + +// if (app(GeneralSettings::class)->users_must_verify_email) { +// $this->addVerificationMiddleware($kernel); +// } } private function bootSsoSocialite(): void @@ -67,8 +72,13 @@ private function bootCollectionMacros(): void $nonPrioritized = $this->reject($callback); return $this - ->filter($callback) - ->merge($nonPrioritized); + ->filter($callback) + ->merge($nonPrioritized); }); } + + protected function addVerificationMiddleware(Kernel $kernel) + { + $kernel->appendMiddlewareToGroup('authed', 'verified'); + } } diff --git a/app/Settings/GeneralSettings.php b/app/Settings/GeneralSettings.php index 1f755d5b..533cc02f 100644 --- a/app/Settings/GeneralSettings.php +++ b/app/Settings/GeneralSettings.php @@ -25,6 +25,7 @@ class GeneralSettings extends Settings public bool $project_required_when_creating_item; public bool $block_robots; public string $inbox_workflow; + public bool $users_must_verify_email; public function getInboxWorkflow(): InboxWorkflow { diff --git a/app/View/Components/App.php b/app/View/Components/App.php index 025ab316..59e0f3e4 100644 --- a/app/View/Components/App.php +++ b/app/View/Components/App.php @@ -13,6 +13,7 @@ class App extends Component public Collection $projects; public string $brandColors; public bool $blockRobots = false; + public bool $userNeedsToVerify = false; public function __construct(public array $breadcrumbs = []) { @@ -38,6 +39,10 @@ public function render() $this->brandColors = $tw->getCssFormat(); + $this->userNeedsToVerify = app(GeneralSettings::class)->users_must_verify_email && + auth()->check() && + !auth()->user()->hasVerifiedEmail(); + return view('components.app'); } } diff --git a/database/migrations/2022_06_24_092158_create_jobs_table.php b/database/migrations/2022_06_24_092158_create_jobs_table.php index a786a891..4de64e58 100644 --- a/database/migrations/2022_06_24_092158_create_jobs_table.php +++ b/database/migrations/2022_06_24_092158_create_jobs_table.php @@ -1,11 +1,10 @@ migrator->add('general.users_must_verify_email', false); + } +} diff --git a/lang/en/auth.php b/lang/en/auth.php index 3d145862..2ff50c5f 100644 --- a/lang/en/auth.php +++ b/lang/en/auth.php @@ -2,6 +2,7 @@ return [ 'login' => 'Log in', + 'verify-email' => 'Verify email', 'register' => 'Register', 'profile' => 'Profile', 'register_for_free' => 'Or register for free.', diff --git a/lang/nl/auth.php b/lang/nl/auth.php index 9d90af94..3221eb39 100644 --- a/lang/nl/auth.php +++ b/lang/nl/auth.php @@ -13,6 +13,7 @@ return [ 'login' => 'Inloggen', + 'verify-email' => 'Verifieer email', 'register' => 'Registreren', 'profile' => 'Profiel', 'register_for_free' => 'Of maak een gratis account aan.', diff --git a/resources/views/auth/verify-email.blade.php b/resources/views/auth/verify-email.blade.php new file mode 100644 index 00000000..0a26b758 --- /dev/null +++ b/resources/views/auth/verify-email.blade.php @@ -0,0 +1,29 @@ +@section('title', trans('auth.verify-email')) +@section('image', App\Services\OgImageGenerator::make('Verify email')->withSubject('Roadmap')->withFilename('verify-email.jpg')->generate()->getPublicUrl()) + + +
+
+
+

+ {{ trans('auth.verify-email') }} +

+ + @if (session('resent')) + + @endif + +
+ {{ __('Before proceeding, please check your email for a verification link.') }} + {{ __('If you did not receive the email') }}, +
+ @csrf + . +
+
+
+
+
+
diff --git a/resources/views/components/app.blade.php b/resources/views/components/app.blade.php index 98ad88ac..0b9c7659 100644 --- a/resources/views/components/app.blade.php +++ b/resources/views/components/app.blade.php @@ -25,6 +25,21 @@ @endif +@if($userNeedsToVerify) +
+
+
+

+ You have not verified your email yet, please verify your email. + + Verify + +

+
+
+
+@endif @include('partials.header') diff --git a/resources/views/components/comment.blade.php b/resources/views/components/comment.blade.php index ff79c8ca..9a5fe4ac 100644 --- a/resources/views/components/comment.blade.php +++ b/resources/views/components/comment.blade.php @@ -2,7 +2,7 @@ @class([ 'ml-1 md:ml-6' => $comment->parent_id !== null, 'mr-1 bg-brand-50 rounded-lg ring-1 ring-brand-200' => $reply == $comment->id, - 'bg-yellow-50 border border-yellow-700 rounded-md mt-1' => $comment->private && !$comment->parent?->private, + 'bg-yellow-50 border border-yellow-700 rounded-md mt-1 mb-1' => $comment->private && !$comment->parent?->private, 'block py-2 overflow-hidden transition' ]) id="comment-{{ $comment->id }}"> diff --git a/resources/views/livewire/item/comments.blade.php b/resources/views/livewire/item/comments.blade.php index 88359982..e01c48fa 100644 --- a/resources/views/livewire/item/comments.blade.php +++ b/resources/views/livewire/item/comments.blade.php @@ -21,7 +21,7 @@ if (hash) { const commentElement = document.getElementById(hash.replace('#', '')); - commentElement.classList.add('bg-brand-50', 'rounded-lg', 'ring-1', 'ring-brand-200'); + commentElement.classList.add('bg-brand-50', 'rounded-lg', 'ring-1', 'ring-brand-200', 'mt-2', 'mb-2'); } })(); diff --git a/routes/web.php b/routes/web.php index 7e79de03..54099d89 100644 --- a/routes/web.php +++ b/routes/web.php @@ -5,13 +5,14 @@ use App\Http\Controllers\ItemController; use App\Http\Controllers\BoardsController; use App\Http\Controllers\ProjectController; +use App\Http\Controllers\Auth\VerificationController; use App\Http\Controllers\Auth\PasswordProtectionController; Auth::routes(); Route::get('oauth/login', [\App\Http\Controllers\Auth\LoginController::class, 'redirectToProvider']) - ->middleware('guest') - ->name('oauth.login'); + ->middleware('guest') + ->name('oauth.login'); Route::get('oauth/callback', [\App\Http\Controllers\Auth\LoginController::class, 'handleProviderCallback'])->middleware('guest'); Route::get('password-protection', PasswordProtectionController::class)->name('password.protection'); @@ -22,10 +23,14 @@ Route::get('projects/{project}', [ProjectController::class, 'show'])->name('projects.show'); Route::get('items/{item}', [ItemController::class, 'show'])->name('items.show'); Route::get('projects/{project}/items/{item}', [ItemController::class, 'show'])->name('projects.items.show'); -Route::post('projects/{project}/items/{item}/vote', [ItemController::class, 'vote'])->middleware('auth')->name('projects.items.vote'); +Route::post('projects/{project}/items/{item}/vote', [ItemController::class, 'vote'])->middleware('authed')->name('projects.items.vote'); Route::get('projects/{project}/boards/{board}', [BoardsController::class, 'show'])->name('projects.boards.show'); -Route::group(['middleware' => 'auth'], function () { +Route::get('/email/verify', [VerificationController::class, 'show'])->middleware('auth')->name('verification.notice'); +Route::post('/email/verification-notification', [VerificationController::class, 'resend'])->middleware(['auth', 'throttle:6,1'])->name('verification.resend'); +Route::get('/email/verify/{id}/{hash}', [VerificationController::class, 'verify'])->middleware(['auth', 'signed'])->name('verification.verify'); + +Route::group(['middleware' => 'authed'], function () { Route::get('profile', [\App\Http\Controllers\Auth\ProfileController::class, 'show'])->name('profile'); Route::get('my', MyController::class)->name('my');