Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
f14b9a8
fix: editorconfig for blade files (4 spaces instead of 2)
MrWeez May 3, 2026
a6c61e2
refactor: tabbed profile page
MrWeez May 3, 2026
8552e69
refactor: improve structure of profile page
MrWeez May 3, 2026
dbf055c
feat: add two-factor database migrations and models
MrWeez May 3, 2026
daf739c
feat: add two-factor service layer with TOTP and recovery code support
MrWeez May 4, 2026
42ed623
feat: add two-factor middleware with impersonation bypass and registe…
MrWeez May 4, 2026
6f2458e
feat: integrate two-factor challenge into login and logout flow
MrWeez May 4, 2026
0b122b5
feat: add TOTP challenge controller and view
MrWeez May 4, 2026
9d4a890
feat: add TOTP activation flow with QR setup and recovery code manage…
MrWeez May 4, 2026
6a56928
feat: add TOTP management UI to profile security tab
MrWeez May 4, 2026
a5c6399
feat: add rate limiting, token pruning, and security hardening
MrWeez May 4, 2026
00cebba
feat: implement modular 2FA core infrastructure
MrWeez May 5, 2026
a842db1
feat: enhance ExtensionServiceProvider to auto-load 2FA assets
MrWeez May 5, 2026
069bd51
refactor: move TOTP logic into modular extension
MrWeez May 5, 2026
c8326bb
feat: implement dynamic 2FA picker and update UI/routes
MrWeez May 5, 2026
d99aa4e
test: add Dummy 2FA extension for multi-method testing
MrWeez May 5, 2026
2085e18
style: standardize app name default to CtrlPanel.gg across the project
MrWeez May 5, 2026
632b3ef
fix: 2fa action route name and throttle
MrWeez May 5, 2026
5cde507
feat: improve views for 2FA and profile
MrWeez May 5, 2026
6c85aba
feat: add theming guide for 2FA system
MrWeez May 5, 2026
d25fadb
fix: dummy method profile card and login layout
MrWeez May 6, 2026
2ab9241
security: fix action whitelist and dummy method availability
MrWeez May 6, 2026
d768290
security: fix token expiration logic and path traversal in extensions
MrWeez May 6, 2026
4ff5650
refactor: add route constraints and tighten model guarding
MrWeez May 6, 2026
bb5a441
perf: fix N+1 in profile and cleanup redundant tokens
MrWeez May 6, 2026
7547a81
docs: clarify google2fa configuration
MrWeez May 6, 2026
c30773d
fix: extend 2fa token expiration time to match remember cookie
MrWeez May 6, 2026
5afd8d2
fix: correct 2FA token lifetime config and persistence logic
MrWeez May 6, 2026
85463cd
style: optimize 2FA UI and refactor profile data injection
MrWeez May 6, 2026
7cd9606
docs: add warnings and optimize recovery code verification logic
MrWeez May 6, 2026
28b060b
refactor: 2fa modals and profile views
MrWeez May 6, 2026
ca3d2ce
Merge branch 'development' into feat/2fa
MrWeez May 6, 2026
5f6e524
Merge branch 'development' into feat/2fa
MrWeez May 6, 2026
3afc51d
refactor: use SweetAlert2 toasts instead of toastr for 2FA
MrWeez May 6, 2026
8a173ba
refactor: use proper dependency injection for 2FA services across con…
MrWeez May 6, 2026
80a536d
style: remove useless coment
MrWeez May 6, 2026
ab407a5
fix: lower throttle limits for some 2fa routes
MrWeez May 6, 2026
997bdd9
feat: add 2FA status badge to admin datatable and raw method names to…
MrWeez May 6, 2026
19e24e4
feat: add cp:user:2fa:disable artisan command for 2FA recovery
MrWeez May 6, 2026
606043c
fix: make TwoFactorService CLI-safe by checking for active session
MrWeez May 6, 2026
784b4e2
fix: display none if no 2fa methods activated
MrWeez May 6, 2026
1738a27
refactor: disable 2fa command
MrWeez May 6, 2026
d4c15ea
Merge branch 'development' into feat/2fa
MrWeez May 7, 2026
b78e592
refactor: remove unused TwoFactorService dependency from UserController
MrWeez May 12, 2026
7e1b5a9
security: restrict Dummy 2FA extension to local environment only
MrWeez May 12, 2026
5b215a9
feat: implement dynamic, extension-aware 2FA rate limiting
MrWeez May 12, 2026
6b166d6
docs: complete 2FA modular system documentation with custom routes an…
MrWeez May 12, 2026
22ef0dc
fix: restore accidentally removed web rate limiter
MrWeez May 12, 2026
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
8 changes: 1 addition & 7 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,10 @@ indent_size = 2
indent_size = 4

