Skip to content

Conversation

@ljonesfl
Copy link
Member

@ljonesfl ljonesfl commented Nov 13, 2025

Adds member signup and portal functionality. Much refactoring and cleanup.

Summary by CodeRabbit

  • New Features

    • Member registration with email verification, resend flow and verification emails.
    • Member dashboard, profile editing (timezone, password), and conditional Sign Up CTA.
    • Member-only layout and routes; theme selection per role (admin/subscriber/guest).
  • Documentation

    • Updated docs and installer to use neuron.yaml; added example neuron config and member settings.
  • Chores

    • Added database migrations for users, password resets and email verification tokens.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 13, 2025

Walkthrough

Adds a full member registration and email verification system, new user-related database migrations, member controllers/views/routes, repositories and services, initializers and tests, and renames the primary configuration file from config.yaml to neuron.yaml across examples and docs.

Changes

Cohort / File(s) Summary
Configuration & Examples
examples/config/*, resources/config/neuron.yaml, resources/config/neuron.yaml.example, resources/config/database.yaml.example, resources/config/maintenance.yaml.example
Remove old examples/config/config.yaml; add neuron.yaml examples and update docs to use config/neuron.yaml. Introduces member settings (registration, verification, default role, token expiration).
Auth Config
resources/config/auth.yaml
Adds member block with registration and email verification settings (registration_enabled, require_email_verification, default_role, verification_token_expiration_minutes, verification_url, resend_throttle_seconds).
Migrations
resources/database/migrate/20250111000000_create_users_table.php, resources/database/migrate/20250112000000_create_email_verification_tokens_table.php
New Phinx migrations: users table (auth/profile fields, indexes) and email_verification_tokens table (FK to users, token + expiry).
Models & Repos
src/Cms/Models/EmailVerificationToken.php, src/Cms/Repositories/IEmailVerificationTokenRepository.php, src/Cms/Repositories/DatabaseEmailVerificationTokenRepository.php
Add EmailVerificationToken model, repository interface, and PDO-backed repository implementation with create/find/delete/cleanup operations.
Verification Manager & Filter
src/Cms/Auth/EmailVerificationManager.php, src/Cms/Auth/Filters/MemberAuthenticationFilter.php
New EmailVerificationManager (token lifecycle, send/validate/verify/resend/cleanup) and MemberAuthenticationFilter enforcing login and optional email verification; Registry population on success.
Registration Service & DTO
src/Cms/Services/Member/RegistrationService.php, src/Cms/Dtos/RegisterUserDto.yaml
Add RegistrationService handling validation, user creation, conditional verification email, and DTO schema for registration payload.
Controllers
src/Cms/Controllers/Member/Registration.php, src/Cms/Controllers/Member/Dashboard.php, src/Cms/Controllers/Member/Profile.php
New member controllers: registration flow (forms, processing, verify/resend), dashboard, and profile edit/update with CSRF and flash handling.
Initializers & View Data
resources/app/Initializers/RegistrationInitializer.php, resources/app/Initializers/ViewDataInitializer.php
New initializer wiring registration/verification components into Registry and a ViewDataInitializer that shares siteName, appVersion, currentUser, theme, currentYear, isAuthenticated.
Views & Layouts
resources/views/member/**, resources/views/emails/email-verification.php, resources/views/layouts/*.php, resources/views/home/index.php
Add member views (register, verify-sent, verified, registration-disabled), dashboard/profile views, email verification template, member layout, and update layouts/home to use dynamic theme and shared view data.
Routes
resources/config/routes.yaml
Adds routes for register, registration POST, verify email, verify sent, resend verification, member dashboard and profile update.
CLI / Install
src/Cms/Cli/Commands/Install/InstallCommand.php, src/Cms/Cli/Commands/Maintenance/EnableCommand.php, src/Cms/Cli/Commands/Queue/InstallCommand.php
Replace migration generation with migration copying, and change CLI tooling/docs to target config/neuron.yaml instead of config.yaml.
Home Controller
src/Cms/Controllers/Home.php
Exposes RegistrationService runtime flag to views (RegistrationEnabled).
Tests & Test Bootstrap
tests/*.php, tests/Cms/**
Add extensive tests for EmailVerificationManager, MemberAuthenticationFilter, repository, model, and RegistrationService; update tests and bootstrap to use ViewDataProvider and neuron.yaml.
Docs & Changelog
readme.md, specs/authentication-implementation-plan.md, src/Cms/Maintenance/README.md, versionlog.md
Update docs to reference neuron.yaml; add changelog entries (timezone support and config rename).

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Controller as RegistrationController
    participant Service as RegistrationService
    participant UserRepo as UserRepository
    participant Manager as EmailVerificationManager
    participant TokenRepo as TokenRepository
    participant Sender as EmailSender

    User->>Controller: POST /register
    Controller->>Service: register(username,email,password,confirm)
    Service->>UserRepo: findByUsername / findByEmail
    Service->>Service: validateRegistration(...)
    Service->>UserRepo: create(user)
    Service->>Manager: sendVerificationEmail(user)
    Manager->>TokenRepo: deleteByUserId(user.id)
    Manager->>TokenRepo: create(token)
    Manager->>Sender: send(verification email)
    Sender-->>Manager: success
    Manager-->>Service: sent
    Service-->>Controller: registration result
    Controller-->>User: redirect (verify-email-sent)
Loading
sequenceDiagram
    participant User
    participant Controller as RegistrationController
    participant Manager as EmailVerificationManager
    participant TokenRepo as TokenRepository
    participant UserRepo as UserRepository

    User->>Controller: GET /verify-email?token=xyz
    Controller->>Manager: validateToken(xyz)
    Manager->>TokenRepo: findByToken(hashed)
    TokenRepo-->>Manager: tokenRecord
    Manager->>Manager: check isExpired()
    alt valid
        Manager->>UserRepo: findById(token.user_id)
        Manager->>UserRepo: update(email_verified=true, status=active)
        Manager->>TokenRepo: deleteByToken(hashed)
        Manager-->>Controller: success
        Controller-->>User: render success
    else invalid/expired
        Manager-->>Controller: failure
        Controller-->>User: render failure
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas requiring extra attention:

  • src/Cms/Auth/EmailVerificationManager.php — token generation/hashing, expiration and send logic, error handling.
  • src/Cms/Services/Member/RegistrationService.php — validation edge cases, uniqueness checks, state transitions when verification is optional.
  • src/Cms/Auth/Filters/MemberAuthenticationFilter.php — redirect flow, Registry side-effects, and exit() behavior.
  • Database migrations — schema correctness, timestamp defaults, indexes and FK behavior.
  • resources/app/Initializers/RegistrationInitializer.php — dependency wiring and router filter registration.
  • Tests that rely on randomness or external sending — ensure deterministic mocks.

Possibly related PRs

Poem

🐰 I hopped through configs, swapped a name with grace,
Tokens tucked in pockets, ready for the race,
Themes now dance on every page, members verified true,
A little rabbit cheers — welcome, Neuron crew!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.63% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title references the two main changes: member portal (feature addition) and refactoring, aligning with the PR description and extensive changeset scope.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/member-portal

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 88fd7d9 and e28fa69.

📒 Files selected for processing (4)
  • resources/app/Initializers/RegistrationInitializer.php (1 hunks)
  • resources/database/migrate/20250112000000_create_email_verification_tokens_table.php (1 hunks)
  • src/Cms/Auth/EmailVerificationManager.php (1 hunks)
  • src/Cms/Controllers/Member/Registration.php (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • resources/database/migrate/20250112000000_create_email_verification_tokens_table.php
🧰 Additional context used
🧬 Code graph analysis (3)
src/Cms/Controllers/Member/Registration.php (5)
src/Cms/Controllers/Content.php (3)
  • Content (57-279)
  • getSessionManager (219-227)
  • redirect (237-247)
src/Cms/Auth/CsrfTokenManager.php (2)
  • CsrfTokenManager (13-69)
  • validate (49-60)
src/Cms/Auth/EmailVerificationManager.php (4)
  • EmailVerificationManager (21-247)
  • __construct (39-52)
  • verifyEmail (123-164)
  • resendVerification (173-190)
src/Cms/Services/Member/RegistrationService.php (4)
  • RegistrationService (21-214)
  • __construct (38-51)
  • isRegistrationEnabled (58-61)
  • register (73-139)
src/Cms/Auth/SessionManager.php (3)
  • get (108-112)
  • set (99-103)
  • getFlash (144-150)
src/Cms/Auth/EmailVerificationManager.php (6)
src/Cms/Models/EmailVerificationToken.php (4)
  • EmailVerificationToken (14-178)
  • __construct (29-35)
  • getId (40-43)
  • isExpired (125-128)
src/Cms/Services/Email/Sender.php (5)
  • Sender (17-271)
  • to (40-44)
  • subject (67-71)
  • template (105-132)
  • send (137-197)
src/Cms/Repositories/DatabaseEmailVerificationTokenRepository.php (6)
  • __construct (29-32)
  • deleteByUserId (90-96)
  • create (37-54)
  • findByToken (59-67)
  • deleteByToken (101-107)
  • deleteExpired (112-120)
src/Cms/Repositories/IEmailVerificationTokenRepository.php (5)
  • deleteByUserId (46-46)
  • create (22-22)
  • findByToken (30-30)
  • deleteByToken (54-54)
  • deleteExpired (61-61)
src/Cms/Models/User.php (3)
  • isEmailVerified (203-206)
  • setEmailVerified (211-215)
  • getUsername (78-81)
src/Cms/Email/helpers.php (1)
  • email (98-101)
resources/app/Initializers/RegistrationInitializer.php (7)
src/Cms/Auth/EmailVerificationManager.php (2)
  • EmailVerificationManager (21-247)
  • setTokenExpirationMinutes (57-61)
src/Cms/Auth/Filters/MemberAuthenticationFilter.php (1)
  • MemberAuthenticationFilter (19-105)
src/Cms/Auth/AuthManager.php (1)
  • AuthManager (17-321)
src/Cms/Auth/PasswordHasher.php (1)
  • PasswordHasher (13-200)
src/Cms/Repositories/DatabaseEmailVerificationTokenRepository.php (1)
  • DatabaseEmailVerificationTokenRepository (19-135)
src/Cms/Repositories/DatabaseUserRepository.php (1)
  • DatabaseUserRepository (19-263)
src/Cms/Services/Member/RegistrationService.php (1)
  • RegistrationService (21-214)
🪛 PHPMD (2.15.0)
src/Cms/Controllers/Member/Registration.php

66-66: Avoid unused parameters such as '$parameters'. (undefined)

(UnusedFormalParameter)


107-107: Avoid unused parameters such as '$parameters'. (undefined)

(UnusedFormalParameter)


128-128: Avoid unused local variables such as '$user'. (undefined)

(UnusedLocalVariable)


162-162: Avoid unused parameters such as '$parameters'. (undefined)

(UnusedFormalParameter)


183-183: Avoid unused parameters such as '$parameters'. (undefined)

(UnusedFormalParameter)


268-268: Avoid unused parameters such as '$parameters'. (undefined)

(UnusedFormalParameter)

resources/app/Initializers/RegistrationInitializer.php

29-29: Avoid unused parameters such as '$argv'. (undefined)

(UnusedFormalParameter)

🔇 Additional comments (8)
resources/app/Initializers/RegistrationInitializer.php (1)

74-76: LGTM! URL normalization properly implemented.

The verification URL construction now correctly handles both leading and trailing slashes, ensuring exactly one slash between the site URL and verification path. This addresses the previous concern about malformed links.

src/Cms/Controllers/Member/Registration.php (3)

38-59: LGTM! Proper dependency validation.

The constructor validates that required services are available in the Registry and throws a clear RuntimeException if not. This fail-fast approach prevents cryptic errors downstream.


289-295: Excellent enumeration prevention.

The rate-limiting response deliberately returns a generic success message to prevent attackers from using throttling behavior to determine whether an email exists in the system. This is a solid security practice.


302-310: Good security posture: consistent messaging prevents enumeration.

The catch block returns the same generic success message as the happy path. This prevents information leakage about which emails are registered, even when errors occur.

src/Cms/Auth/EmailVerificationManager.php (4)

152-155: LGTM! Properly guarded status transition.

The conditional check ensures only STATUS_INACTIVE users are activated upon email verification. This prevents suspended or banned users from self-reactivating via a verification link, preserving moderation controls.


77-79: Secure token generation.

Using random_bytes(32) with bin2hex produces a cryptographically secure 64-character token, and hashing it with SHA-256 before storage follows best practices for token management.


177-181: Good enumeration prevention.

Returning true even when the user is not found prevents attackers from using this method to discover which email addresses are registered in the system.


142-147: Idempotent verification handling.

Checking if the user is already verified and cleaning up the token regardless ensures the operation is idempotent, which is good for handling duplicate verification attempts gracefully.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (2)
resources/views/layouts/auth.php (1)

9-9: Verify that $theme is validated to prevent CSS injection.

Same concern as in resources/views/layouts/default.php Line 22: ensure $theme is validated against an allowlist before use in URLs.

resources/views/layouts/member.php (1)

12-12: Verify that $theme is validated to prevent CSS injection.

Same concern as in resources/views/layouts/default.php Line 22: ensure $theme is validated against an allowlist before use in URLs.

🧹 Nitpick comments (14)
resources/database/migrate/20250111000000_create_users_table.php (1)

41-48: Enforce uniqueness on reset tokens

Password reset lookups typically address records by token; allowing duplicates risks ambiguous matches. Please make the token index unique so each token maps to exactly one row.

-			->addIndex( [ 'token' ] )
+			->addIndex( [ 'token' ], [ 'unique' => true ] )
tests/Cms/Cli/Commands/Install/InstallCommandTest.php (1)

109-119: Consider adding functional tests for copyMigrations.

The current test only verifies the method exists and is private, but doesn't test the actual copying behavior. Consider adding tests for:

  • Successful migration file copying
  • Handling of missing source directory
  • Skipping existing destination files
  • Error handling when copy fails

Example test structure using vfsStream:

public function testCopyMigrationsCopiesToCorrectLocation(): void
{
    // Set up virtual filesystem with source migrations
    $root = vfsStream::setup('test');
    $componentPath = vfsStream::url('test/component/resources/database/migrate');
    $projectPath = vfsStream::url('test/project');
    
    // Create source migration file
    mkdir($componentPath, 0755, true);
    file_put_contents($componentPath . '/001_create_users.php', '<?php // migration');
    
    // Execute copyMigrations via reflection
    // Assert file copied to project/db/migrate/
    // Assert appropriate messages logged
}
resources/views/layouts/admin.php (2)

9-9: Consider adding null coalescing for defensive coding.

While the ViewDataInitializer should provide the $theme variable, adding a null coalescing operator would make the code more defensive against initialization issues.

Apply this diff:

-	<link href="https://cdn.jsdelivr.net/npm/bootswatch@5.3.7/dist/<?= htmlspecialchars($theme) ?>/bootstrap.min.css" rel="stylesheet">
+	<link href="https://cdn.jsdelivr.net/npm/bootswatch@5.3.7/dist/<?= htmlspecialchars($theme ?? 'sandstone') ?>/bootstrap.min.css" rel="stylesheet">

22-22: Consider adding null coalescing for consistency.

Similar to Line 6 which uses $Title ?? 'Admin', consider adding a fallback for $siteName.

Apply this diff:

-				<?= htmlspecialchars($siteName) ?> Admin
+				<?= htmlspecialchars($siteName ?? 'Neuron CMS') ?> Admin
src/Cms/Controllers/Home.php (1)

28-35: Consider using an interface or type hint instead of method_exists.

While the defensive checks are safe, using method_exists on a service suggests an unstable contract. Consider defining an interface with isRegistrationEnabled() or using type hints to eliminate the runtime check.

Example refactor:

-	// Check if registration is enabled
-	$registrationEnabled = false;
-	$registrationService = \Neuron\Patterns\Registry::getInstance()->get( 'RegistrationService' );
-
-	if( $registrationService && method_exists( $registrationService, 'isRegistrationEnabled' ) )
-	{
-		$registrationEnabled = $registrationService->isRegistrationEnabled();
-	}
+	// Check if registration is enabled
+	$registrationEnabled = false;
+	$registrationService = \Neuron\Patterns\Registry::getInstance()->get( 'RegistrationService' );
+
+	if( $registrationService instanceof \Neuron\Cms\Services\Member\RegistrationService )
+	{
+		$registrationEnabled = $registrationService->isRegistrationEnabled();
+	}
resources/views/layouts/member.php (1)

37-37: Consider extracting the inline SVG fallback to a helper function.

The onerror attribute contains a very long inline data URL SVG (300+ characters), making the template harder to read and maintain. Consider extracting this to a helper function like default_avatar_svg() or storing it as a constant.

Example refactor in src/Cms/View/helpers.php:

function default_avatar_data_url(): string
{
    return "data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%2232%22 height=%2232%22 fill=%22%23ffffff%22 viewBox=%220 0 16 16%22><circle cx=%228%22 cy=%228%22 r=%228%22 fill=%22%236c757d%22/><path d=%22M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0zm4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4zm-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10c-2.29 0-3.516.68-4.168 1.332-.678.678-.83 1.418-.832 1.664h10z%22 fill=%22%23ffffff%22/></svg>";
}

Then use in template:

<img src="<?= gravatar_url($currentUser->getEmail(), 32) ?>" 
     class="rounded-circle" width="32" height="32" 
     alt="<?= htmlspecialchars($currentUser->getUsername()) ?>" 
     onerror="this.onerror=null; this.src='<?= default_avatar_data_url() ?>';">
resources/config/neuron.yaml.example (1)

16-17: Consider documenting cache behavior for development environments.

The example shows enabled: true which is appropriate for production. However, developers copying this file might benefit from a comment noting that cache should typically be disabled during development for immediate feedback on view changes.

Apply this diff to add a helpful comment:

 cache:
-  enabled: true  # Enable view caching
+  enabled: true  # Enable view caching (set to false in development for immediate view updates)
   storage: file  # Cache storage type
resources/views/member/registration/register.php (1)

15-81: LGTM! Consider adding server-side validation notes.

The registration form is well-structured with proper CSRF protection, HTML5 validation, and good UX. The client-side validation attributes (pattern, minlength, required) provide immediate feedback to users.

A few observations:

  • The password helper text on Line 58 mentions "uppercase, lowercase, and numbers" but only minlength="8" is enforced client-side. This is acceptable since server-side validation should handle the full password requirements.
  • Consider adding maxlength attributes to prevent excessively long inputs that could cause issues during processing or storage.

Optionally add maxlength constraints:

 						<input
 							type="text"
 							id="username"
 							name="username"
 							class="form-control"
 							placeholder="Choose a username"
 							required
 							autofocus
 							pattern="[a-zA-Z0-9_]+"
 							minlength="3"
 							maxlength="50"
+							title="3-50 characters, letters, numbers, and underscores only"
 						>
 						<input
 							type="email"
 							id="email"
 							name="email"
 							class="form-control"
 							placeholder="your.email@example.com"
 							required
+							maxlength="255"
 						>
tests/Cms/Models/EmailVerificationTokenTest.php (1)

25-41: Consider edge case in expiration time calculation test.

The expiration time calculation test works correctly for the 30-minute case, but the calculation on lines 37-38 only accounts for hours and minutes:

$minutesDiff = ( $diff->h * 60 ) + $diff->i;

This could produce incorrect results if the expiration spans multiple days (e.g., if someone configures expiration > 1440 minutes). While this is unlikely given typical use cases (60 minutes default), consider making the test more robust.

Apply this diff to handle multi-day differences:

 		// Calculate the difference in minutes
 		$diff = $createdAt->diff( $expiresAt );
-		$minutesDiff = ( $diff->h * 60 ) + $diff->i;
+		$minutesDiff = ( $diff->days * 24 * 60 ) + ( $diff->h * 60 ) + $diff->i;
 
 		$this->assertEquals( $expirationMinutes, $minutesDiff );
src/Cms/Controllers/Member/Dashboard.php (1)

43-45: Consider extracting CSRF token generation to a helper method.

The CSRF token generation pattern (instantiating CsrfTokenManager, calling getToken(), and storing in Registry) is repeated across multiple controllers (Dashboard and Profile). Consider extracting this to a protected method in the Content base class to reduce duplication.

For example, add to the Content base class:

/**
 * Generate and store CSRF token in Registry
 */
