Skip to content

Implement password reset and email flows with SMTP integration - #1375

Open
piciolo wants to merge 1 commit into
lanedirt:mainfrom
piciolo:SMTP
Open

Implement password reset and email flows with SMTP integration#1375
piciolo wants to merge 1 commit into
lanedirt:mainfrom
piciolo:SMTP

Conversation

@piciolo

@piciolo piciolo commented Apr 12, 2026

Copy link
Copy Markdown
Contributor

Description

This PR implements a complete SMTP email system for OGameX, covering two core account-recovery flows: password reset and email retrieval.

Changes by file:

app/Mail/ResetPasswordMail.php (new)
Mailable class for the password reset flow. Uses Envelope/Content API (Laravel 10+), reads the reset URL and username from constructor arguments, and renders the branded outgame.mail.reset-password Blade view.

app/Mail/RetrieveEmailMail.php (new)
Mailable class for the "forgot email" flow. Sends a masked version of the user's email address (e.g. pi***@gmail.com) along with a login link, using the branded outgame.mail.retrieve-email Blade view.

app/Http/Controllers/ForgotEmailController.php (new)
Controller for the /forgot-email route. Accepts a username, looks up the associated account, and sends the masked email hint via RetrieveEmailMail. Always returns the same success message regardless of whether the username exists, to prevent user enumeration.

app/Models/User.php
Added sendPasswordResetNotification($token) override to replace Laravel's default plain-text notification with the branded ResetPasswordMail. Builds the full reset URL (including email parameter) before passing it to the Mailable.

app/Providers/FortifyServiceProvider.php
Registered custom Blade views for Fortify's password reset link request page (outgame.forgot-password) and reset password page (outgame.reset-password), replacing the default Fortify responses which were previously throwing BindingResolutionException.

resources/views/outgame/forgot-password.blade.php (new)
Branded outgame page for requesting a password reset link. Matches the login page visual style (space background, OGameX card design).

resources/views/outgame/reset-password.blade.php (new)
Branded outgame page for submitting a new password after clicking the reset link from email.

resources/views/outgame/forgot-email.blade.php (new)
Branded outgame page for the "forgot email" flow — accepts a username and shows a confirmation message after submission.

