diff --git a/app/Http/Controllers/Auth/RegisteredUserController.php b/app/Http/Controllers/Auth/RegisteredUserController.php index 96dbff0..327aa76 100644 --- a/app/Http/Controllers/Auth/RegisteredUserController.php +++ b/app/Http/Controllers/Auth/RegisteredUserController.php @@ -7,6 +7,7 @@ use App\Models\User; use App\Notifications\NewUserRegistered; use App\Rules\NotDisposableEmail; use App\Rules\ReservedIdentifier; +use App\Support\SafeNotifier; use Illuminate\Auth\Events\Registered; use Illuminate\Http\Request; use Illuminate\Support\Facades\Hash; @@ -49,7 +50,7 @@ class RegisteredUserController extends Controller // Notify all super admins of the new registration User::where('role', 'super_admin')->each(function (User $admin) use ($user) { - $admin->notify(new NewUserRegistered($user)); + SafeNotifier::send($admin, new NewUserRegistered($user)); }); auth()->login($user); diff --git a/app/Http/Controllers/CommentController.php b/app/Http/Controllers/CommentController.php index 9a2ff69..3f74b3f 100644 --- a/app/Http/Controllers/CommentController.php +++ b/app/Http/Controllers/CommentController.php @@ -9,6 +9,7 @@ use App\Notifications\NewCommentLikeNotification; use App\Notifications\NewCommentNotification; use App\Notifications\NewReplyNotification; use App\Services\LlmLyricsService; +use App\Support\SafeNotifier; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; @@ -71,12 +72,12 @@ class CommentController extends Controller // Reply — notify the parent comment's author $parent = Comment::find($request->parent_id); if ($parent && $parent->user_id !== $commenter->id) { - $parent->user->notify(new NewReplyNotification($video, $comment, $commenter)); + SafeNotifier::send($parent->user, new NewReplyNotification($video, $comment, $commenter)); } } else { // Top-level comment — notify the video owner if ($video->user_id !== $commenter->id) { - $video->user->notify(new NewCommentNotification($video, $comment, $commenter)); + SafeNotifier::send($video->user, new NewCommentNotification($video, $comment, $commenter)); } } @@ -162,7 +163,8 @@ class CommentController extends Controller if ($comment->user_id !== $userId) { $video = $comment->video; if ($video) { - $comment->user->notify( + SafeNotifier::send( + $comment->user, new NewCommentLikeNotification($video, $comment, Auth::user()) ); } diff --git a/app/Models/User.php b/app/Models/User.php index 16375ab..26e91ac 100755 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -281,7 +281,19 @@ class User extends Authenticatable implements MustVerifyEmail public function sendEmailVerificationNotification(): void { - $this->notify(new VerifyEmail); + // Mail failures must never bubble up as a 500 to the user — the account + // exists, the HTTP request was fine, only the delivery pipe is broken. + // We log the failure (ops picks it up) and move on. Re-sending via + // `php artisan app:resend-verification` fixes the user once SMTP recovers. + try { + $this->notify(new VerifyEmail); + } catch (\Throwable $e) { + \Illuminate\Support\Facades\Log::warning('verify-email send failed', [ + 'user_id' => $this->id, + 'email' => $this->email, + 'error' => $e->getMessage(), + ]); + } } // Role helper methods diff --git a/app/Support/SafeNotifier.php b/app/Support/SafeNotifier.php new file mode 100644 index 0000000..3364545 --- /dev/null +++ b/app/Support/SafeNotifier.php @@ -0,0 +1,43 @@ +notify(new SomeNotification(...)); + * + * we now do + * + * SafeNotifier::send($user, new SomeNotification(...)); + * + * which runs the same code, catches any Throwable, and writes a warning line + * so ops still has the signal. + */ +class SafeNotifier +{ + /** + * @param mixed $notifiable Anything with ->notify(), usually a User. + */ + public static function send(mixed $notifiable, Notification $notification): void + { + if (! $notifiable || ! is_object($notifiable) || ! method_exists($notifiable, 'notify')) { + return; + } + + try { + $notifiable->notify($notification); + } catch (\Throwable $e) { + Log::warning('notification send failed', [ + 'notification' => $notification::class, + 'notifiable' => $notifiable::class, + 'notifiable_id'=> $notifiable->getKey() ?? null, + 'error' => $e->getMessage(), + ]); + } + } +} diff --git a/docs/mail-setup.md b/docs/mail-setup.md new file mode 100644 index 0000000..e7ff55f --- /dev/null +++ b/docs/mail-setup.md @@ -0,0 +1,62 @@ +# Mail setup + +Mail failures are now non-fatal — a down SMTP will log a warning in +`data/logs/laravel.log` but will NOT 500 the user. Still, delivery is a +feature; here's how to fix the current Gmail 535 auth errors. + +## Option A — Gmail App Password (quickest, free, ~500 sends/day) + +1. On `platformtakeone@gmail.com`: + - Enable 2-Step Verification (Google Account → Security). + - Generate an **App Password** (Security → App passwords → "Mail", "Other: TAKEONE"). + - Copy the 16-character password Google shows (no spaces). + +2. `.env`: + ```env + MAIL_MAILER=smtp + MAIL_HOST=smtp.gmail.com + MAIL_PORT=587 + MAIL_USERNAME=platformtakeone@gmail.com + MAIL_PASSWORD=xxxx-xxxx-xxxx-xxxx # the app password, no quotes needed + MAIL_ENCRYPTION=tls + MAIL_FROM_ADDRESS=platformtakeone@gmail.com + MAIL_FROM_NAME="TAKEONE" + ``` + +3. `php artisan config:clear && php artisan queue:restart` + +4. Smoke test: + ```bash + php artisan app:resend-verification --user= + ``` + +## Option B — Mailgun (recommended for production) + +```env +MAIL_MAILER=mailgun +MAILGUN_DOMAIN=mg.takeone.bh +MAILGUN_SECRET=key-... +MAILGUN_ENDPOINT=api.eu.mailgun.net # or api.mailgun.net for US region +MAIL_FROM_ADDRESS=no-reply@takeone.bh +MAIL_FROM_NAME="TAKEONE" +``` + +## Option C — Postmark (also fine) + +```env +MAIL_MAILER=postmark +POSTMARK_TOKEN=... +MAIL_FROM_ADDRESS=no-reply@takeone.bh +MAIL_FROM_NAME="TAKEONE" +``` + +## Resend verification to users whose first email failed + +```bash +# One specific user +php artisan app:resend-verification --user=42 + +# Everyone still unverified (dry-run first) +php artisan app:resend-verification --all-unverified --dry-run +php artisan app:resend-verification --all-unverified +```