protected function ensureCsrfToken(): string
{
	$csrfManager = new CsrfTokenManager( $this->getSessionManager() );
	$token = $csrfManager->getToken();
	Registry::getInstance()->set( 'Auth.CsrfToken', $token );
	return $token;
}

Then simplify the usage:

-		// Generate CSRF token and store in Registry
-		$csrfManager = new CsrfTokenManager( $this->getSessionManager() );
-		Registry::getInstance()->set( 'Auth.CsrfToken', $csrfManager->getToken() );
+		$this->ensureCsrfToken();
src/Cms/Controllers/Member/Profile.php (1)

87-134: Consider adding password strength validation.

The update() method correctly handles the profile update flow, but when a new password is provided, it only checks confirmation matching. Consider validating the new password strength using PasswordHasher::meetsRequirements() before calling the updater service.

Apply this diff to add password strength validation:

 		// Validate password change if requested
 		if( !empty( $newPassword ) )
 		{
 			// Verify current password
 			if( empty( $currentPassword ) || !$this->_hasher->verify( $currentPassword, $user->getPasswordHash() ) )
 			{
 				$this->redirect( 'member_profile', [], ['error', 'Current password is incorrect'] );
 			}
 
 			// Validate new password matches confirmation
 			if( $newPassword !== $confirmPassword )
 			{
 				$this->redirect( 'member_profile', [], ['error', 'New passwords do not match'] );
 			}
+
+			// Validate password strength
+			if( !$this->_hasher->meetsRequirements( $newPassword ) )
+			{
+				$errors = $this->_hasher->getValidationErrors( $newPassword );
+				$this->redirect( 'member_profile', [], ['error', implode( ', ', $errors )] );
+			}
 		}
