feat: add app and database modules
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
|
||||
class CheckActivePermission
|
||||
{
|
||||
public function handle(Request $request, Closure $next, $permission)
|
||||
{
|
||||
$cacheKey = "permission_status:{$permission}";
|
||||
|
||||
$isActive = Cache::remember($cacheKey, now()->addMinutes(5), function () use ($permission) {
|
||||
$permissionModel = Permission::where('name', $permission)->first();
|
||||
|
||||
return $permissionModel && $permissionModel->is_active;
|
||||
});
|
||||
|
||||
// If permission not found OR inactive -> deny access
|
||||
if (! $isActive) {
|
||||
abort(403, 'This permission is inactive or not available.');
|
||||
}
|
||||
|
||||
// Continue request (permission is active)
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class CheckLegalAgreement
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Closure(Request): (Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$user = Auth::user();
|
||||
|
||||
// Skip for guests
|
||||
if (! $user) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
// Avoid infinite redirect loop; also skip auth verification routes
|
||||
if ($request->routeIs('legal.*', 'verification.*', 'password.*') || $request->is('logout')) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
// Check if user has agreed to current ToS and PDP versions
|
||||
if (! $user->hasAgreedToCurrentLegal('tos') || ! $user->hasAgreedToCurrentLegal('privacy')) {
|
||||
return redirect()->route('legal.re-agree');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Middleware: CheckMenuPermission
|
||||
*
|
||||
* Protects parent menu route groups.
|
||||
* Allows access if the user has legacy menu-level permissions OR at least
|
||||
* one granular scoped tab permission within the given menu.
|
||||
*
|
||||
* Route usage:
|
||||
* ->middleware('menu-permission:global settings')
|
||||
* ->middleware('menu-permission:mobile settings,manage')
|
||||
*/
|
||||
class CheckMenuPermission
|
||||
{
|
||||
public function handle(Request $request, Closure $next, string $menu, string $action = 'view'): Response
|
||||
{
|
||||
if (! auth()->check()) {
|
||||
return $request->expectsJson()
|
||||
? response()->json(['message' => 'Unauthenticated.'], 401)
|
||||
: redirect()->route('login');
|
||||
}
|
||||
|
||||
$allowed = $action === 'manage'
|
||||
? can_manage_any_tab($menu)
|
||||
: can_view_any_tab($menu);
|
||||
|
||||
if (! $allowed) {
|
||||
return $request->expectsJson()
|
||||
? response()->json(['message' => 'This action is unauthorized.'], 403)
|
||||
: abort(403, "Access denied to menu: {$menu}");
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Middleware: CheckTabPermission
|
||||
*
|
||||
* Protects individual tab/section endpoints within a menu page.
|
||||
* Checks both scoped (e.g. "view global settings:login-security") and
|
||||
* legacy menu-level ("manage global settings") permissions.
|
||||
*
|
||||
* Route usage:
|
||||
* ->middleware('tab-permission:global settings,login-security')
|
||||
* ->middleware('tab-permission:mobile settings,branding,manage')
|
||||
*
|
||||
* Parameters:
|
||||
* $menu — the menu slug, e.g. "global settings"
|
||||
* $tab — the tab slug, e.g. "login-security"
|
||||
* $action — "view" (default) or "manage"
|
||||
*/
|
||||
class CheckTabPermission
|
||||
{
|
||||
public function handle(Request $request, Closure $next, string $menu, string $tab, string $action = 'view'): Response
|
||||
{
|
||||
if (! auth()->check()) {
|
||||
return $request->expectsJson()
|
||||
? response()->json(['message' => 'Unauthenticated.'], 401)
|
||||
: redirect()->route('login');
|
||||
}
|
||||
|
||||
$allowed = $action === 'manage'
|
||||
? can_manage_tab($menu, $tab)
|
||||
: can_view_tab($menu, $tab);
|
||||
|
||||
if (! $allowed) {
|
||||
return $request->expectsJson()
|
||||
? response()->json(['message' => 'This action is unauthorized.'], 403)
|
||||
: abort(403, "Access denied to tab: {$tab}");
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class GzipCompression
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Closure(Request): (Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$response = $next($request);
|
||||
|
||||
if (
|
||||
$response instanceof StreamedResponse ||
|
||||
$response instanceof BinaryFileResponse
|
||||
) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
if (
|
||||
extension_loaded('zlib') &&
|
||||
str_contains($request->header('Accept-Encoding'), 'gzip') &&
|
||||
function_exists('gzencode') &&
|
||||
! $response->headers->has('Content-Encoding')
|
||||
) {
|
||||
$content = $response->getContent();
|
||||
$compressedContent = gzencode($content, 6);
|
||||
|
||||
if ($compressedContent !== false) {
|
||||
$response->setContent($compressedContent);
|
||||
$response->headers->add([
|
||||
'Content-Encoding' => 'gzip',
|
||||
'Content-Length' => strlen($compressedContent),
|
||||
'X-Compressed' => 'true',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Services\Notification\TelegramService;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class IpAccessControl
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Closure(Request): (Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$ip = $request->ip();
|
||||
|
||||
// Batch get common settings to reduce function overhead
|
||||
$settings = [
|
||||
'blacklist' => get_setting('ip_blacklist', ''),
|
||||
'whitelist_admin' => get_setting('ip_whitelist_admin', ''),
|
||||
'auto_block' => get_setting('auto_block_ip', false),
|
||||
'single_session' => get_setting('session_single_session', false),
|
||||
'hsts' => get_setting('hsts_enabled', false),
|
||||
];
|
||||
|
||||
// 1. GLOBAL BLACKLIST
|
||||
$blacklistArr = array_filter(array_map('trim', explode(',', $settings['blacklist'])));
|
||||
if (in_array($ip, $blacklistArr)) {
|
||||
abort(403, 'Your IP address has been blocked.');
|
||||
}
|
||||
|
||||
// 2. ADMIN WHITELIST (Protects specific routes)
|
||||
// Check if current route is an admin/restricted route
|
||||
if ($request->is('system-config*') || $request->is('users*') || $request->is('roles*') || $request->is('permissions*') || $request->is('backups*') || $request->is('admin/*')) {
|
||||
$whitelistArr = array_filter(array_map('trim', explode(',', $settings['whitelist_admin'])));
|
||||
if (! empty($whitelistArr) && ! in_array($ip, $whitelistArr)) {
|
||||
abort(403, 'Access denied: Admin IP Whitelist restricted.');
|
||||
}
|
||||
}
|
||||
|
||||
// 3. RATE LIMITING & AUTO BLOCK
|
||||
if ($settings['auto_block']) {
|
||||
$cacheKey = "ip_block:{$ip}";
|
||||
if (Cache::has($cacheKey)) {
|
||||
abort(429, 'Your IP has been temporarily blocked due to excessive requests.');
|
||||
}
|
||||
|
||||
$threshold = get_setting('threshold_auto_block', 100);
|
||||
|
||||
$hitKey = "ip_hits:{$ip}";
|
||||
Cache::add($hitKey, 0, now()->addMinute());
|
||||
$hits = Cache::increment($hitKey);
|
||||
|
||||
if ($hits > $threshold) {
|
||||
Cache::put($cacheKey, true, now()->addHours(24));
|
||||
|
||||
// 🚨 Send Security Alert to Telegram
|
||||
try {
|
||||
$telegram = app(TelegramService::class);
|
||||
$msg = "<b>[FIREWALL BLOCK]</b>\n\n";
|
||||
$msg .= "IP Address: <code>{$ip}</code>\n";
|
||||
$msg .= "Reason: <b>Excessive Requests</b> ({$hits} hits)\n";
|
||||
$msg .= "Action: <b>Auto-Blocked (24h)</b>\n\n";
|
||||
$msg .= "Check configuration: <a href='".url('/system-config')."'>Admin Panel</a>";
|
||||
$telegram->sendMessage($msg);
|
||||
} catch (\Exception $e) {
|
||||
\Log::error('Firewall Telegram Alert Failed: '.$e->getMessage());
|
||||
}
|
||||
|
||||
abort(429, 'Excessive requests detected. Your IP has been blocked for 24 hours.');
|
||||
}
|
||||
}
|
||||
|
||||
// 4. SINGLE SESSION ENFORCEMENT
|
||||
// Skip check if we are currently impersonating to prevent logout
|
||||
if ($request->user() && $settings['single_session'] && ! session()->has('impersonator_id')) {
|
||||
if ($request->user()->last_session_id !== session()->getId()) {
|
||||
Auth::logout();
|
||||
|
||||
return redirect()->route('login')->with('error', 'You have been logged out because another device logged into your account.');
|
||||
}
|
||||
}
|
||||
|
||||
$response = $next($request);
|
||||
|
||||
// 5. HSTS (Transport Security)
|
||||
if ($request->isSecure() && $settings['hsts']) {
|
||||
$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Services\MobileConfig\MobileConfigService;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class MobileMaintenanceMiddleware
|
||||
{
|
||||
public function __construct(
|
||||
protected MobileConfigService $service
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
// Only apply to mobile API v1 routes
|
||||
if (! $request->is('api/v1/*')) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
$config = $this->service->all();
|
||||
$control = $config['control_center'] ?? [];
|
||||
$updates = $config['app_updates'] ?? [];
|
||||
|
||||
// 1. Check KILL SWITCH (Emergency Maintenance)
|
||||
if (filter_var($control['kill_switch_active'] ?? false, FILTER_VALIDATE_BOOLEAN)) {
|
||||
// Check IP Bypass
|
||||
$bypassIps = array_map('trim', explode(',', $control['maintenance_bypass_ips'] ?? ''));
|
||||
if (! in_array($request->ip(), $bypassIps)) {
|
||||
return response()->json([
|
||||
'status' => 'maintenance',
|
||||
'message' => $control['kill_switch_message'] ?? 'System is currently undergoing emergency maintenance.',
|
||||
'maintenance_info' => [
|
||||
'start' => $control['maintenance_start_at'] ?? null,
|
||||
'end' => $control['maintenance_end_at'] ?? null,
|
||||
],
|
||||
], 503);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Check APP VERSION (Force Update)
|
||||
$clientVersion = $request->query('v') ?: $request->header('X-App-Version');
|
||||
$minVersion = $updates['min_app_version'] ?? '1.0.0';
|
||||
|
||||
if ($clientVersion && version_compare($clientVersion, $minVersion, '<')) {
|
||||
return response()->json([
|
||||
'status' => 'upgrade_required',
|
||||
'message' => 'A mandatory update is required to continue using the application.',
|
||||
'current_version' => $clientVersion,
|
||||
'required_version' => $minVersion,
|
||||
'update_urls' => [
|
||||
'android' => $updates['store_url_android'] ?? null,
|
||||
'ios' => $updates['store_url_ios'] ?? null,
|
||||
],
|
||||
], 426); // 426 Upgrade Required
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Services\Auth\PasswordPolicyService;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class PasswordExpiryMiddleware
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
if (Auth::check()) {
|
||||
$user = Auth::user();
|
||||
|
||||
// Skip for specific routes to avoid infinite loops
|
||||
if ($request->is('profile/password*') || $request->is('logout')) {
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
if (PasswordPolicyService::isPasswordExpired($user)) {
|
||||
return redirect()->route('profile.edit')
|
||||
->with('warning', __('Your password has expired. Please update it to continue using the application.'));
|
||||
}
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class SecurityHeaders
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param Closure(Request): (Response) $next
|
||||
*/
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$response = $next($request);
|
||||
|
||||
// 🛡️ SECURITY HEADERS
|
||||
|
||||
// Prevent Clickjacking
|
||||
$response->headers->set('X-Frame-Options', 'SAMEORIGIN');
|
||||
|
||||
// Prevent Mime-Type Sniffing
|
||||
$response->headers->set('X-Content-Type-Options', 'nosniff');
|
||||
|
||||
// Cross-Site Scripting (XSS) Protection (for older browsers)
|
||||
$response->headers->set('X-XSS-Protection', '1; mode=block');
|
||||
|
||||
// Referrer Policy
|
||||
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||
|
||||
// Permissions Policy
|
||||
$response->headers->set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
|
||||
|
||||
// Content Security Policy — enforced (current CDN stack requires unsafe-inline/eval)
|
||||
$cdnSources = 'https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://cdn.datatables.net https://cdn.ckeditor.com https://unpkg.com https://code.jquery.com https://www.google.com https://www.gstatic.com';
|
||||
$csp = implode('; ', [
|
||||
"default-src 'self'",
|
||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval' {$cdnSources}",
|
||||
"style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://cdn.datatables.net https://unpkg.com https://fonts.googleapis.com",
|
||||
"font-src 'self' https://cdn.jsdelivr.net https://fonts.gstatic.com https://cdnjs.cloudflare.com",
|
||||
"img-src 'self' data: blob: https:",
|
||||
"connect-src 'self' https://o*.ingest.sentry.io wss:",
|
||||
"frame-src 'self' https://www.google.com",
|
||||
"object-src 'none'",
|
||||
"base-uri 'self'",
|
||||
"form-action 'self'",
|
||||
]);
|
||||
$response->headers->set('Content-Security-Policy', $csp);
|
||||
|
||||
// Report-Only: stricter policy (no unsafe-inline/eval) — violations logged in browser
|
||||
// console without blocking users. Use this to audit inline scripts before tightening
|
||||
// the enforced policy above.
|
||||
$cspReportOnly = implode('; ', [
|
||||
"default-src 'self'",
|
||||
"script-src 'self' {$cdnSources}",
|
||||
"style-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://cdn.datatables.net https://unpkg.com https://fonts.googleapis.com",
|
||||
"font-src 'self' https://cdn.jsdelivr.net https://fonts.gstatic.com https://cdnjs.cloudflare.com",
|
||||
"img-src 'self' data: blob: https:",
|
||||
"connect-src 'self' https://o*.ingest.sentry.io wss:",
|
||||
"frame-src 'self' https://www.google.com",
|
||||
"object-src 'none'",
|
||||
"base-uri 'self'",
|
||||
"form-action 'self'",
|
||||
]);
|
||||
$response->headers->set('Content-Security-Policy-Report-Only', $cspReportOnly);
|
||||
|
||||
// Strict-Transport-Security (Only if HTTPS)
|
||||
if ($request->isSecure()) {
|
||||
$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user