[*.blade.php]
indent_size = 2
indent_size = 4

[*.js]
indent_size = 4

[*.jsx]
indent_size = 2

[*.tsx]
indent_size = 2

[*.json]
indent_size = 4
104 changes: 104 additions & 0 deletions app/Classes/TwoFactorExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php

namespace App\Classes;

use App\Models\User;
use Illuminate\Http\Request;

abstract class TwoFactorExtension extends AbstractExtension
{
/**
* Get the unique identifier for this 2FA method.
*/
abstract public function getName(): string;

/**
* Get the display label for this 2FA method.
*/
abstract public function getLabel(): string;

/**
* Get the FontAwesome icon for this 2FA method.
*/
abstract public function getIcon(): string;

/**
* Get a short description for this 2FA method.
*/
abstract public function getDescription(): string;

/**
* Check if this 2FA method is available for the given user.
*/
public function isAvailable(User $user): bool
{
return true;
}

/**
* Get the view name for the settings card in the profile.
*/
abstract public function getSettingsView(): string;

/**
* Get the view name for the login challenge.
*/
abstract public function getChallengeView(): string;

/**
* Verify the 2FA challenge.
*/
abstract public function verify(Request $request): bool;

/**
* Handle the setup logic (AJAX).
*/
abstract public function setup(Request $request);

/**
* Handle the enable logic (AJAX).
*/
abstract public function enable(Request $request);

/**
* Handle the disable logic (AJAX).
*/
abstract public function disable(Request $request);

/**
* Get the list of allowed actions that can be called via the action route.
*
* @return array
*/
public function getAllowedActions(): array
{
return [];
}

/**
* Get the rate limit for a specific action.
*
* @param string $action (setup, enable, disable, verify, action)
* @return array ['attempts' => int, 'minutes' => int]
*/
public function getRateLimit(string $action): array
{
$defaults = [
'setup' => ['attempts' => 3, 'minutes' => 1],
'enable' => ['attempts' => 3, 'minutes' => 1],
'disable' => ['attempts' => 3, 'minutes' => 1],
'verify' => ['attempts' => 5, 'minutes' => 1],
'action' => ['attempts' => 3, 'minutes' => 5], // Default 3 per 5 mins for sensitive actions
];

return $this->getConfig()['rate_limits'][$action] ?? $defaults[$action];
}

/**
* Get method-specific routes configuration if any.
*/
public static function getConfig(): array
{
return [];
}
}
133 changes: 133 additions & 0 deletions app/Console/Commands/DisableTwoFactorCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<?php

namespace App\Console\Commands;

use App\Models\User;
use App\Services\TwoFactor\TwoFactorService;
use Illuminate\Console\Command;

class DisableTwoFactorCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'cp:user:2fa:disable {search? : The ID, Email, Username or Discord ID of the user}';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Forcibly disable 2FA methods for a user';

protected $twoFactorService;

public function __construct(TwoFactorService $twoFactorService)
{
parent::__construct();
$this->twoFactorService = $twoFactorService;
}

/**
* Execute the console command.
*/
public function handle()
{
$search = $this->argument('search');

if (!$search) {
$search = $this->ask('Please enter User ID, Email, Username or Discord ID');
}

if (!$search) {
$this->error('No search term provided.');
return 1;
}

$user = User::query()
->where('id', $search)
->orWhere('email', $search)
->orWhere('name', $search)
->orWhereHas('discordUser', function ($query) use ($search) {
$query->where('id', $search);
})
->first();

if (!$user) {
$this->error("User not found with term: {$search}");
return 1;
}

$this->info("Found User: {$user->name} ({$user->email}) [ID: {$user->id}]");

$methods = $user->twoFactorMethods()->where('is_enabled', true)->get();

if ($methods->isEmpty()) {
$this->warn('This user does not have any 2FA methods enabled.');
return 0;
}

$choices = $methods->mapWithKeys(function ($m) {
$label = $this->twoFactorService->getExtension($m->method)?->getLabel() ?? ucfirst($m->method);
return [$m->method => "{$label} ({$m->method})"];
})->toArray();

$choices['all'] = 'Disable ALL methods';

$selected = $this->choice(
'Which 2FA methods do you want to disable?',
$choices,
null,
null,
true // Multiple selection
);

if (empty($selected)) {
$this->info('Nothing selected. Aborting.');
return 0;
}

if (in_array('Disable ALL methods', $selected) || in_array('all', $selected)) {
if ($this->confirm("Are you sure you want to disable ALL 2FA methods for {$user->name}?", true)) {
$user->twoFactorMethods()->delete();
$this->twoFactorService->clearVerified(request(), $user);
$this->success("Successfully disabled all 2FA methods for {$user->name}.");
}
return 0;
}

// Map back titles to keys if necessary (Laravel's choice with multiple can return values or keys depending on version/selection)
$methodKeys = [];
foreach ($selected as $choice) {
$key = array_search($choice, $choices);
if ($key !== false) {
$methodKeys[] = $key;
} else {
// If it already returned the key
if (isset($choices[$choice])) {
$methodKeys[] = $choice;
}
}
}

if ($this->confirm("Disable selected methods: " . implode(', ', $methodKeys) . "?", true)) {
$user->twoFactorMethods()->whereIn('method', $methodKeys)->delete();

// If we disabled everything, clear verified state
if ($user->twoFactorMethods()->where('is_enabled', true)->count() === 0) {
$this->twoFactorService->clearVerified(request(), $user);
}

$this->success("Successfully disabled " . implode(', ', $methodKeys) . " for {$user->name}.");
}

return 0;
}