tests/Cms/Repositories/DatabaseEmailVerificationTokenRepositoryTest.php (1)

44-79: Enable SQLite foreign-key enforcement in tests.

Without PRAGMA foreign_keys = ON, SQLite quietly ignores the ON DELETE CASCADE in your schema, so these tests won’t surface regressions where referential cleanup breaks. Flip the pragma right after you grab the PDO handle to keep the test environment aligned with production.

Apply this diff:

 		$property->setAccessible( true );
 		$this->pdo = $property->getValue( $this->repository );
+
+		// Ensure SQLite enforces foreign key constraints like production
+		$this->pdo->exec( 'PRAGMA foreign_keys = ON' );
src/Cms/Services/Member/RegistrationService.php (1)

121-129: Log the verification mail failure before swallowing it

We swallow the exception from sendVerificationEmail() and the comment says “Log error” but no logging happens, so support can’t see when verification delivery breaks. Please log (e.g., Log::error(...)) before returning so we retain failure visibility while still keeping registration non-fatal.

 			catch( Exception $e )
 			{
-				// Log error but don't fail registration
-				// User can request resend later
+				// Log error but don't fail registration. User can request resend later.
+				Log::error( sprintf(
+					'Failed to send verification email for user %s: %s',
+					$user->getEmail(),
+					$e->getMessage()
+				) );
 			}
