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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,7 @@ coverage*
.env.*.local

var/

# Runtime directories (logs, cache, temporary files)
storage/
resources/storage/
2 changes: 1 addition & 1 deletion config/neuron.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

system:
timezone: UTC
base_path: resources
base_path: .
routes_path: resources/config

logging:
Expand Down
7 changes: 6 additions & 1 deletion resources/views/http_codes/404.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
<div class="row">
<div class="col">
<h1 class="centered">The page you are looking for does not exist.</h1>
<h1 class="centered text-secondary">404</h1>
</div>
</div>
<div class="row">
<div class="col">
<h2 class="centered">The page you are looking for does not exist.</h2>
</div>
</div>

Expand Down
2 changes: 1 addition & 1 deletion resources/views/layouts/default.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
</div>
<?php endif; ?>
<div class="text-center small">
Powered by <a href="https://neuronphp.com" target="_blank">NeuronPHP</a>.
Powered by <a href="https://neuronphp.com" target="_blank">NeuronCMS</a>.
</div>
</footer>
</div>
Expand Down
2 changes: 1 addition & 1 deletion resources/views/layouts/member.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
</div>
<?php endif; ?>
<div class="text-center small">
Powered by <a href="https://neuronphp.com" target="_blank">NeuronPHP</a>.
Powered by <a href="https://neuronphp.com" target="_blank">NeuronCMS</a>.
</div>
</footer>
</div>
Expand Down
252 changes: 252 additions & 0 deletions src/Cms/Cli/Commands/User/ResetPasswordCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
<?php

namespace Neuron\Cms\Cli\Commands\User;

use Neuron\Core\Registry\RegistryKeys;
use Neuron\Cli\Commands\Command;
use Neuron\Cms\Repositories\DatabaseUserRepository;
use Neuron\Cms\Auth\PasswordHasher;
use Neuron\Patterns\Registry;