protected function success($message)
{
$this->output->writeln("<fg=green;options=bold> SUCCESS </> $message");
}
}
1 change: 1 addition & 0 deletions app/Console/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ protected function schedule(Schedule $schedule)
$schedule->command('payments:open:clear')->daily();
$schedule->command('coupons:delete')->hourly();
$schedule->command('vouchers:delete')->hourly();
$schedule->command('model:prune')->daily();

//log cronjob activity
$schedule->call(function () {
Expand Down
105 changes: 105 additions & 0 deletions app/Extensions/TwoFactor/Dummy/DummyExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

namespace App\Extensions\TwoFactor\Dummy;

use App\Classes\TwoFactorExtension;
use App\Models\User;
use App\Models\UserTwoFactorMethod;
use Illuminate\Http\Request;

class DummyExtension extends TwoFactorExtension
{
public function getName(): string
{
return 'dummy';
}

public function getLabel(): string
{
return __('Dummy 2FA (Test)');
}

public function getIcon(): string
{
return 'fas fa-flask';
}

public function getDescription(): string
{
return __('A temporary method for testing modular 2FA.');
}

public function isAvailable(User $user): bool
{
return app()->environment('local');
}

public function getSettingsView(): string
{
if (!app()->environment('local')) {
abort(403);
}

return 'twofactor_dummy::profile_card';
}

public function getChallengeView(): string
{
if (!app()->environment('local')) {
abort(403);
}

return 'twofactor_dummy::auth.two-factor.dummy-challenge';
}

public function verify(Request $request): bool
{
if (!app()->environment('local')) {
abort(403);
}

return $request->input('code') === '123456';
}
Comment thread
MrWeez marked this conversation as resolved.

public function setup(Request $request)
{
if (!app()->environment('local')) {
abort(403);
}

return response()->json(['message' => 'Dummy setup ready. Use code 123456 to enable.']);
}

public function enable(Request $request)
{
if (!app()->environment('local')) {
abort(403);
}

if ($request->input('code') !== '123456') {
return response()->json(['errors' => ['code' => ['Use 123456']]], 422);
}

UserTwoFactorMethod::updateOrCreate(
['user_id' => $request->user()->id, 'method' => 'dummy'],
['is_enabled' => true]
);

return response()->json(['message' => 'Dummy 2FA enabled!']);
}

/**
* NOTE: This is a dummy method for development only.
* In a production-ready extension, this method SHOULD require
* password or 2FA code verification before disabling.
*/
public function disable(Request $request)
{
if (!app()->environment('local')) {
abort(403);
}

$request->user()->twoFactorMethods()->where('method', 'dummy')->delete();
return response()->json(['message' => 'Dummy 2FA disabled.']);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
@extends('layouts.app')

@section('content')
@php($suppressSweetAlert2 = true)

<body class="hold-transition dark-mode login-page">
<div class="login-box">
<div class="card card-outline card-primary">
<div class="text-center card-header">
<a href="{{ route('welcome') }}" class="mb-2 h1"><b class="mr-1">{{ config('app.name', 'CtrlPanel.gg') }}</b></a>
</div>
<div class="card-body">
<p class="login-box-msg">{{ __('Dummy 2FA Challenge') }}</p>
<p class="text-center small text-muted">Enter 123456 to pass.</p>

<form action="{{ route('login.2fa.verify', ['method' => 'dummy']) }}" method="post">
@csrf
<div class="form-group">
<input type="text" name="code" class="form-control" placeholder="123456" autofocus>
</div>
<button type="submit" class="btn btn-primary btn-block">{{ __('Verify') }}</button>
</form>
</div>
</div>
</div>
</body>
@endsection
Loading
Loading