tests/Cms/Auth/EmailVerificationManagerTest.php (1)

180-187: Update expectation to match guarded status change

Once verifyEmail() stops forcing every account to ACTIVE, this callback should assert that we only flip status when it was STATUS_INACTIVE (and otherwise preserve the prior status). Please tweak the callback assertion after adjusting the implementation.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7cab47d and 88fd7d9.

📒 Files selected for processing (53)
  • examples/config/config.yaml (0 hunks)
  • examples/config/neuron.yaml (1 hunks)
  • readme.md (5 hunks)
  • resources/app/Initializers/RegistrationInitializer.php (1 hunks)
  • resources/app/Initializers/ViewDataInitializer.php (1 hunks)
  • resources/config/auth.yaml (1 hunks)
  • resources/config/database.yaml.example (1 hunks)
  • resources/config/maintenance.yaml.example (1 hunks)
  • resources/config/neuron.yaml (1 hunks)
  • resources/config/neuron.yaml.example (1 hunks)
  • resources/config/routes.yaml (1 hunks)
  • resources/database/migrate/20250111000000_create_users_table.php (1 hunks)
  • resources/database/migrate/20250112000000_create_email_verification_tokens_table.php (1 hunks)
  • resources/views/emails/email-verification.php (1 hunks)
  • resources/views/home/index.php (1 hunks)
  • resources/views/layouts/admin.php (2 hunks)
  • resources/views/layouts/auth.php (2 hunks)
  • resources/views/layouts/default.php (4 hunks)
  • resources/views/layouts/member.php (1 hunks)
  • resources/views/member/dashboard/index.php (1 hunks)
  • resources/views/member/profile/edit.php (1 hunks)
  • resources/views/member/registration/email-verified.php (1 hunks)
  • resources/views/member/registration/register.php (1 hunks)
  • resources/views/member/registration/registration-disabled.php (1 hunks)
  • resources/views/member/registration/verify-email-sent.php (1 hunks)
  • specs/authentication-implementation-plan.md (4 hunks)
  • src/Cms/Auth/EmailVerificationManager.php (1 hunks)
  • src/Cms/Auth/Filters/MemberAuthenticationFilter.php (1 hunks)
  • src/Cms/Cli/Commands/Install/InstallCommand.php (7 hunks)
  • src/Cms/Cli/Commands/Maintenance/EnableCommand.php (1 hunks)
  • src/Cms/Cli/Commands/Queue/InstallCommand.php (3 hunks)
  • src/Cms/Controllers/Home.php (1 hunks)
  • src/Cms/Controllers/Member/Dashboard.php (1 hunks)
  • src/Cms/Controllers/Member/Profile.php (1 hunks)
  • src/Cms/Controllers/Member/Registration.php (1 hunks)
  • src/Cms/Dtos/RegisterUserDto.yaml (1 hunks)
  • src/Cms/Maintenance/README.md (2 hunks)
  • src/Cms/Models/EmailVerificationToken.php (1 hunks)
  • src/Cms/Repositories/DatabaseEmailVerificationTokenRepository.php (1 hunks)
  • src/Cms/Repositories/IEmailVerificationTokenRepository.php (1 hunks)
  • src/Cms/Services/Member/RegistrationService.php (1 hunks)
  • tests/BootstrapTest.php (3 hunks)
  • tests/Cms/Auth/EmailVerificationManagerTest.php (1 hunks)
  • tests/Cms/Auth/MemberAuthenticationFilterTest.php (1 hunks)
  • tests/Cms/BlogControllerTest.php (1 hunks)
  • tests/Cms/Cli/Commands/Install/InstallCommandTest.php (1 hunks)
  • tests/Cms/Cli/Commands/User/CreateCommandTest.php (1 hunks)
  • tests/Cms/Maintenance/MaintenanceConfigTest.php (5 hunks)
  • tests/Cms/Models/EmailVerificationTokenTest.php (1 hunks)
  • tests/Cms/Repositories/DatabaseEmailVerificationTokenRepositoryTest.php (1 hunks)
  • tests/Cms/Services/RegistrationServiceTest.php (1 hunks)
  • tests/bootstrap.php (1 hunks)
  • versionlog.md (1 hunks)