/**
* Reset user password
*/
class ResetPasswordCommand extends Command
{

/**
* @inheritDoc
*/
public function getName(): string
{
return 'cms:user:reset-password';
}

/**
* @inheritDoc
*/
public function getDescription(): string
{
return 'Reset a user\'s password';
}

/**
* Configure the command
*/
public function configure(): void
{
$this->addOption( 'username', 'u', true, 'Username of the user' );
$this->addOption( 'email', 'e', true, 'Email of the user' );
}

/**
* Execute the command
*/
public function execute( array $parameters = [] ): int
{
$this->output->writeln( "\n╔═══════════════════════════════════════╗" );
$this->output->writeln( "║ Neuron CMS - Reset User Password ║" );
$this->output->writeln( "╚═══════════════════════════════════════╝\n" );

// Load database configuration
$repository = $this->getUserRepository();
if( !$repository )
{
return 1;
}

$hasher = new PasswordHasher();

// Load password policy from configuration
try
{
$settings = Registry::getInstance()->get( RegistryKeys::SETTINGS );

if( $settings )
{
// Read password policy from auth.passwords section
$minLength = $settings->get( 'auth', 'passwords', 'min_length' );
$requireUppercase = $settings->get( 'auth', 'passwords', 'require_uppercase' );
$requireLowercase = $settings->get( 'auth', 'passwords', 'require_lowercase' );
$requireNumbers = $settings->get( 'auth', 'passwords', 'require_numbers' );
$requireSpecialChars = $settings->get( 'auth', 'passwords', 'require_special_chars' );

// Configure hasher with policy
if( $minLength !== null )
{
$hasher->setMinLength( (int)$minLength );
}

if( $requireUppercase !== null )
{
$hasher->setRequireUppercase( (bool)$requireUppercase );
}

if( $requireLowercase !== null )
{
$hasher->setRequireLowercase( (bool)$requireLowercase );
}

if( $requireNumbers !== null )
{
$hasher->setRequireNumbers( (bool)$requireNumbers );
}

if( $requireSpecialChars !== null )
{
$hasher->setRequireSpecialChars( (bool)$requireSpecialChars );
}
}
}
catch( \Exception $e )
{
// Fall back to defaults on any exception
}

// Get username or email from options or prompt
$username = $this->input->getOption( 'username' );
$email = $this->input->getOption( 'email' );

// If neither provided, prompt for identifier
if( !$username && !$email )
{
$identifier = $this->prompt( "Enter username or email: " );
$identifier = trim( $identifier );

if( empty( $identifier ) )
{
$this->output->error( "Username or email is required!" );
return 1;
}

// Determine if it's an email or username
if( filter_var( $identifier, FILTER_VALIDATE_EMAIL ) )
{
$email = $identifier;
}
else
{
$username = $identifier;
}
}

// Find the user
$user = null;
if( $username )
{
$user = $repository->findByUsername( $username );
if( !$user )
{
$this->output->error( "User '$username' not found!" );
return 1;
}
}
elseif( $email )
{
$user = $repository->findByEmail( $email );
if( !$user )
{
$this->output->error( "User with email '$email' not found!" );
return 1;
}
}

// Display user info
$this->output->writeln( "User found:" );
$this->output->writeln( " ID: " . $user->getId() );
$this->output->writeln( " Username: " . $user->getUsername() );
$this->output->writeln( " Email: " . $user->getEmail() );
$this->output->writeln( " Role: " . $user->getRole() );
$this->output->writeln( "" );

// Confirm action
$confirm = $this->prompt( "Reset password for this user? (yes/no) [no]: " );
if( strtolower( trim( $confirm ) ) !== 'yes' )
{
$this->output->warning( "Password reset cancelled." );
return 0;
}

// Get new password
$password = $this->secret( "\nEnter new password: " );

// Validate password against configured policy
if( !$hasher->meetsRequirements( $password ) )
{
$this->output->error( "Password does not meet requirements:" );

foreach( $hasher->getValidationErrors( $password ) as $error )
{
$this->output->writeln( " - $error" );
}

$this->output->writeln( "" );
return 1;
}

// Confirm password
$confirmPassword = $this->secret( "Confirm new password: " );

if( $password !== $confirmPassword )
{
$this->output->error( "Passwords do not match!" );
return 1;
}

// Update password
try
{
$user->setPasswordHash( $hasher->hash( $password ) );
$user->setUpdatedAt( new \DateTimeImmutable() );

// Clear any lockout
$user->setFailedLoginAttempts( 0 );
$user->setLockedUntil( null );

$success = $repository->update( $user );

if( !$success )
{
$this->output->error( "Failed to update password in database" );
return 1;
}

$this->output->success( "Password reset successfully for user: " . $user->getUsername() );
$this->output->writeln( "" );

return 0;
}
catch( \Exception $e )
{
$this->output->error( "Error resetting password: " . $e->getMessage() );
return 1;
}
}

/**
* Get user repository
*
* Protected to allow mocking in tests
*/
protected function getUserRepository(): ?DatabaseUserRepository
{
try
{
$settings = Registry::getInstance()->get( RegistryKeys::SETTINGS );

if( !$settings )
{
$this->output->error( "Application not initialized: Settings not found in Registry" );
$this->output->writeln( "This is a configuration error - the application should load settings into the Registry" );
return null;
}

return new DatabaseUserRepository( $settings );
}
catch( \Exception $e )
{
$this->output->error( "Database connection failed: " . $e->getMessage() );
return null;
}
}
}
5 changes: 5 additions & 0 deletions src/Cms/Cli/Provider.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ public static function register( Registry $registry ): void
'Neuron\\Cms\\Cli\\Commands\\User\\DeleteCommand'
);

$registry->register(
'cms:user:reset-password',
'Neuron\\Cms\\Cli\\Commands\\User\\ResetPasswordCommand'
);

// Maintenance mode commands
$registry->register(
'cms:maintenance:enable',
Expand Down
Loading