Add all files
This commit is contained in:
142
extensions/Others/Security/Admin/Resources/BlockedIPResource.php
Normal file
142
extensions/Others/Security/Admin/Resources/BlockedIPResource.php
Normal file
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Security\Admin\Resources;
|
||||
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Components\DateTimePicker;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\BadgeColumn;
|
||||
use Filament\Tables\Columns\BooleanColumn;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Paymenter\Extensions\Others\Security\Models\BlockedIP;
|
||||
use Paymenter\Extensions\Others\Security\Admin\Resources\BlockedIPResource\Pages;
|
||||
use Filament\Forms\Get;
|
||||
|
||||
class BlockedIPResource extends Resource
|
||||
{
|
||||
protected static ?string $model = BlockedIP::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-shield-exclamation';
|
||||
protected static ?string $navigationGroup = 'Security';
|
||||
protected static ?string $navigationLabel = 'Blocked IPs';
|
||||
protected static ?string $pluralLabel = 'Blocked IPs';
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form->schema([
|
||||
TextInput::make('ip_address')
|
||||
->label('IP Address')
|
||||
->required()
|
||||
->ip()
|
||||
->maxLength(45),
|
||||
|
||||
TextInput::make('reason')
|
||||
->label('Reason')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
|
||||
Textarea::make('notes')
|
||||
->label('Notes')
|
||||
->rows(3)
|
||||
->maxLength(1000),
|
||||
|
||||
Toggle::make('is_permanent')
|
||||
->label('Permanent Block')
|
||||
->default(false)
|
||||
->reactive(),
|
||||
|
||||
DateTimePicker::make('blocked_until')
|
||||
->label('Blocked Until')
|
||||
->nullable()
|
||||
->hidden(fn (Get $get) => $get('is_permanent')),
|
||||
|
||||
Select::make('blocked_by')
|
||||
->label('Blocked By')
|
||||
->relationship('blockedBy', 'email')
|
||||
->nullable()
|
||||
->default(fn () => Auth::id()),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('ip_address')
|
||||
->label('IP Address')
|
||||
->searchable()
|
||||
->sortable()
|
||||
->copyable(),
|
||||
|
||||
TextColumn::make('reason')
|
||||
->label('Reason')
|
||||
->searchable()
|
||||
->limit(50),
|
||||
|
||||
BadgeColumn::make('is_permanent')
|
||||
->label('Type')
|
||||
->getStateUsing(fn (BlockedIP $record) => $record->is_permanent ? 'Permanent' : 'Temporary')
|
||||
->colors([
|
||||
'danger' => 'Permanent',
|
||||
'warning' => 'Temporary',
|
||||
]),
|
||||
|
||||
TextColumn::make('blocked_until')
|
||||
->label('Until')
|
||||
->dateTime()
|
||||
->sortable(),
|
||||
|
||||
BooleanColumn::make('is_active')
|
||||
->label('Active'),
|
||||
|
||||
TextColumn::make('failed_login_attempts')
|
||||
->label('Failed Attempts')
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('blockedBy.email')
|
||||
->label('Blocked By')
|
||||
->sortable()
|
||||
->default('System'),
|
||||
|
||||
TextColumn::make('blocked_at')
|
||||
->label('Blocked At')
|
||||
->dateTime()
|
||||
->sortable(),
|
||||
])
|
||||
->actions([
|
||||
EditAction::make(),
|
||||
Action::make('unblock')
|
||||
->label('Unblock IP')
|
||||
->icon('heroicon-m-check')
|
||||
->color('success')
|
||||
->action(fn (BlockedIP $record) => $record->unblock(Auth::id()))
|
||||
->requiresConfirmation()
|
||||
->visible(fn (BlockedIP $record) => $record->is_active),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
])
|
||||
->defaultSort('blocked_at', 'desc');
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListBlockedIPs::route('/'),
|
||||
'create' => Pages\CreateBlockedIP::route('/create'),
|
||||
'edit' => Pages\EditBlockedIP::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Security\Admin\Resources\BlockedIPResource\Pages;
|
||||
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Paymenter\Extensions\Others\Security\Admin\Resources\BlockedIPResource;
|
||||
|
||||
class CreateBlockedIP extends CreateRecord
|
||||
{
|
||||
protected static string $resource = BlockedIPResource::class;
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
$data['blocked_at'] = now();
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Security\Admin\Resources\BlockedIPResource\Pages;
|
||||
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Paymenter\Extensions\Others\Security\Admin\Resources\BlockedIPResource;
|
||||
|
||||
class EditBlockedIP extends EditRecord
|
||||
{
|
||||
protected static string $resource = BlockedIPResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Security\Admin\Resources\BlockedIPResource\Pages;
|
||||
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Paymenter\Extensions\Others\Security\Admin\Resources\BlockedIPResource;
|
||||
|
||||
class ListBlockedIPs extends ListRecords
|
||||
{
|
||||
protected static string $resource = BlockedIPResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Security\Admin\Resources;
|
||||
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Forms\Form;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Forms\Components\TextInput;
|
||||
use Filament\Forms\Components\Textarea;
|
||||
use Filament\Forms\Components\Toggle;
|
||||
use Filament\Forms\Components\DateTimePicker;
|
||||
use Filament\Forms\Components\Select;
|
||||
use Filament\Tables\Columns\TextColumn;
|
||||
use Filament\Tables\Columns\BadgeColumn;
|
||||
use Filament\Tables\Columns\BooleanColumn;
|
||||
use Filament\Tables\Actions\EditAction;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Tables\Actions\BulkActionGroup;
|
||||
use Filament\Tables\Actions\DeleteBulkAction;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Paymenter\Extensions\Others\Security\Models\SuspendedAccount;
|
||||
use Paymenter\Extensions\Others\Security\Admin\Resources\SuspendedAccountResource\Pages;
|
||||
use Filament\Forms\Get;
|
||||
|
||||
class SuspendedAccountResource extends Resource
|
||||
{
|
||||
protected static ?string $model = SuspendedAccount::class;
|
||||
|
||||
protected static ?string $navigationIcon = 'heroicon-o-user-minus';
|
||||
protected static ?string $navigationGroup = 'Security';
|
||||
protected static ?string $navigationLabel = 'Suspended Accounts';
|
||||
protected static ?string $pluralLabel = 'Suspended Accounts';
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form->schema([
|
||||
Select::make('user_id')
|
||||
->label('User')
|
||||
->relationship('user', 'email')
|
||||
->searchable()
|
||||
->preload()
|
||||
->required(),
|
||||
|
||||
TextInput::make('reason')
|
||||
->label('Reason')
|
||||
->required()
|
||||
->maxLength(255),
|
||||
|
||||
Textarea::make('notes')
|
||||
->label('Notes')
|
||||
->rows(3)
|
||||
->maxLength(1000),
|
||||
|
||||
Toggle::make('is_permanent')
|
||||
->label('Permanent Suspension')
|
||||
->default(false)
|
||||
->reactive(),
|
||||
|
||||
DateTimePicker::make('suspended_until')
|
||||
->label('Suspended Until')
|
||||
->nullable()
|
||||
->hidden(fn (Get $get) => $get('is_permanent')),
|
||||
|
||||
Select::make('suspended_by')
|
||||
->label('Suspended By')
|
||||
->relationship('suspendedBy', 'email')
|
||||
->nullable()
|
||||
->default(fn () => Auth::id())
|
||||
->required(),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
TextColumn::make('user.email')
|
||||
->label('User')
|
||||
->searchable()
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('reason')
|
||||
->label('Reason')
|
||||
->searchable()
|
||||
->limit(50),
|
||||
|
||||
BadgeColumn::make('is_permanent')
|
||||
->label('Type')
|
||||
->getStateUsing(fn (SuspendedAccount $record) => $record->is_permanent ? 'Permanent' : 'Temporary')
|
||||
->colors([
|
||||
'danger' => 'Permanent',
|
||||
'warning' => 'Temporary',
|
||||
]),
|
||||
|
||||
TextColumn::make('suspended_until')
|
||||
->label('Until')
|
||||
->dateTime()
|
||||
->sortable(),
|
||||
|
||||
BooleanColumn::make('is_active')
|
||||
->label('Active'),
|
||||
|
||||
TextColumn::make('suspendedBy.email')
|
||||
->label('Suspended By')
|
||||
->sortable(),
|
||||
|
||||
TextColumn::make('suspended_at')
|
||||
->label('Suspended At')
|
||||
->dateTime()
|
||||
->sortable(),
|
||||
])
|
||||
->actions([
|
||||
EditAction::make(),
|
||||
Action::make('lift')
|
||||
->label('Lift Suspension')
|
||||
->icon('heroicon-m-check')
|
||||
->color('success')
|
||||
->action(fn (SuspendedAccount $record) => $record->lift(Auth::id()))
|
||||
->requiresConfirmation()
|
||||
->visible(fn (SuspendedAccount $record) => $record->is_active),
|
||||
])
|
||||
->bulkActions([
|
||||
BulkActionGroup::make([
|
||||
DeleteBulkAction::make(),
|
||||
]),
|
||||
])
|
||||
->defaultSort('suspended_at', 'desc');
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListSuspendedAccounts::route('/'),
|
||||
'create' => Pages\CreateSuspendedAccount::route('/create'),
|
||||
'edit' => Pages\EditSuspendedAccount::route('/{record}/edit'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Security\Admin\Resources\SuspendedAccountResource\Pages;
|
||||
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
use Paymenter\Extensions\Others\Security\Admin\Resources\SuspendedAccountResource;
|
||||
|
||||
class CreateSuspendedAccount extends CreateRecord
|
||||
{
|
||||
protected static string $resource = SuspendedAccountResource::class;
|
||||
|
||||
protected function mutateFormDataBeforeCreate(array $data): array
|
||||
{
|
||||
$data['suspended_at'] = now();
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Security\Admin\Resources\SuspendedAccountResource\Pages;
|
||||
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
use Paymenter\Extensions\Others\Security\Admin\Resources\SuspendedAccountResource;
|
||||
|
||||
class EditSuspendedAccount extends EditRecord
|
||||
{
|
||||
protected static string $resource = SuspendedAccountResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Security\Admin\Resources\SuspendedAccountResource\Pages;
|
||||
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
use Paymenter\Extensions\Others\Security\Admin\Resources\SuspendedAccountResource;
|
||||
|
||||
class ListSuspendedAccounts extends ListRecords
|
||||
{
|
||||
protected static string $resource = SuspendedAccountResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\CreateAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
128
extensions/Others/Security/Listeners/SecurityLoginListener.php
Normal file
128
extensions/Others/Security/Listeners/SecurityLoginListener.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Security\Listeners;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Paymenter\Extensions\Others\Security\Models\LoginAttempt;
|
||||
use Paymenter\Extensions\Others\Security\Models\BlockedIP;
|
||||
use Paymenter\Extensions\Others\Security\Models\SuspendedAccount;
|
||||
use Paymenter\Extensions\Others\Security\Services\FraudDetectionService;
|
||||
use Paymenter\Extensions\Others\Security\Services\VPNDetectionService;
|
||||
|
||||
class SecurityLoginListener
|
||||
{
|
||||
protected $request;
|
||||
|
||||
public function __construct(Request $request)
|
||||
{
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
public static function checkLoginSecurity(string $email, string $ipAddress): ?string
|
||||
{
|
||||
if (BlockedIP::isIPBlocked($ipAddress)) {
|
||||
$block = BlockedIP::getActiveBlock($ipAddress);
|
||||
return 'Your IP address has been blocked. Reason: ' . $block->reason;
|
||||
}
|
||||
|
||||
if (VPNDetectionService::isVPNBlocked($ipAddress)) {
|
||||
VPNDetectionService::checkAndBlockVPN($ipAddress, $email);
|
||||
return 'Access denied. VPN/Proxy connections are not allowed.';
|
||||
}
|
||||
|
||||
$user = \App\Models\User::where('email', $email)->first();
|
||||
if ($user && SuspendedAccount::isUserSuspended($user->id)) {
|
||||
$suspension = SuspendedAccount::getActiveSuspension($user->id);
|
||||
return 'Your account has been suspended. Reason: ' . $suspension->reason;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static function checkRegistrationSecurity(string $email, string $ipAddress): ?string
|
||||
{
|
||||
if (BlockedIP::isIPBlocked($ipAddress)) {
|
||||
$block = BlockedIP::getActiveBlock($ipAddress);
|
||||
return 'Your IP address has been blocked. Reason: ' . $block->reason;
|
||||
}
|
||||
|
||||
if (VPNDetectionService::isVPNBlocked($ipAddress)) {
|
||||
VPNDetectionService::checkAndBlockVPN($ipAddress, $email, 'registration');
|
||||
return 'Access denied. VPN/Proxy connections are not allowed for registration.';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function handleSuccessfulLogin($event): void
|
||||
{
|
||||
$ipAddress = $this->request->ip();
|
||||
$email = $event->user->email ?? null;
|
||||
$userId = $event->user->id;
|
||||
|
||||
if (SuspendedAccount::isUserSuspended($userId)) {
|
||||
$suspension = SuspendedAccount::getActiveSuspension($userId);
|
||||
|
||||
LoginAttempt::recordAttempt(
|
||||
$ipAddress,
|
||||
$email,
|
||||
false,
|
||||
$this->request->userAgent(),
|
||||
null,
|
||||
'Login blocked - Account suspended: ' . $suspension->reason
|
||||
);
|
||||
|
||||
Auth::logout();
|
||||
session()->flash('suspension_error', 'Your account has been suspended. Reason: ' . $suspension->reason);
|
||||
return;
|
||||
}
|
||||
|
||||
LoginAttempt::recordAttempt(
|
||||
$ipAddress,
|
||||
$email,
|
||||
true,
|
||||
$this->request->userAgent(),
|
||||
null,
|
||||
'Successful login'
|
||||
);
|
||||
|
||||
if ($event->user) {
|
||||
FraudDetectionService::checkAndSuspendForFraud($event->user);
|
||||
}
|
||||
}
|
||||
|
||||
public function handleFailedLogin($event): void
|
||||
{
|
||||
$ipAddress = $this->request->ip();
|
||||
$email = $this->request->input('email');
|
||||
|
||||
LoginAttempt::recordAttempt(
|
||||
$ipAddress,
|
||||
$email,
|
||||
false,
|
||||
$this->request->userAgent(),
|
||||
null,
|
||||
'Failed login attempt'
|
||||
);
|
||||
|
||||
BlockedIP::recordFailedAttempt($ipAddress);
|
||||
|
||||
$extension = app(\Paymenter\Extensions\Others\Security\Security::class);
|
||||
$config = $extension->getConfig();
|
||||
|
||||
$maxAttempts = 5;
|
||||
$blockDuration = 60;
|
||||
|
||||
foreach ($config as $setting) {
|
||||
if ($setting['name'] === 'max_login_attempts') {
|
||||
$maxAttempts = $setting['default'] ?? 5;
|
||||
}
|
||||
if ($setting['name'] === 'ip_block_duration') {
|
||||
$blockDuration = $setting['default'] ?? 60;
|
||||
}
|
||||
}
|
||||
|
||||
BlockedIP::autoBlockIfNeeded($ipAddress, $maxAttempts, $blockDuration);
|
||||
}
|
||||
}
|
||||
64
extensions/Others/Security/Middleware/SecurityCheck.php
Normal file
64
extensions/Others/Security/Middleware/SecurityCheck.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Security\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Paymenter\Extensions\Others\Security\Models\SuspendedAccount;
|
||||
use Paymenter\Extensions\Others\Security\Models\BlockedIP;
|
||||
use Paymenter\Extensions\Others\Security\Models\LoginAttempt;
|
||||
|
||||
class SecurityCheck
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$ipAddress = $request->ip();
|
||||
|
||||
// Check if IP is blocked
|
||||
if (BlockedIP::isIPBlocked($ipAddress)) {
|
||||
$block = BlockedIP::getActiveBlock($ipAddress);
|
||||
|
||||
// Log the blocked attempt
|
||||
LoginAttempt::recordAttempt(
|
||||
$ipAddress,
|
||||
$request->input('email'),
|
||||
false,
|
||||
$request->userAgent(),
|
||||
null,
|
||||
'Blocked IP attempted access'
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'error' => 'Access denied',
|
||||
'message' => 'Your IP address has been blocked. Reason: ' . $block->reason,
|
||||
'blocked_until' => $block->blocked_until ? $block->blocked_until->toISOString() : null
|
||||
], 403);
|
||||
}
|
||||
|
||||
// Check if user is suspended (only if authenticated)
|
||||
if (Auth::check()) {
|
||||
$userId = Auth::id();
|
||||
|
||||
if (SuspendedAccount::isUserSuspended($userId)) {
|
||||
$suspension = SuspendedAccount::getActiveSuspension($userId);
|
||||
|
||||
// Log out the user
|
||||
Auth::logout();
|
||||
|
||||
return response()->json([
|
||||
'error' => 'Account suspended',
|
||||
'message' => 'Your account has been suspended. Reason: ' . $suspension->reason,
|
||||
'suspended_until' => $suspension->suspended_until ? $suspension->suspended_until->toISOString() : null,
|
||||
'is_permanent' => $suspension->is_permanent
|
||||
], 403);
|
||||
}
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
181
extensions/Others/Security/Models/BlockedIP.php
Normal file
181
extensions/Others/Security/Models/BlockedIP.php
Normal file
@@ -0,0 +1,181 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Security\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class BlockedIP extends Model
|
||||
{
|
||||
protected $table = 'ext_security_blocked_ips';
|
||||
|
||||
protected $fillable = [
|
||||
'ip_address',
|
||||
'reason',
|
||||
'notes',
|
||||
'is_permanent',
|
||||
'blocked_at',
|
||||
'blocked_until',
|
||||
'blocked_by',
|
||||
'unblocked_at',
|
||||
'unblocked_by',
|
||||
'is_active',
|
||||
'failed_login_attempts',
|
||||
'last_attempt_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'blocked_at' => 'datetime',
|
||||
'blocked_until' => 'datetime',
|
||||
'unblocked_at' => 'datetime',
|
||||
'last_attempt_at' => 'datetime',
|
||||
'is_permanent' => 'boolean',
|
||||
'is_active' => 'boolean',
|
||||
'failed_login_attempts' => 'integer',
|
||||
];
|
||||
|
||||
public function blockedBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'blocked_by');
|
||||
}
|
||||
|
||||
public function unblockedBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'unblocked_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the IP block is currently active
|
||||
*/
|
||||
public function isCurrentlyActive(): bool
|
||||
{
|
||||
if (!$this->is_active) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->is_permanent) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->blocked_until && Carbon::now()->isAfter($this->blocked_until)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unblock the IP
|
||||
*/
|
||||
public function unblock(?int $unblockedBy = null): void
|
||||
{
|
||||
$this->update([
|
||||
'is_active' => false,
|
||||
'unblocked_at' => now(),
|
||||
'unblocked_by' => $unblockedBy,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active block for an IP
|
||||
*/
|
||||
public static function getActiveBlock(string $ipAddress): ?self
|
||||
{
|
||||
return static::where('ip_address', $ipAddress)
|
||||
->where('is_active', true)
|
||||
->where(function ($query) {
|
||||
$query->where('is_permanent', true)
|
||||
->orWhere('blocked_until', '>', now())
|
||||
->orWhereNull('blocked_until');
|
||||
})
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an IP is blocked
|
||||
*/
|
||||
public static function isIPBlocked(string $ipAddress): bool
|
||||
{
|
||||
$block = static::getActiveBlock($ipAddress);
|
||||
return $block && $block->isCurrentlyActive();
|
||||
}
|
||||
|
||||
/**
|
||||
* Block an IP address
|
||||
*/
|
||||
public static function blockIP(
|
||||
string $ipAddress,
|
||||
string $reason,
|
||||
?int $blockedBy = null,
|
||||
bool $isPermanent = false,
|
||||
?Carbon $blockedUntil = null,
|
||||
?string $notes = null
|
||||
): self {
|
||||
// Deactivate any existing blocks
|
||||
static::where('ip_address', $ipAddress)
|
||||
->where('is_active', true)
|
||||
->update(['is_active' => false]);
|
||||
|
||||
return static::create([
|
||||
'ip_address' => $ipAddress,
|
||||
'reason' => $reason,
|
||||
'notes' => $notes,
|
||||
'is_permanent' => $isPermanent,
|
||||
'blocked_at' => now(),
|
||||
'blocked_until' => $blockedUntil,
|
||||
'blocked_by' => $blockedBy,
|
||||
'is_active' => true,
|
||||
'failed_login_attempts' => 0,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a failed login attempt
|
||||
*/
|
||||
public static function recordFailedAttempt(string $ipAddress): void
|
||||
{
|
||||
$existing = static::where('ip_address', $ipAddress)
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
$existing->increment('failed_login_attempts');
|
||||
$existing->update(['last_attempt_at' => now()]);
|
||||
} else {
|
||||
static::create([
|
||||
'ip_address' => $ipAddress,
|
||||
'reason' => 'Failed login attempts',
|
||||
'is_permanent' => false,
|
||||
'blocked_at' => now(),
|
||||
'is_active' => false, // Not blocked yet, just tracking attempts
|
||||
'failed_login_attempts' => 1,
|
||||
'last_attempt_at' => now(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-block IP after too many failed attempts
|
||||
*/
|
||||
public static function autoBlockIfNeeded(string $ipAddress, int $maxAttempts, int $blockDurationMinutes): bool
|
||||
{
|
||||
$record = static::where('ip_address', $ipAddress)->first();
|
||||
|
||||
if (!$record || $record->failed_login_attempts < $maxAttempts) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Block the IP
|
||||
static::blockIP(
|
||||
$ipAddress,
|
||||
"Auto-blocked after {$maxAttempts} failed login attempts",
|
||||
null, // System blocked
|
||||
false, // Not permanent
|
||||
now()->addMinutes($blockDurationMinutes)
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
69
extensions/Others/Security/Models/LoginAttempt.php
Normal file
69
extensions/Others/Security/Models/LoginAttempt.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Security\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class LoginAttempt extends Model
|
||||
{
|
||||
protected $table = 'ext_security_login_attempts';
|
||||
|
||||
protected $fillable = [
|
||||
'ip_address',
|
||||
'email',
|
||||
'successful',
|
||||
'user_agent',
|
||||
'country',
|
||||
'notes',
|
||||
'attempted_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'successful' => 'boolean',
|
||||
'attempted_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Record a login attempt
|
||||
*/
|
||||
public static function recordAttempt(
|
||||
string $ipAddress,
|
||||
?string $email = null,
|
||||
bool $successful = false,
|
||||
?string $userAgent = null,
|
||||
?string $country = null,
|
||||
?string $notes = null
|
||||
): self {
|
||||
return static::create([
|
||||
'ip_address' => $ipAddress,
|
||||
'email' => $email,
|
||||
'successful' => $successful,
|
||||
'user_agent' => $userAgent,
|
||||
'country' => $country,
|
||||
'notes' => $notes,
|
||||
'attempted_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent failed attempts for an IP
|
||||
*/
|
||||
public static function getRecentFailedAttempts(string $ipAddress, int $minutes = 60): int
|
||||
{
|
||||
return static::where('ip_address', $ipAddress)
|
||||
->where('successful', false)
|
||||
->where('attempted_at', '>', now()->subMinutes($minutes))
|
||||
->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent failed attempts for an email
|
||||
*/
|
||||
public static function getRecentFailedAttemptsForEmail(string $email, int $minutes = 60): int
|
||||
{
|
||||
return static::where('email', $email)
|
||||
->where('successful', false)
|
||||
->where('attempted_at', '>', now()->subMinutes($minutes))
|
||||
->count();
|
||||
}
|
||||
}
|
||||
133
extensions/Others/Security/Models/SuspendedAccount.php
Normal file
133
extensions/Others/Security/Models/SuspendedAccount.php
Normal file
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Security\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use App\Models\User;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class SuspendedAccount extends Model
|
||||
{
|
||||
protected $table = 'ext_security_suspended_accounts';
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'reason',
|
||||
'notes',
|
||||
'is_permanent',
|
||||
'suspended_at',
|
||||
'suspended_until',
|
||||
'suspended_by',
|
||||
'lifted_at',
|
||||
'lifted_by',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'suspended_at' => 'datetime',
|
||||
'suspended_until' => 'datetime',
|
||||
'lifted_at' => 'datetime',
|
||||
'is_permanent' => 'boolean',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function suspendedBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'suspended_by');
|
||||
}
|
||||
|
||||
public function liftedBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'lifted_by');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the suspension is currently active
|
||||
*/
|
||||
public function isCurrentlyActive(): bool
|
||||
{
|
||||
if (!$this->is_active) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->is_permanent) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->suspended_until && Carbon::now()->isAfter($this->suspended_until)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lift the suspension
|
||||
*/
|
||||
public function lift(int $liftedBy): void
|
||||
{
|
||||
$this->update([
|
||||
'is_active' => false,
|
||||
'lifted_at' => now(),
|
||||
'lifted_by' => $liftedBy,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active suspension for a user
|
||||
*/
|
||||
public static function getActiveSuspension(int $userId): ?self
|
||||
{
|
||||
return static::where('user_id', $userId)
|
||||
->where('is_active', true)
|
||||
->where(function ($query) {
|
||||
$query->where('is_permanent', true)
|
||||
->orWhere('suspended_until', '>', now())
|
||||
->orWhereNull('suspended_until');
|
||||
})
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a user is suspended
|
||||
*/
|
||||
public static function isUserSuspended(int $userId): bool
|
||||
{
|
||||
$suspension = static::getActiveSuspension($userId);
|
||||
return $suspension && $suspension->isCurrentlyActive();
|
||||
}
|
||||
|
||||
/**
|
||||
* Suspend a user
|
||||
*/
|
||||
public static function suspendUser(
|
||||
int $userId,
|
||||
string $reason,
|
||||
int $suspendedBy,
|
||||
bool $isPermanent = false,
|
||||
?Carbon $suspendedUntil = null,
|
||||
?string $notes = null
|
||||
): self {
|
||||
// Deactivate any existing suspensions
|
||||
static::where('user_id', $userId)
|
||||
->where('is_active', true)
|
||||
->update(['is_active' => false]);
|
||||
|
||||
return static::create([
|
||||
'user_id' => $userId,
|
||||
'reason' => $reason,
|
||||
'notes' => $notes,
|
||||
'is_permanent' => $isPermanent,
|
||||
'suspended_at' => now(),
|
||||
'suspended_until' => $suspendedUntil,
|
||||
'suspended_by' => $suspendedBy,
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
97
extensions/Others/Security/Security.php
Normal file
97
extensions/Others/Security/Security.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Security;
|
||||
|
||||
use Exception;
|
||||
use App\Classes\Extension\Extension;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\HtmlString;
|
||||
|
||||
use Paymenter\Extensions\Others\Security\Models\SuspendedAccount;
|
||||
use Paymenter\Extensions\Others\Security\Models\BlockedIP;
|
||||
use Paymenter\Extensions\Others\Security\Admin\Resources\SuspendedAccountResource;
|
||||
use Paymenter\Extensions\Others\Security\Admin\Resources\BlockedIPResource;
|
||||
use Paymenter\Extensions\Others\Security\Listeners\SecurityLoginListener;
|
||||
|
||||
class Security extends Extension
|
||||
{
|
||||
public function getConfig($values = [])
|
||||
{
|
||||
try {
|
||||
return [
|
||||
[
|
||||
'name' => 'Notice',
|
||||
'type' => 'placeholder',
|
||||
'label' => new HtmlString(
|
||||
'Use this extension to improve your security, manage account suspensions, block VPN/Proxy IPs and block IP addresses from logging in.
|
||||
<br><br><small><i>Need support? Join our Discord at <a href="https://discord.gg/DP3atuhnsB">https://discord.gg/DP3atuhnsB</a></small></i>'
|
||||
),
|
||||
],
|
||||
[
|
||||
'name' => 'auto_suspend_fraud',
|
||||
'type' => 'checkbox',
|
||||
'label' => 'Auto-suspend accounts flagged for fraud',
|
||||
'description' => 'Automatically suspend accounts when fraud is detected',
|
||||
'default' => false,
|
||||
],
|
||||
[
|
||||
'name' => 'block_vpn_ips',
|
||||
'type' => 'checkbox',
|
||||
'label' => 'Block known VPN/Proxy IPs',
|
||||
'description' => 'Block login attempts from known VPN and proxy services',
|
||||
'default' => false,
|
||||
],
|
||||
[
|
||||
'name' => 'max_login_attempts',
|
||||
'type' => 'number',
|
||||
'label' => 'Max login attempts before IP block',
|
||||
'description' => 'Number of failed login attempts before temporarily blocking IP',
|
||||
'default' => 5,
|
||||
],
|
||||
[
|
||||
'name' => 'ip_block_duration',
|
||||
'type' => 'number',
|
||||
'label' => 'IP block duration (minutes)',
|
||||
'description' => 'How long to block an IP after max login attempts',
|
||||
'default' => 60,
|
||||
],
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
return [
|
||||
[
|
||||
'name' => 'Notice',
|
||||
'type' => 'placeholder',
|
||||
'label' => new HtmlString(
|
||||
'You need to enable this extension before using it.
|
||||
<br><br><small><i>Need support? Join our Discord at <a href="https://discord.gg/DP3atuhnsB">https://discord.gg/DP3atuhnsB</a></small></i>'
|
||||
),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
public function enabled()
|
||||
{
|
||||
Artisan::call('migrate', [
|
||||
'--path' => 'extensions/Others/Security/database/migrations',
|
||||
'--force' => true
|
||||
]);
|
||||
}
|
||||
|
||||
public function boot()
|
||||
{
|
||||
app('router')->aliasMiddleware('security.check', \Paymenter\Extensions\Others\Security\Middleware\SecurityCheck::class);
|
||||
|
||||
if (class_exists(\Filament\Filament::class)) {
|
||||
\Filament\Filament::serving(function () {
|
||||
\Filament\Filament::registerResources([
|
||||
SuspendedAccountResource::class,
|
||||
BlockedIPResource::class,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
Event::listen(\Illuminate\Auth\Events\Failed::class, [SecurityLoginListener::class, 'handleFailedLogin']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Security\Services;
|
||||
|
||||
use App\Models\User;
|
||||
use Paymenter\Extensions\Others\Security\Models\SuspendedAccount;
|
||||
use Paymenter\Extensions\Others\Security\Models\LoginAttempt;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class FraudDetectionService
|
||||
{
|
||||
public static function checkAndSuspendForFraud(User $user): bool
|
||||
{
|
||||
$extension = app(\Paymenter\Extensions\Others\Security\Security::class);
|
||||
$config = $extension->getConfig();
|
||||
|
||||
$autoSuspend = false;
|
||||
foreach ($config as $setting) {
|
||||
if ($setting['name'] === 'auto_suspend_fraud') {
|
||||
$autoSuspend = $setting['default'] ?? false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$autoSuspend) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$fraudScore = static::calculateFraudScore($user);
|
||||
|
||||
if ($fraudScore >= 70) {
|
||||
SuspendedAccount::suspendUser(
|
||||
$user->id,
|
||||
"Auto-suspended due to suspected fraudulent activity (Score: {$fraudScore})",
|
||||
Auth::id() ?? 1,
|
||||
false,
|
||||
now()->addDays(7),
|
||||
"Automated fraud detection triggered suspension"
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected static function calculateFraudScore(User $user): int
|
||||
{
|
||||
$score = 0;
|
||||
|
||||
$recentFailedLogins = LoginAttempt::where('email', $user->email)
|
||||
->where('successful', false)
|
||||
->where('attempted_at', '>', now()->subDay())
|
||||
->count();
|
||||
|
||||
if ($recentFailedLogins > 10) $score += 30;
|
||||
elseif ($recentFailedLogins > 5) $score += 15;
|
||||
|
||||
$recentIPs = LoginAttempt::where('email', $user->email)
|
||||
->where('attempted_at', '>', now()->subHours(2))
|
||||
->distinct('ip_address')
|
||||
->count();
|
||||
|
||||
if ($recentIPs > 3) $score += 25;
|
||||
elseif ($recentIPs > 2) $score += 10;
|
||||
|
||||
$rapidLogins = LoginAttempt::where('email', $user->email)
|
||||
->where('attempted_at', '>', now()->subMinutes(10))
|
||||
->count();
|
||||
|
||||
if ($rapidLogins > 10) $score += 35;
|
||||
elseif ($rapidLogins > 5) $score += 20;
|
||||
|
||||
return min($score, 100);
|
||||
}
|
||||
|
||||
public static function shouldFlagForReview(User $user): bool
|
||||
{
|
||||
$fraudScore = static::calculateFraudScore($user);
|
||||
return $fraudScore >= 40 && $fraudScore < 70;
|
||||
}
|
||||
}
|
||||
123
extensions/Others/Security/Services/SecurityAuthService.php
Normal file
123
extensions/Others/Security/Services/SecurityAuthService.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Security\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Paymenter\Extensions\Others\Security\Models\LoginAttempt;
|
||||
use Paymenter\Extensions\Others\Security\Models\BlockedIP;
|
||||
use Paymenter\Extensions\Others\Security\Models\SuspendedAccount;
|
||||
use Paymenter\Extensions\Others\Security\Services\VPNDetectionService;
|
||||
|
||||
class SecurityAuthService
|
||||
{
|
||||
public static function attemptLogin(array $credentials, bool $remember = false, string $ipAddress = null): array
|
||||
{
|
||||
$email = $credentials['email'] ?? '';
|
||||
$ipAddress = $ipAddress ?? request()->ip();
|
||||
|
||||
if (BlockedIP::isIPBlocked($ipAddress)) {
|
||||
$block = BlockedIP::getActiveBlock($ipAddress);
|
||||
|
||||
LoginAttempt::recordAttempt(
|
||||
$ipAddress,
|
||||
$email,
|
||||
false,
|
||||
request()->userAgent(),
|
||||
null,
|
||||
'Login blocked - IP blocked: ' . $block->reason
|
||||
);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Your IP address has been blocked. Reason: ' . $block->reason,
|
||||
'type' => 'ip_blocked'
|
||||
];
|
||||
}
|
||||
|
||||
if (VPNDetectionService::checkAndBlockVPN($ipAddress, $email)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Access denied. VPN/Proxy connections are not allowed.',
|
||||
'type' => 'vpn_blocked'
|
||||
];
|
||||
}
|
||||
|
||||
$user = \App\Models\User::where('email', $email)->first();
|
||||
if ($user && SuspendedAccount::isUserSuspended($user->id)) {
|
||||
$suspension = SuspendedAccount::getActiveSuspension($user->id);
|
||||
|
||||
LoginAttempt::recordAttempt(
|
||||
$ipAddress,
|
||||
$email,
|
||||
false,
|
||||
request()->userAgent(),
|
||||
null,
|
||||
'Login blocked - Account suspended: ' . $suspension->reason
|
||||
);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Your account has been suspended. Reason: ' . $suspension->reason,
|
||||
'type' => 'account_suspended'
|
||||
];
|
||||
}
|
||||
|
||||
if (!Auth::attempt($credentials, $remember)) {
|
||||
LoginAttempt::recordAttempt(
|
||||
$ipAddress,
|
||||
$email,
|
||||
false,
|
||||
request()->userAgent(),
|
||||
null,
|
||||
'Failed login attempt - Invalid credentials'
|
||||
);
|
||||
|
||||
BlockedIP::recordFailedAttempt($ipAddress);
|
||||
static::autoBlockIfNeeded($ipAddress);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'These credentials do not match our records.',
|
||||
'type' => 'invalid_credentials'
|
||||
];
|
||||
}
|
||||
|
||||
LoginAttempt::recordAttempt(
|
||||
$ipAddress,
|
||||
$email,
|
||||
true,
|
||||
request()->userAgent(),
|
||||
null,
|
||||
'Successful login'
|
||||
);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'message' => 'Login successful',
|
||||
'type' => 'success'
|
||||
];
|
||||
}
|
||||
|
||||
protected static function autoBlockIfNeeded(string $ipAddress): void
|
||||
{
|
||||
try {
|
||||
$extension = app(\Paymenter\Extensions\Others\Security\Security::class);
|
||||
$config = $extension->getConfig();
|
||||
|
||||
$maxAttempts = 5;
|
||||
$blockDuration = 60;
|
||||
|
||||
foreach ($config as $setting) {
|
||||
if ($setting['name'] === 'max_login_attempts') {
|
||||
$maxAttempts = $setting['default'] ?? 5;
|
||||
}
|
||||
if ($setting['name'] === 'ip_block_duration') {
|
||||
$blockDuration = $setting['default'] ?? 60;
|
||||
}
|
||||
}
|
||||
|
||||
BlockedIP::autoBlockIfNeeded($ipAddress, $maxAttempts, $blockDuration);
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
180
extensions/Others/Security/Services/VPNDetectionService.php
Normal file
180
extensions/Others/Security/Services/VPNDetectionService.php
Normal file
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Security\Services;
|
||||
|
||||
use Paymenter\Extensions\Others\Security\Models\BlockedIP;
|
||||
use Paymenter\Extensions\Others\Security\Models\LoginAttempt;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class VPNDetectionService
|
||||
{
|
||||
public static function checkAndBlockVPN(string $ipAddress, string $email = null, string $action = 'login'): bool
|
||||
{
|
||||
$extension = app(\Paymenter\Extensions\Others\Security\Security::class);
|
||||
$config = $extension->getConfig();
|
||||
|
||||
$blockVPNs = false;
|
||||
foreach ($config as $setting) {
|
||||
if ($setting['name'] === 'block_vpn_ips') {
|
||||
$blockVPNs = $setting['default'] ?? false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$blockVPNs) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$cacheKey = "vpn_check_{$ipAddress}";
|
||||
$isVPN = Cache::get($cacheKey);
|
||||
|
||||
if ($isVPN === null) {
|
||||
$isVPN = static::detectVPN($ipAddress);
|
||||
Cache::put($cacheKey, $isVPN, now()->addHours(6));
|
||||
}
|
||||
|
||||
if ($isVPN) {
|
||||
LoginAttempt::recordAttempt(
|
||||
$ipAddress,
|
||||
$email,
|
||||
false,
|
||||
request()->userAgent(),
|
||||
null,
|
||||
"Blocked VPN/Proxy IP detected during {$action}"
|
||||
);
|
||||
|
||||
BlockedIP::blockIP(
|
||||
$ipAddress,
|
||||
'Auto-blocked VPN/Proxy IP address',
|
||||
null,
|
||||
false,
|
||||
now()->addDays(1),
|
||||
'Detected as VPN/Proxy service'
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected static function detectVPN(string $ipAddress): bool
|
||||
{
|
||||
if (static::isPrivateIP($ipAddress)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (static::isKnownVPNRange($ipAddress)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (static::checkWithFreeAPI($ipAddress)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected static function isPrivateIP(string $ipAddress): bool
|
||||
{
|
||||
return !filter_var($ipAddress, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
|
||||
}
|
||||
|
||||
protected static function isKnownVPNRange(string $ipAddress): bool
|
||||
{
|
||||
$knownVPNRanges = [
|
||||
'185.220.', // Tor exit nodes
|
||||
'199.87.154.', // NordVPN
|
||||
'103.216.60.', // ExpressVPN
|
||||
'146.70.', // Surfshark
|
||||
'91.219.', // CyberGhost
|
||||
'185.159.', // ProtonVPN
|
||||
'104.244.', // Private Internet Access
|
||||
'149.154.', // Windscribe
|
||||
'193.148.', // IPVanish
|
||||
'185.246.', // Hide.me
|
||||
];
|
||||
|
||||
foreach ($knownVPNRanges as $range) {
|
||||
if (str_starts_with($ipAddress, $range)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected static function checkWithFreeAPI(string $ipAddress): bool
|
||||
{
|
||||
try {
|
||||
$response = Http::timeout(5)->get("http://ip-api.com/json/{$ipAddress}", [
|
||||
'fields' => 'proxy,hosting,query'
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
$data = $response->json();
|
||||
if (isset($data['proxy']) && $data['proxy']) {
|
||||
return true;
|
||||
}
|
||||
if (isset($data['hosting']) && $data['hosting']) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('VPN detection API failed', ['ip' => $ipAddress, 'error' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::timeout(5)->get("https://vpnapi.io/api/{$ipAddress}", [
|
||||
'key' => 'free'
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
$data = $response->json();
|
||||
if (isset($data['security']['vpn']) && $data['security']['vpn']) {
|
||||
return true;
|
||||
}
|
||||
if (isset($data['security']['proxy']) && $data['security']['proxy']) {
|
||||
return true;
|
||||
}
|
||||
if (isset($data['security']['tor']) && $data['security']['tor']) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('VPN detection API failed', ['ip' => $ipAddress, 'error' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function isVPNBlocked(string $ipAddress): bool
|
||||
{
|
||||
$extension = app(\Paymenter\Extensions\Others\Security\Security::class);
|
||||
$config = $extension->getConfig();
|
||||
|
||||
$blockVPNs = false;
|
||||
foreach ($config as $setting) {
|
||||
if ($setting['name'] === 'block_vpn_ips') {
|
||||
$blockVPNs = $setting['default'] ?? false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$blockVPNs) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$cacheKey = "vpn_check_{$ipAddress}";
|
||||
$isVPN = Cache::get($cacheKey);
|
||||
|
||||
if ($isVPN === null) {
|
||||
$isVPN = static::detectVPN($ipAddress);
|
||||
Cache::put($cacheKey, $isVPN, now()->addHours(6));
|
||||
}
|
||||
|
||||
return $isVPN;
|
||||
}
|
||||
}
|
||||
64
extensions/Others/Security/Traits/HasSecurityChecks.php
Normal file
64
extensions/Others/Security/Traits/HasSecurityChecks.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Paymenter\Extensions\Others\Security\Traits;
|
||||
|
||||
use Paymenter\Extensions\Others\Security\Models\BlockedIP;
|
||||
use Paymenter\Extensions\Others\Security\Models\SuspendedAccount;
|
||||
use Paymenter\Extensions\Others\Security\Services\VPNDetectionService;
|
||||
|
||||
trait HasSecurityChecks
|
||||
{
|
||||
protected function checkSecurityRestrictions(string $email, string $ipAddress = null): ?string
|
||||
{
|
||||
$ipAddress = $ipAddress ?? request()->ip();
|
||||
|
||||
if (BlockedIP::isIPBlocked($ipAddress)) {
|
||||
$block = BlockedIP::getActiveBlock($ipAddress);
|
||||
return 'Your IP address has been blocked. Reason: ' . $block->reason;
|
||||
}
|
||||
|
||||
if (VPNDetectionService::isVPNBlocked($ipAddress)) {
|
||||
VPNDetectionService::checkAndBlockVPN($ipAddress, $email);
|
||||
return 'Access denied. VPN/Proxy connections are not allowed.';
|
||||
}
|
||||
|
||||
$user = \App\Models\User::where('email', $email)->first();
|
||||
if ($user && SuspendedAccount::isUserSuspended($user->id)) {
|
||||
$suspension = SuspendedAccount::getActiveSuspension($user->id);
|
||||
return 'Your account has been suspended. Reason: ' . $suspension->reason;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function attemptSecureLogin(array $credentials, bool $remember = false): bool
|
||||
{
|
||||
$email = $credentials['email'] ?? '';
|
||||
$ipAddress = request()->ip();
|
||||
|
||||
$securityError = $this->checkSecurityRestrictions($email, $ipAddress);
|
||||
if ($securityError) {
|
||||
$this->addError('email', $securityError);
|
||||
return false;
|
||||
}
|
||||
|
||||
return \Illuminate\Support\Facades\Auth::attempt($credentials, $remember);
|
||||
}
|
||||
|
||||
protected function checkRegistrationSecurity(string $email, string $ipAddress = null): ?string
|
||||
{
|
||||
$ipAddress = $ipAddress ?? request()->ip();
|
||||
|
||||
if (BlockedIP::isIPBlocked($ipAddress)) {
|
||||
$block = BlockedIP::getActiveBlock($ipAddress);
|
||||
return 'Your IP address has been blocked. Reason: ' . $block->reason;
|
||||
}
|
||||
|
||||
if (VPNDetectionService::isVPNBlocked($ipAddress)) {
|
||||
VPNDetectionService::checkAndBlockVPN($ipAddress, $email, 'registration');
|
||||
return 'Access denied. VPN/Proxy connections are not allowed for registration.';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('ext_security_suspended_accounts', function (Blueprint $table) {
|
||||
$table->engine = 'InnoDB';
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('user_id');
|
||||
$table->string('reason');
|
||||
$table->text('notes')->nullable();
|
||||
$table->boolean('is_permanent')->default(false);
|
||||
$table->timestamp('suspended_at');
|
||||
$table->timestamp('suspended_until')->nullable();
|
||||
$table->unsignedBigInteger('suspended_by'); // Admin user ID
|
||||
$table->timestamp('lifted_at')->nullable();
|
||||
$table->unsignedBigInteger('lifted_by')->nullable(); // Admin user ID
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['user_id', 'is_active']);
|
||||
$table->index(['suspended_until']);
|
||||
$table->index(['is_permanent']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('ext_security_suspended_accounts');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('ext_security_blocked_ips', function (Blueprint $table) {
|
||||
$table->engine = 'InnoDB';
|
||||
$table->id();
|
||||
$table->string('ip_address');
|
||||
$table->string('reason');
|
||||
$table->text('notes')->nullable();
|
||||
$table->boolean('is_permanent')->default(false);
|
||||
$table->timestamp('blocked_at');
|
||||
$table->timestamp('blocked_until')->nullable();
|
||||
$table->unsignedBigInteger('blocked_by')->nullable(); // Admin user ID, null if auto-blocked
|
||||
$table->timestamp('unblocked_at')->nullable();
|
||||
$table->unsignedBigInteger('unblocked_by')->nullable(); // Admin user ID
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->integer('failed_login_attempts')->default(0);
|
||||
$table->timestamp('last_attempt_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['ip_address', 'is_active']);
|
||||
$table->index(['blocked_until']);
|
||||
$table->index(['is_permanent']);
|
||||
$table->index(['failed_login_attempts']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('ext_security_blocked_ips');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('ext_security_login_attempts', function (Blueprint $table) {
|
||||
$table->engine = 'InnoDB';
|
||||
$table->id();
|
||||
$table->string('ip_address');
|
||||
$table->string('email')->nullable();
|
||||
$table->boolean('successful')->default(false);
|
||||
$table->string('user_agent')->nullable();
|
||||
$table->string('country')->nullable();
|
||||
$table->text('notes')->nullable();
|
||||
$table->timestamp('attempted_at');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['ip_address', 'attempted_at']);
|
||||
$table->index(['email', 'attempted_at']);
|
||||
$table->index(['successful']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('ext_security_login_attempts');
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user