💤 Files with no reviewable changes (1)
  • examples/config/config.yaml
🧰 Additional context used
🧬 Code graph analysis (28)
resources/views/member/registration/verify-email-sent.php (2)
src/Cms/View/helpers.php (1)
  • route_path (70-79)
src/Cms/Auth/helpers.php (1)
  • csrf_field (122-126)
resources/views/member/dashboard/index.php (3)
src/Cms/View/helpers.php (1)
  • route_path (70-79)
src/Cms/Models/User.php (3)
  • getUsername (78-81)
  • isEmailVerified (203-206)
  • getLastLoginAt (379-382)
src/Cms/Models/EmailVerificationToken.php (1)
  • getCreatedAt (91-94)
src/Cms/Controllers/Member/Registration.php (5)
src/Cms/Controllers/Content.php (3)
  • Content (57-279)
  • getSessionManager (219-227)
  • redirect (237-247)
src/Cms/Auth/CsrfTokenManager.php (2)
  • CsrfTokenManager (13-69)
  • validate (49-60)
src/Cms/Auth/EmailVerificationManager.php (4)
  • EmailVerificationManager (21-243)
  • __construct (39-52)
  • verifyEmail (123-160)
  • resendVerification (169-186)
src/Cms/Services/Member/RegistrationService.php (4)
  • RegistrationService (21-214)
  • __construct (38-51)
  • isRegistrationEnabled (58-61)
  • register (73-139)
src/Cms/Auth/SessionManager.php (3)
  • get (108-112)
  • set (99-103)
  • getFlash (144-150)
resources/views/member/registration/email-verified.php (1)
src/Cms/View/helpers.php (1)
  • route_path (70-79)
resources/app/Initializers/RegistrationInitializer.php (6)
src/Cms/Auth/EmailVerificationManager.php (2)
  • EmailVerificationManager (21-243)
  • setTokenExpirationMinutes (57-61)
src/Cms/Auth/Filters/MemberAuthenticationFilter.php (1)
  • MemberAuthenticationFilter (19-105)
src/Cms/Auth/PasswordHasher.php (1)
  • PasswordHasher (13-200)
src/Cms/Repositories/DatabaseEmailVerificationTokenRepository.php (1)
  • DatabaseEmailVerificationTokenRepository (19-135)
src/Cms/Repositories/DatabaseUserRepository.php (1)
  • DatabaseUserRepository (19-263)
src/Cms/Services/Member/RegistrationService.php (1)
  • RegistrationService (21-214)
src/Cms/Controllers/Member/Profile.php (6)
src/Cms/Controllers/Content.php (3)
  • Content (57-279)
  • getSessionManager (219-227)
  • redirect (237-247)
src/Cms/Repositories/DatabaseUserRepository.php (1)
  • DatabaseUserRepository (19-263)
src/Cms/Auth/PasswordHasher.php (1)
  • PasswordHasher (13-200)
src/Cms/Auth/CsrfTokenManager.php (1)
  • CsrfTokenManager (13-69)
src/Cms/Auth/SessionManager.php (3)
  • get (108-112)
  • set (99-103)
  • getFlash (144-150)
src/Cms/Models/User.php (3)
  • getPasswordHash (112-115)
  • getUsername (78-81)
  • getRole (129-132)
resources/database/migrate/20250112000000_create_email_verification_tokens_table.php (3)
resources/database/migrate/20250111000000_create_users_table.php (1)
  • change (13-49)
src/Cms/Repositories/DatabaseEmailVerificationTokenRepository.php (1)
  • create (37-54)
src/Cms/Repositories/IEmailVerificationTokenRepository.php (1)
  • create (22-22)
resources/app/Initializers/ViewDataInitializer.php (1)
resources/app/Initializers/RegistrationInitializer.php (1)
  • run (29-127)
resources/views/home/index.php (1)
src/Cms/View/helpers.php (1)
  • route_path (70-79)
tests/Cms/Cli/Commands/Install/InstallCommandTest.php (1)
src/Cms/Cli/Commands/Install/InstallCommand.php (1)
  • InstallCommand (18-1021)
tests/Cms/Models/EmailVerificationTokenTest.php (1)
src/Cms/Models/EmailVerificationToken.php (14)
  • EmailVerificationToken (14-178)
  • getUserId (57-60)
  • getToken (74-77)
  • getCreatedAt (91-94)
  • getExpiresAt (108-111)
  • setId (48-52)
  • getId (40-43)
  • setUserId (65-69)
  • setToken (82-86)
  • setCreatedAt (99-103)
  • setExpiresAt (116-120)
  • isExpired (125-128)
  • toArray (168-177)
  • fromArray (133-163)
src/Cms/Repositories/IEmailVerificationTokenRepository.php (2)
src/Cms/Models/EmailVerificationToken.php (1)
  • EmailVerificationToken (14-178)
src/Cms/Repositories/DatabaseEmailVerificationTokenRepository.php (6)
  • create (37-54)
  • findByToken (59-67)
  • findByUserId (72-85)
  • deleteByUserId (90-96)
  • deleteByToken (101-107)
  • deleteExpired (112-120)
tests/Cms/Auth/MemberAuthenticationFilterTest.php (3)
src/Cms/Auth/AuthManager.php (1)
  • AuthManager (17-321)
src/Cms/Auth/Filters/MemberAuthenticationFilter.php (4)
  • MemberAuthenticationFilter (19-105)
  • setLoginUrl (82-86)
  • setVerifyEmailUrl (91-95)
  • setRequireEmailVerification (100-104)
src/Cms/Models/User.php (2)
  • setEmailVerified (211-215)
  • isEmailVerified (203-206)
src/Cms/Repositories/DatabaseEmailVerificationTokenRepository.php (3)
src/Cms/Database/ConnectionFactory.php (2)
  • ConnectionFactory (17-88)
  • createFromSettings (26-36)
src/Cms/Models/EmailVerificationToken.php (8)
  • EmailVerificationToken (14-178)
  • __construct (29-35)
  • getUserId (57-60)
  • getToken (74-77)
  • getCreatedAt (91-94)
  • getExpiresAt (108-111)
  • setId (48-52)
  • fromArray (133-163)