resources/views/outgame/mail/reset-password.blade.php (new)
HTML email template for password reset. Table-based layout for email client compatibility. Matches the login page color palette (#22303f card, #619fc8 blue headings, #1a2530 header). OGameX logo embedded as base64 data URI so it renders correctly in all mail clients (Gmail, Outlook, etc.) without depending on APP_URL being publicly accessible.

resources/views/outgame/mail/retrieve-email.blade.php (new)
HTML email template for the email retrieval flow. Same visual style as the reset-password template. Displays the masked email address in a highlighted inner box.

resources/views/outgame/layouts/main.blade.php
Replaced href="#" placeholder links in the login modal with proper route() calls: route('password.request') for forgot password and route('password.email-lookup') for forgot email.

resources/js/outgame/b55eb79922e157d28e811c7452ab10.js
Removed legacy JS handlers that intercepted clicks on #pwLost / #emailLost and redirected them to https://{server}/game/reg/mail.php and https://{server}/game/index.php?page=standalone&component=retrieveEmail (original OGame live server URLs). Replaced with internal route navigation.

routes/web.php
Added two routes for the forgot-email flow:

  • GET /forgot-emailForgotEmailController@show (named password.email-lookup)
  • POST /forgot-emailForgotEmailController@send (with throttle:5,1 middleware)

config/auth.php
Made the password reset throttle configurable via AUTH_PASSWORD_RESET_THROTTLE env variable (default: 60 seconds) instead of hardcoded value.

database/migrations/2026_04_05_193349_create_password_reset_tokens_table.php (new)
Creates the password_reset_tokens table required by Laravel Fortify's password reset flow (email PK, token, created_at). This table was missing from the project's migration history.

.env.example
Added SMTP-related environment variables:

  • MAIL_MAILER=smtp — critical: without this key Laravel defaults to the log driver, silently dropping all emails
  • MAIL_FROM_ADDRESS, MAIL_FROM_NAME
  • AUTH_PASSWORD_RESET_THROTTLE=60
  • Comments for common SMTP providers (Gmail, Mailgun, SES)

Type of Change:

  • Bug fix (missing password_reset_tokens table, broken forgot-password link, logo not rendering in emails)
  • Feature enhancement (complete SMTP email system with branded templates)
  • Documentation update
  • Other

Related Issues

N/A


Checklist

  • Automated Refactoring: Rector has been run and no outstanding issues remain.
  • Code Standards: Code adheres to PSR-12 coding standards. Verified with Laravel Pint.
  • Static Analysis: Code passes PHPStan static code analysis.
  • Testing:
    • Relevant unit and feature tests are included or updated.
    • Tests successfully run locally.
  • CSS & JS Build: CSS and JS assets compiled via npm run dev after changes to b55eb79922e157d28e811c7452ab10.js.
  • Documentation: Documentation has been updated to reflect any changes made.

Additional Information

Email delivery verified: both ResetPasswordMail and RetrieveEmailMail confirmed delivered via Gmail SMTP (smtp.gmail.com:587, TLS).

Logo rendering fix: the OGameX icon in email templates is now embedded as a base64 data URI. This avoids the issue where external mail servers (Gmail, Outlook) cannot load images from http://localhost during development, or from servers with self-signed certificates.

No breaking changes. The password_reset_tokens migration must be run on existing installations: php artisan migrate.

image image image image

@Geda173 Geda173 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @piciolo,

thank you for your contribution. Having this in the project will make the user experience better! The core implementation is well-structured: username enumeration protection in ForgotEmailController, correct sendPasswordResetNotification override on the User model, clean Mailable classes, and the throttle configurable via AUTH_PASSWORD_RESET_THROTTLE env is nice to have.

I have added a couple change requests below. Furthermore I would ask that you add some form of test validation for your PR. The forgot-password and forgot-email flows have no feature test coverage. Given the project has thorough tests for other auth-adjacent flows, these should have equivalent coverage. At minimum: happy path (email sent), unknown username/email (silent success, no leak), throttle enforcement, and token expiry/invalid token on reset.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both changes in resources/views/ingame/chat/index.blade.php and resources/views/ingame/layouts/main.blade.php are unrelated to your PR and should not be in the diff.

Comment thread .env.example Outdated
AUTH_PASSWORD_RESET_THROTTLE=60

MAIL_MAILER=smtp
MAIL_DRIVER=smtp

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR adds MAIL_MAILER=smtp but leaves the pre-existing MAIL_DRIVER=smtp directly below it. MAIL_DRIVER has been deprecated since Laravel 7; MAIL_MAILER is the correct key. The old one should be removed to avoid confusion.


<!-- Outer wrapper -->
<table width="100%" border="0" cellpadding="0" cellspacing="0"
style="background-color:#000000;background-image:url('{{ config('app.url') }}/img/outgame/1867da5b5f8769b547bb91d88bb4f8.jpg');background-repeat:no-repeat;background-position:center top;">

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CSS background-image with an external URL is ignored by virtually all major email clients - Gmail strips it entirely, Outlook has never supported it, and Apple Mail blocks it unless the user explicitly loads remote content. This background will not render for most recipients. Since the logo was already embedded as base64 in a follow-up commit, the same approach should be used here, or the background mage should simply be dropped from the email layout.

<body style="margin:0;padding:0;background-color:#000000;">

<table width="100%" border="0" cellpadding="0" cellspacing="0"
style="background-color:#000000;background-image:url('{{ config('app.url') }}/img/outgame/1867da5b5f8769b547bb91d88bb4f8.jpg');background-repeat:no-repeat;background-position:center top;">

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CSS background-image with an external URL is ignored by virtually all major email clients - Gmail strips it entirely, Outlook has never supported it, and Apple Mail blocks it unless the user explicitly loads remote content. This background will not render for most recipients. Since the logo was already embedded as base64 in a follow-up commit, the same approach should be used here, or the background mage should simply be dropped from the email layout.

@piciolo

piciolo commented Apr 13, 2026

Copy link
Copy Markdown
Contributor Author

Summary

Full implementation of SMTP email system for account recovery flows: password reset (via Laravel Fortify) and email retrieval (custom flow by username lookup).

New features

  • Password reset flow — integrated with Fortify: custom branded views for /forgot-password and /reset-password, ResetPasswordMail Mailable class, User::sendPasswordResetNotification() override
  • Forgot email flow — custom ForgotEmailController: accepts username, sends masked email hint (e.g. pi***@gmail.com) via RetrieveEmailMail, no username enumeration (always returns success)
  • Branded HTML email templates — table-based layout for email client compatibility, OGameX card design with dark header, white outer background, black copyright text
  • i18n support — all UI and email strings added in EN, IT, NL (t_external.php)

Bug fixes

  • Legacy JS intercept removed#pwLost / #emailLost click handlers no longer redirect to https://{server}/game/reg/mail.php (original OGame URLs); now navigate to internal /forgot-password and /forgot-email routes
  • Missing password_reset_tokens table — added migration required by Fortify's reset flow
  • MAIL_DRIVER deprecated — removed from .env.example, replaced with MAIL_MAILER=smtp (correct key since Laravel 7+)
  • Email background-image removed — Gmail strips CSS background-image, Outlook ignores it; replaced with solid #ffffff background
  • Logo removed from emails — base64 embedded image was not rendering in all clients; removed entirely to avoid broken icon
  • Copyright text fixed — removed duplicate © OGameX — prefix (translation key already contains full text), changed color to #000000 for readability on white background

Cleanup

  • Removed unrelated changes to resources/views/ingame/chat/index.blade.php and resources/views/ingame/layouts/main.blade.php that were accidentally included in a previous version of the branch

Configuration

  • AUTH_PASSWORD_RESET_THROTTLE env variable added to config/auth.php (default: 60s, configurable)
  • .env.example updated with MAIL_MAILER, MAIL_FROM_ADDRESS, MAIL_FROM_NAME, SMTP provider hints

Tests (11 tests, 77 assertions — all passing)

  • ForgotPasswordTest (5 tests): page load, reset link sent for existing email, no mail for unknown email, valid token reset, invalid token rejection
  • ForgotEmailTest (6 tests): page load, email sent for existing username, no mail for unknown username, masked email validation, empty username validation

@piciolo
piciolo requested a review from Geda173 April 13, 2026 15:49
@Geda173

Geda173 commented Apr 20, 2026

Copy link
Copy Markdown
Collaborator

Thanks for addressing the previous feedback. The tests, background image fix, and rate limiter cleanup all look good.

However, before this can be merged there are two issues that need to be resolved per the CONTRIBUTING.md:

  1. Branch must be based on main

"Always create your feature branch from the latest main. Do not base a branch on another feature branch or an outdated commit."

This branch was not created from lanedirt/OGameX main. It was brought up to date by merging from a fork remote (piciolo/OGame) rather than rebasing onto upstream main. Please rebase onto the current main before resubmitting.

  1. Unrelated changes must be in a separate PR

"Do not bundle unrelated changes — even small cleanups or fixes to other areas should be submitted separately."

Commit 0b5ec53 (fix: restore chat title styling and fix sidebar player list) is included in this branch but has nothing to do with SMTP. It modifies chat.js, ingame.css, chat/index.blade.php, and ingame/layouts/main.blade.php. It also causes ~1,473 lines of noise in the compiled ingame.js / ingame.min.jsbundles, which makes the diff significantly harder to review. Note that the chat changes this commit addresses are already merged upstream via #1371 and #1372. Dropping this commit and rebasing onto main should make it disappear entirely with no loss of functionality.

Once those two things are done the diff should be clean and the PR ready to merge. Please note that repeated violations of the CONTRIBUTING.md guidelines will cause PRs, which do not meet the criteria, to be closed without comment.

@piciolo

piciolo commented Apr 22, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for addressing the previous feedback. The tests, background image fix, and rate limiter cleanup all look good.

However, before this can be merged there are two issues that need to be resolved per the CONTRIBUTING.md:

  1. Branch must be based on main

"Always create your feature branch from the latest main. Do not base a branch on another feature branch or an outdated commit."

This branch was not created from lanedirt/OGameX main. It was brought up to date by merging from a fork remote (piciolo/OGame) rather than rebasing onto upstream main. Please rebase onto the current main before resubmitting.

  1. Unrelated changes must be in a separate PR

"Do not bundle unrelated changes — even small cleanups or fixes to other areas should be submitted separately."

Commit 0b5ec53 (fix: restore chat title styling and fix sidebar player list) is included in this branch but has nothing to do with SMTP. It modifies chat.js, ingame.css, chat/index.blade.php, and ingame/layouts/main.blade.php. It also causes ~1,473 lines of noise in the compiled ingame.js / ingame.min.jsbundles, which makes the diff significantly harder to review. Note that the chat changes this commit addresses are already merged upstream via #1371 and #1372. Dropping this commit and rebasing onto main should make it disappear entirely with no loss of functionality.

Once those two things are done the diff should be clean and the PR ready to merge. Please note that repeated violations of the CONTRIBUTING.md guidelines will cause PRs, which do not meet the criteria, to be closed without comment.

Done

@Geda173

Geda173 commented Apr 22, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the rebase and for addressing all previous feedback. The diff is now clean and all concerns are resolved.
Two small nits before I approve, neither blocking:

  1. routes/web.php line 36: ForgotEmailController is imported between RewardsController and RulesController but alphabetically it belongs in the F-section (near FleetController).
  2. forgot-password.blade.php and reset-password.blade.php: the email input fields use type="text". Changing to type="email" gives mobile users the right keyboard and enables browser format validation for free.

Fix those and this is good to go.

@piciolo

piciolo commented Apr 22, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the rebase and for addressing all previous feedback. The diff is now clean and all concerns are resolved. Two small nits before I approve, neither blocking:

  1. routes/web.php line 36: ForgotEmailController is imported between RewardsController and RulesController but alphabetically it belongs in the F-section (near FleetController).
  2. forgot-password.blade.php and reset-password.blade.php: the email input fields use type="text". Changing to type="email" gives mobile users the right keyboard and enables browser format validation for free.

Fix those and this is good to go.
Fix

@tiweb442

Copy link
Copy Markdown
Contributor

Production smoke test (tiweb442 / space.tiweb-host.com)

Tested on a live OGameX deployment (Jul 2026):

  • /forgot-password and /forgot-email render (200)
  • password_reset_tokens migration applied
  • SMTP (STARTTLS port 587, self-signed cert allowed) — auth succeeds; password reset flow wired to panel mail host
  • Login modal links point to Fortify / forgot-email routes (not legacy mail.php)

Related local fixes in the same batch: #1491#1494 (tiweb442).

Happy to help rebase or re-test if this branch needs an update against current main.

@tiweb442

Copy link
Copy Markdown
Contributor

Clean rebase onto current main (CONTRIBUTING)

#1375 is currently conflicting with main. Per CONTRIBUTING.md (branch from latest main, one issue per PR), I rebased the email-only commits onto current main with no unrelated changes:

Replacement PR: #1500

Includes the prior review follow-ups (tests, rate limiter, alphabetized import, type="email", template cleanup). Full credit remains with @piciolo for the original implementation — happy for this to be closed in favor of #1500, or for @piciolo to take the same rebase on SMTP if preferred.

@piciolo

piciolo commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

Hi @Geda173, rebased and green — ready for another look.

The branch had gone stale (main advanced ~3 months and it was CONFLICTING), so I rebased it onto current main and squashed the iteration commits into one. All previously agreed changes and the two final nits (import alphabetization in routes/web.php, type="email" on the reset/forgot inputs) are preserved.

Two adaptations were needed for current main:

  • Vite migrationmain moved the outgame bundle from Laravel Mix (public/js/*) to Vite (public/build/assets/*). The forgot-password / forgot-email login-modal links were reapplied to the outgame source (resources/js/outgame/…) and the built bundle updated accordingly.
  • phpstan level 8 — the feature tests used User::find() (which is User|null); switched to User::findOrFail() so the reset/lookup assertions are type-safe at level 8.

@piciolo

piciolo commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

Hi @Geda173, rebased and green — ready for another look.

The branch had gone stale (main advanced ~3 months and it was CONFLICTING), so I rebased it onto current main and squashed the iteration commits into one. All previously agreed changes and the two final nits (import alphabetization in routes/web.php, type="email" on the reset/forgot inputs) are preserved.

Two adaptations were needed for current main:

  • Vite migrationmain moved the outgame bundle from Laravel Mix (public/js/*) to Vite (public/build/assets/*). The forgot-password / forgot-email login-modal links were reapplied to the outgame source (resources/js/outgame/…) and the built bundle updated accordingly.
  • phpstan level 8 — the feature tests used User::find() (which is User|null); switched to User::findOrFail() so the reset/lookup assertions are type-safe at level 8.

CI is green: test-docker, phpstan (level 8), Pint, Rector and the preview all pass. Let me know if anything else is needed.

@Geda173 Geda173 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the rebase. The branch is clean now. A few things left before I can approve:

  1. The built frontend bundle needs regenerating.The file public/build/assets/outgame-b0a7e11e.js was edited directly instead of being rebuilt. The filename contains a hash of the file's contents, so when the contents change the filename has to change too and public/build/manifest.json needs to update alongside it. Running npm run build and committing the result should sort it out.

  2. A couple of test cases from the original request are still missing.

  • Rate limiting isn't tested. The limiter is currently switched off in the test environment (FortifyServiceProvider.php), so it can't be exercised at all. I confirmed that repeated rapid requests are never blocked during tests. Could you remove that exemption and add a test that the limit actually kicks in? As it stands, if the rate limiting broke or got removed, nothing would notice.

  • Expired reset tokens aren't tested. The invalid-token case is covered, but not expiry. The behaviour works correctly, it just isn't protected by a test.

  1. One small thing on testPasswordCanBeResetWithValidToken: it only checks that the request redirects, not that the password actually changed. The reset does work correctly, but as written the test would still pass if it silently did nothing ,which makes it worth tightening, since it's the most important test in the PR. Asserting the new password works afterwards would cover it.

  2. Minor style: ResetPasswordMail and RetrieveEmailMail both declare their traits on one line (use Queueable, SerializesModels;). Everywhere else in app/ uses one trait per line, so please split them to match.

Nothing here is a big lift, the implementation itself is in good shape. Happy to take another look once these are addressed.

Adds two account-recovery flows:
- Password reset (Fortify): branded /forgot-password and /reset-password views,
  ResetPasswordMail, User::sendPasswordResetNotification override,
  password_reset_tokens migration, and a configurable reset throttle
  (AUTH_PASSWORD_RESET_THROTTLE).
- Forgot email: /forgot-email (ForgotEmailController) looks up a user by username
  and emails a masked address hint via RetrieveEmailMail, with username-enumeration
  protection and a named 'forgot-email' rate limiter.

Branded HTML email templates, i18n keys (EN/IT/NL), login-modal links wired to the
new routes (blade + outgame JS), and feature tests (ForgotPasswordTest,
ForgotEmailTest) that cover the reset flow, the forgot-email lookup, the active
'forgot-email' rate limiter (now exercised in the testing environment), reset-token
expiry, and that a valid reset actually changes the account password.

Rebased onto current main; outgame Vite bundle rebuilt (npm run build) for the new links.
@github-actions

Copy link
Copy Markdown

Preview deployment

Preview environment for this PR is ready for functional testing. Use one of the test accounts below to log in.

Status URL Commit Last deployed
✅ Live https://pr-1375.preview.ogamex.dev 7631a4e 2026-07-26 11:42:57 UTC
Test accounts (click to expand)
Email (login) Password Role Class Planets Description
test1@ogamex.dev test admin Collector 2 Admin, high resources, mining focus
test2@ogamex.dev test player General 1 Military focus, ships & fleet
test3@ogamex.dev test player Discoverer 1 Exploration, probes & cargo ships
test4@ogamex.dev test player None 1 New player, no class selected yet
test5@ogamex.dev test moderator Collector 1 Moderator, balanced setup
test6@ogamex.dev test player General 1 Vacation mode enabled
test7@ogamex.dev test player General 3 Heavy defense across planets
test8@ogamex.dev test player Discoverer 1 Fleet focus, inactive (i) 8 days
test9@ogamex.dev test player Collector 1 Beginner, long inactive (I) 40 days
test10@ogamex.dev test player General 5 Endgame, 90 unread messages

This preview will be automatically destroyed when the PR is closed.

@piciolo

piciolo commented Jul 26, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review @Geda173 — all four points are addressed in the latest push.

1. Rebuilt the outgame bundle
Regenerated the outgame bundle so its filename is a real content hash of the new contents (outgame-b0a7e11e.jsoutgame-8f625bec.js) and updated public/build/manifest.json to point at it; the hand-edited file is gone. I verified the regeneration reproduces the existing hashes byte-for-byte for unchanged input, so only the bundle whose source actually changed gets a new hash.

2. Rate limiting is now exercised
Removed the testing-environment Limit::none() exemption on the forgot-email limiter, so the throttle is live in tests, and added testForgotEmailRequestsAreRateLimited — five requests pass, the sixth gets a 429.

One wrinkle worth flagging: with the exemption gone, the limiter state leaked across test methods, because phpunit.xml sets CACHE_DRIVER (the pre-Laravel-11 name) while the framework now reads CACHE_STORE, so tests fall back to the persistent database cache instead of the intended array store. I kept the change local by flushing the cache in ForgotEmailTest::setUp() so each test starts from a clean counter, but the phpunit.xml env key might be worth fixing separately.

3. Expired-token coverage
Added testPasswordResetFailsWithExpiredToken — creates a token, travels past the 60-minute reset lifetime, and asserts the reset is rejected with errors while the password stays unchanged.

4. testPasswordCanBeResetWithValidToken now proves the reset works
It asserts the new password validates against the stored hash (and the old one no longer does) and that the account can log in with the new password afterwards.

5. Trait style
Split use Queueable, SerializesModels; into one trait per line in ResetPasswordMail and RetrieveEmailMail.

CI is green: both docker test jobs, phpstan level 8, Pint, Rector, and the preview deploy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Email system: SMTP configuration, password reset UI, and notification emails

3 participants