src/Cms/Repositories/IEmailVerificationTokenRepository.php (6)
  • create (22-22)
  • findByToken (30-30)
  • findByUserId (38-38)
  • deleteByUserId (46-46)
  • deleteByToken (54-54)
  • deleteExpired (61-61)
src/Cms/Controllers/Home.php (2)
src/Cms/Services/Member/RegistrationService.php (1)
  • isRegistrationEnabled (58-61)
src/Cms/Controllers/Content.php (3)
  • getTitle (138-141)
  • getName (120-123)
  • getDescription (156-159)
tests/Cms/Auth/EmailVerificationManagerTest.php (3)
src/Cms/Auth/EmailVerificationManager.php (7)
  • EmailVerificationManager (21-243)
  • setTokenExpirationMinutes (57-61)
  • sendVerificationEmail (72-94)
  • validateToken (102-114)
  • verifyEmail (123-160)
  • resendVerification (169-186)
  • cleanupExpiredTokens (193-196)
src/Cms/Models/EmailVerificationToken.php (4)
  • EmailVerificationToken (14-178)
  • setId (48-52)
  • getId (40-43)
  • getUserId (57-60)
src/Cms/Models/User.php (3)
  • setUsername (86-90)
  • setEmailVerified (211-215)
  • isEmailVerified (203-206)
resources/views/member/registration/registration-disabled.php (1)
src/Cms/View/helpers.php (1)
  • route_path (70-79)
src/Cms/Auth/Filters/MemberAuthenticationFilter.php (3)
src/Cms/Auth/AuthManager.php (1)
  • AuthManager (17-321)
src/Cms/Models/User.php (2)
  • isEmailVerified (203-206)
  • getRole (129-132)
src/Cms/Auth/SessionManager.php (1)
  • set (99-103)
src/Cms/Services/Member/RegistrationService.php (4)
src/Cms/Auth/EmailVerificationManager.php (3)
  • EmailVerificationManager (21-243)
  • __construct (39-52)
  • sendVerificationEmail (72-94)
src/Cms/Auth/PasswordHasher.php (4)
  • PasswordHasher (13-200)
  • hash (24-30)
  • meetsRequirements (53-86)
  • getValidationErrors (91-121)
src/Cms/Events/UserCreatedEvent.php (1)
  • UserCreatedEvent (13-23)
src/Cms/Models/User.php (4)
  • setUsername (86-90)
  • setPasswordHash (120-124)
  • setRole (137-141)
  • setEmailVerified (211-215)
resources/views/member/registration/register.php (2)
src/Cms/View/helpers.php (1)
  • route_path (70-79)
src/Cms/Auth/helpers.php (1)
  • csrf_field (122-126)
tests/Cms/Repositories/DatabaseEmailVerificationTokenRepositoryTest.php (3)
src/Cms/Repositories/DatabaseEmailVerificationTokenRepository.php (7)
  • DatabaseEmailVerificationTokenRepository (19-135)
  • create (37-54)
  • findByToken (59-67)
  • findByUserId (72-85)
  • deleteByUserId (90-96)
  • deleteByToken (101-107)
  • deleteExpired (112-120)
src/Cms/Models/EmailVerificationToken.php (10)
  • EmailVerificationToken (14-178)
  • getId (40-43)
  • getUserId (57-60)
  • getToken (74-77)
  • setUserId (65-69)
  • setToken (82-86)
  • setCreatedAt (99-103)
  • setExpiresAt (116-120)
  • getCreatedAt (91-94)
  • getExpiresAt (108-111)
src/Cms/Repositories/IEmailVerificationTokenRepository.php (6)
  • create (22-22)
  • findByToken (30-30)
  • findByUserId (38-38)
  • deleteByUserId (46-46)
  • deleteByToken (54-54)
  • deleteExpired (61-61)
src/Cms/Auth/EmailVerificationManager.php (5)
src/Cms/Models/EmailVerificationToken.php (5)
  • EmailVerificationToken (14-178)
  • __construct (29-35)
  • getId (40-43)
  • isExpired (125-128)
  • getUserId (57-60)
src/Cms/Services/Email/Sender.php (5)
  • Sender (17-271)
  • to (40-44)
  • subject (67-71)
  • template (105-132)
  • send (137-197)
src/Cms/Repositories/DatabaseEmailVerificationTokenRepository.php (6)
  • __construct (29-32)
  • deleteByUserId (90-96)
  • create (37-54)
  • findByToken (59-67)
  • deleteByToken (101-107)
  • deleteExpired (112-120)
src/Cms/Repositories/IEmailVerificationTokenRepository.php (5)
  • deleteByUserId (46-46)
  • create (22-22)
  • findByToken (30-30)
  • deleteByToken (54-54)
  • deleteExpired (61-61)
src/Cms/Models/User.php (3)
  • isEmailVerified (203-206)
  • setEmailVerified (211-215)
  • getUsername (78-81)
resources/views/layouts/member.php (3)
src/Cms/View/helpers.php (2)
  • route_path (70-79)
  • gravatar_url (23-27)
src/Cms/Models/User.php (1)
  • getUsername (78-81)
src/Cms/Auth/helpers.php (1)
  • csrf_field (122-126)
resources/views/member/profile/edit.php (3)
src/Cms/View/helpers.php (1)
  • route_path (70-79)
src/Cms/Auth/helpers.php (1)
  • csrf_field (122-126)
src/Cms/Models/User.php (2)
  • getUsername (78-81)
  • getTimezone (396-399)
src/Cms/Models/EmailVerificationToken.php (2)
src/Cms/Auth/EmailVerificationManager.php (1)
  • __construct (39-52)
src/Cms/Repositories/DatabaseEmailVerificationTokenRepository.php (1)
  • __construct (29-32)
src/Cms/Controllers/Member/Dashboard.php (4)
src/Cms/Controllers/Content.php (2)
  • Content (57-279)
  • getSessionManager (219-227)
src/Cms/Auth/CsrfTokenManager.php (1)
  • CsrfTokenManager (13-69)
src/Cms/Auth/SessionManager.php (2)
  • get (108-112)
  • set (99-103)
src/Cms/Models/User.php (1)
  • getUsername (78-81)
tests/Cms/Services/RegistrationServiceTest.php (4)
src/Cms/Auth/EmailVerificationManager.php (1)
  • EmailVerificationManager (21-243)
src/Cms/Auth/PasswordHasher.php (1)
  • PasswordHasher (13-200)
src/Cms/Services/Member/RegistrationService.php (3)
  • RegistrationService (21-214)
  • isRegistrationEnabled (58-61)
  • register (73-139)
src/Cms/Models/User.php (3)
  • getUsername (78-81)
  • getRole (129-132)
  • isEmailVerified (203-206)
resources/database/migrate/20250111000000_create_users_table.php (2)
resources/database/migrate/20250112000000_create_email_verification_tokens_table.php (1)
  • change (13-26)
src/Cms/Repositories/DatabaseEmailVerificationTokenRepository.php (1)
  • create (37-54)
🪛 PHPMD (2.15.0)
resources/app/Initializers/RegistrationInitializer.php

29-29: Avoid unused parameters such as '$argv'. (undefined)

(UnusedFormalParameter)

src/Cms/Controllers/Member/Profile.php

48-48: Avoid unused parameters such as '$parameters'. (undefined)

(UnusedFormalParameter)


87-87: Avoid unused parameters such as '$parameters'. (undefined)

(UnusedFormalParameter)

resources/app/Initializers/ViewDataInitializer.php

37-37: Avoid unused parameters such as '$argv'. (undefined)

(UnusedFormalParameter)

tests/Cms/Auth/MemberAuthenticationFilterTest.php

129-129: Avoid unused local variables such as '$filter'. (undefined)

(UnusedLocalVariable)

src/Cms/Controllers/Member/Dashboard.php

33-33: Avoid unused parameters such as '$parameters'. (undefined)

(UnusedFormalParameter)

🔇 Additional comments (53)
tests/Cms/Cli/Commands/User/CreateCommandTest.php (1)

28-31: Config fixture rename looks good

The virtual config now targets neuron.yaml, keeping the test aligned with the new default filename.

resources/config/database.yaml.example (1)

4-4: Documentation note updated appropriately

The copy instruction now references neuron.yaml, matching the new configuration layout.

resources/config/maintenance.yaml.example (1)

4-4: Maintenance config guidance stays in sync

The rename instruction now points maintainers to neuron.yaml, matching the rest of the docs.

src/Cms/Cli/Commands/Queue/InstallCommand.php (1)

96-110: Queue install messaging aligned with neuron.yaml

All user-facing strings and config lookups now reference config/neuron.yaml, keeping the command in step with the renamed configuration file.

Also applies to: 170-175, 291-330

src/Cms/Dtos/RegisterUserDto.yaml (1)

1-28: Confirm password confirmation enforcement

I don’t see a validator ensuring password_confirmation matches password. Please confirm this equality check exists elsewhere in the registration workflow or add a cross-field validator so mismatched passwords are rejected.

resources/views/member/profile/edit.php (1)

1-114: Well-structured profile form

The view covers success/error feedback, escapes user data, includes CSRF protection, and wires all expected fields for profile and password updates—looks great.

readme.md (1)

71-104: Docs stay in lockstep with neuron.yaml rename

The installer checklist, project structure, and configuration sections now point to neuron.yaml and mention the new initializer, keeping the README accurate.

Also applies to: 160-172, 270-282

resources/app/Initializers/ViewDataInitializer.php (1)

37-73: LGTM! Well-designed initializer with sensible defaults.

The lazy evaluation pattern using closures is appropriate for dynamic values like currentYear and isAuthenticated. All shared data entries have sensible fallback values, ensuring views won't break if Registry entries are missing.

Note: The static analysis warning about unused $argv is a false positive—it's required by the IRunnable interface signature.

src/Cms/Cli/Commands/Install/InstallCommand.php (4)

167-169: LGTM! Member directories added appropriately.

The new member-related view directories (/resources/views/member, /resources/views/member/dashboard, /resources/views/member/profile) align with the PR's member portal functionality.


220-220: LGTM! Member views included in publishing.

The member directory is correctly added to the view directories list for publishing.


688-688: LGTM! Config file renamed to neuron.yaml.

The configuration file rename from config.yaml to neuron.yaml is consistently applied throughout the installer.

Also applies to: 731-731


783-840: LGTM! Well-implemented migration copying logic.

The new copyMigrations() method properly:

  • Creates the migrations directory if needed
  • Validates the source directory exists
  • Skips existing files to avoid overwrites
  • Provides clear feedback messages
  • Handles errors gracefully

This is a cleaner approach than generating migrations from templates.

src/Cms/Cli/Commands/Maintenance/EnableCommand.php (1)

171-171: LGTM! Config file updated to neuron.yaml.

The configuration file reference is correctly updated, consistent with the project-wide rename.

versionlog.md (1)

5-6: LGTM! Changelog entries accurately reflect the changes.

The new entries clearly document the timezone support and configuration file rename.

tests/BootstrapTest.php (1)

47-48: LGTM! Test updated for neuron.yaml configuration.

The test file references are correctly updated to use neuron.yaml while maintaining the same test coverage and logic.

Also applies to: 64-65, 84-84

tests/bootstrap.php (1)

11-20: LGTM! Good test isolation with ViewDataProvider.

Initializing the ViewDataProvider in the test bootstrap ensures tests have predictable, isolated view data. This mirrors the production ViewDataInitializer behavior and prevents tests from depending on external configuration.

tests/Cms/BlogControllerTest.php (1)

71-78: LGTM! ViewDataProvider initialization aligns with view requirements.

The test setup properly initializes shared view data that templates expect, ensuring tests accurately reflect the runtime environment.

resources/config/routes.yaml (1)

254-304: LGTM! Member routes are well-structured.

The new member-related routes follow RESTful conventions with appropriate HTTP methods and filters. Public registration/verification routes are correctly unprotected, while dashboard and profile routes are properly protected with the member filter.

resources/views/layouts/default.php (1)

22-22: No theme validation required — theme is safely constrained to static configuration values.

The $theme variable originates from a YAML configuration source (Neuron\Data\Setting\Source\Yaml), not user input. The configuration file explicitly defines valid theme values (admin: vapor, subscriber: sandstone, guest: sandstone), and no user-editable theme setting exists. Since the theme is limited to these predefined, legitimate Bootswatch theme names from static configuration, CSS injection is not possible. The htmlspecialchars() call is appropriate defensive encoding for the template context.

Likely an incorrect or invalid review comment.

resources/views/member/registration/registration-disabled.php (1)

1-26: LGTM!

This is a clean, static informational view. The navigation links properly use the route_path helper, and there are no security concerns with this implementation.

resources/config/neuron.yaml.example (1)

50-78: Secure defaults for member registration.

The member configuration uses secure defaults:

  • Default role is 'subscriber' (not 'admin')
  • Email verification is required
  • Token expiration of 60 minutes is reasonable

These settings follow security best practices for user registration systems.

resources/views/member/dashboard/index.php (1)

1-64: LGTM! Proper output escaping and null handling.

The dashboard view demonstrates good security practices:

  • All user data is properly escaped with htmlspecialchars() (lines 7, 35-36)
  • Null checks are implemented for optional date fields (lines 56, 58)
  • Framework helpers (route_path) are used consistently
examples/config/neuron.yaml (1)

38-57: LGTM! Secure member configuration defaults.

The member registration configuration follows security best practices:

  • Default role is 'subscriber' (line 45)
  • Email verification is required (line 49)
  • Reasonable token expiration of 60 minutes (line 57)
resources/views/home/index.php (2)

12-17: LGTM! Conditional Sign Up button with proper configuration check.

The conditional rendering of the Sign Up button based on $RegistrationEnabled properly integrates with the member registration configuration. This allows the feature to be easily enabled/disabled via config.


23-40: Content updates align with new member portal features.

The updated card titles and descriptions (Blog, Member Portal, Admin Portal) accurately reflect the new functionality introduced in this PR.

resources/config/neuron.yaml (1)

49-69: LGTM! Secure member registration configuration.

The member configuration block implements security best practices:

  • Registration can be toggled via registration_enabled flag
  • Default role is 'subscriber' (not privileged)
  • Email verification is required before access
  • Token expiration of 60 minutes prevents indefinite token validity
specs/authentication-implementation-plan.md (1)

620-621: LGTM! Documentation updated to reflect config file rename.

The references to the configuration file have been properly updated from config.yaml to neuron.yaml throughout the authentication implementation plan, maintaining consistency with the config file rename in this PR.

Also applies to: 947-947, 1169-1169, 1577-1578

resources/config/auth.yaml (1)

42-52: LGTM! Well-documented configuration.

The member portal configuration is well-structured with clear, descriptive comments and sensible default values. The 60-minute token expiration and 60-second resend throttle provide a good balance between security and user experience.

tests/Cms/Models/EmailVerificationTokenTest.php (1)

11-136: Excellent test coverage!

The test suite comprehensively covers all aspects of the EmailVerificationToken model, including constructor initialization, timestamp handling, expiration logic, and serialization. The inclusion of partial data testing (lines 124-136) is particularly good for ensuring robustness.

resources/database/migrate/20250112000000_create_email_verification_tokens_table.php (1)

13-26: Well-structured migration with proper foreign key constraint.

The foreign key with CASCADE delete is appropriate here—when a user is deleted, their verification tokens should be automatically removed. The indexes on user_id, token, and expires_at support efficient lookups and cleanup operations.

resources/views/emails/email-verification.php (1)

1-183: Excellent email template with proper security measures!

The email verification template is well-crafted with:

  • Proper XSS prevention through htmlspecialchars() on all dynamic content
  • Inline CSS for maximum email client compatibility
  • Responsive design with mobile-friendly styling
  • Clear call-to-action with both button and copyable link
  • Professional layout and branding

The template effectively communicates urgency with the expiration notice and provides fallback options for users who have trouble with the button.

resources/views/member/registration/email-verified.php (1)

1-42: LGTM! Clear and user-friendly verification result page.

The view effectively handles both success and failure states with:

  • Appropriate visual feedback using Bootstrap Icons
  • Properly escaped messages with sensible fallbacks
  • Clear action buttons for each state
  • Good UX with contextual next steps
src/Cms/Controllers/Member/Dashboard.php (1)

33-60: LGTM! Well-structured dashboard controller.

The dashboard controller properly:

  • Retrieves the authenticated user with appropriate error handling
  • Generates and stores a CSRF token for form protection
  • Prepares comprehensive view data
  • Renders the appropriate view

The static analysis warning about the unused $parameters can be safely ignored—it's required by the routing framework's method signature convention.

src/Cms/Controllers/Member/Profile.php (2)

48-79: LGTM! Profile edit form properly secured.

The edit() method correctly:

  • Validates authenticated user presence
  • Generates CSRF token for form protection
  • Retrieves timezone data for the form
  • Handles flash messages for user feedback

The static analysis warning about unused $parameters can be ignored—it's required by the routing framework convention.


103-116: Password validation logic is secure and correct.

The password change validation properly:

  • Verifies the current password before allowing changes (prevents unauthorized password changes if session is compromised)
  • Ensures new password matches confirmation
  • Only requires current password when changing password

The flow correctly prevents password changes without proper authentication.

tests/Cms/Auth/MemberAuthenticationFilterTest.php (1)

69-160: Solid coverage of the happy path

Nice job covering the verified-user path and gating the tricky exit() flows. This gives confidence around Registry wiring.

src/Cms/Models/EmailVerificationToken.php (1)

29-176: Token entity looks clean

Constructor defaults, expiry math, and the array mappers all check out.

src/Cms/Repositories/DatabaseEmailVerificationTokenRepository.php (8)

19-32: LGTM!

The constructor properly delegates connection creation to ConnectionFactory, which handles configuration validation and adapter support.


37-54: LGTM!

The create method correctly uses prepared statements, formats timestamps appropriately, and assigns the generated ID to the token before returning.


59-67: LGTM!

The findByToken method correctly uses a prepared statement and appropriately returns null when no token is found.


72-85: LGTM!

The findByUserId method correctly retrieves the most recent token using ORDER BY created_at DESC and handles the case when no token exists.


90-96: LGTM!

The deleteByUserId method correctly deletes all tokens for a user and returns the count of affected rows.


101-107: LGTM!

The deleteByToken method correctly deletes a specific token and returns a boolean indicating whether a token was deleted.


112-120: LGTM!

The deleteExpired method correctly identifies and removes tokens where expires_at is earlier than the current time.


125-134: LGTM!

The mapRowToToken method appropriately delegates row-to-object mapping to EmailVerificationToken::fromArray(), maintaining separation of concerns.

tests/Cms/Services/RegistrationServiceTest.php (8)

24-49: LGTM!

The setUp method properly initializes dependencies with appropriate mocking. Using a real PasswordHasher and Memory-based settings provides good test isolation while maintaining realistic behavior.


51-79: LGTM!

The constructor and registration toggle tests are straightforward and correctly validate the service's ability to check registration status from settings.


81-129: LGTM!

The valid registration test comprehensively verifies the happy path, including proper user status (inactive), email verification flag (false), default role assignment, and verification email trigger.


131-150: LGTM!

The disabled registration test correctly verifies that registration attempts are rejected with an appropriate error message when the feature is disabled.


152-202: LGTM!

The username validation tests comprehensively cover all validation rules: empty, length constraints (3-50 characters), character restrictions (alphanumeric + underscore), and uniqueness checks.


204-234: LGTM!

The email validation tests properly verify format validation and uniqueness checks, with appropriate mock setup for the existing email scenario.


236-249: LGTM!

The password validation tests correctly verify password matching and strength requirements, leveraging the real PasswordHasher for authentic validation behavior.


251-299: LGTM!

The email verification bypass test correctly validates that when verification is not required, users are created as active with verified email status, and no verification email is sent.

ljonesfl and others added 5 commits November 12, 2025 18:30
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
…ation_tokens_table.php

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@ljonesfl ljonesfl merged commit 21245c5 into develop Nov 13, 2025
2 checks passed
@ljonesfl ljonesfl deleted the feature/member-portal branch November 13, 2025 00:55
@coderabbitai coderabbitai bot mentioned this pull request Nov 16, 2025
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.

2 participants