From 1d88c48df6023e4437a25ce108adc49189db90c1 Mon Sep 17 00:00:00 2001 From: Daniel Covington Date: Sun, 17 May 2026 14:12:58 -0400 Subject: [PATCH] Logins Reaady --- .claude/settings.local.json | 12 +- app/Controllers/AuthController.php | 71 + app/Views/auth/login.php | 52 + app/Views/partials/header.php | 13 + bootstrap/sentinel.php | 21 + composer.json | 8 +- composer.lock | 1831 ++++++++++++++++- config/sentinel.php | 66 + core/helpers.php | 8 +- ...20260517_000001_create_sentinel_tables.php | 99 + public/css/site.css | 94 + public/index.php | 2 + routes/web.php | 5 + scripts/create_user.php | 19 + 14 files changed, 2293 insertions(+), 8 deletions(-) create mode 100644 app/Controllers/AuthController.php create mode 100644 app/Views/auth/login.php create mode 100644 bootstrap/sentinel.php create mode 100644 config/sentinel.php create mode 100644 database/migrations/20260517_000001_create_sentinel_tables.php create mode 100644 scripts/create_user.php diff --git a/.claude/settings.local.json b/.claude/settings.local.json index c96683d..bdf13d0 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -4,7 +4,17 @@ "Bash(Get-ChildItem -Path \"c:\\\\Development\\\\PHP\\\\PHP-MVC-TERRITORY\" -Force)", "Bash(Select-Object Mode, Name)", "Bash(Format-Table -AutoSize)", - "PowerShell(Get-ChildItem -Path \"c:\\\\Development\\\\PHP\\\\PHP-MVC-TERRITORY\" -Force | Where-Object {$_.Name -match '^[A-Z]'} | Select-Object Mode, Name)" + "PowerShell(Get-ChildItem -Path \"c:\\\\Development\\\\PHP\\\\PHP-MVC-TERRITORY\" -Force | Where-Object {$_.Name -match '^[A-Z]'} | Select-Object Mode, Name)", + "Bash(Get-ChildItem -Recurse -Depth 3)", + "Bash(Select-Object -Property FullName, PSIsContainer)", + "PowerShell(Get-ChildItem -Path \"c:\\\\Development\\\\PHP\\\\PHP-MVC-TERRITORY\" -Recurse -Depth 3 | Select-Object -Property @{Name=\"Path\";Expression={$_.FullName.Substring\\(28\\)}}, PSIsContainer | Format-Table -AutoSize)", + "PowerShell(Get-ChildItem -Path \"c:\\\\Development\\\\PHP\\\\PHP-MVC-TERRITORY\" -Depth 1 | Where-Object { $_.PSIsContainer } | ForEach-Object { $_.Name })", + "Bash(composer install *)", + "PowerShell(docker exec php-mvc-territory-app-1 cat /var/www/html/vendor/cartalyst/sentinel/src/Native/SentinelBootstrapper.php 2>&1)", + "PowerShell(docker exec php-mvc-territory-app-1 cat /var/www/html/vendor/cartalyst/sentinel/src/Native/Facades/Sentinel.php 2>&1)", + "PowerShell(docker exec php-mvc-territory-app-1 cat /var/www/html/vendor/cartalyst/sentinel/src/Native/ConfigRepository.php 2>&1)", + "PowerShell(docker exec php-mvc-territory-app-1 cat /var/www/html/vendor/cartalyst/sentinel/src/config/config.php 2>&1)", + "Bash(docker-compose exec app bash -c ' *)" ] } } diff --git a/app/Controllers/AuthController.php b/app/Controllers/AuthController.php new file mode 100644 index 0000000..6cee947 --- /dev/null +++ b/app/Controllers/AuthController.php @@ -0,0 +1,71 @@ +redirect('/'); + } + + return $this->view('auth.login', ['pageTitle' => 'Login']); + } + + public function login(): Response + { + $request = Request::capture(); + + if (!verify_csrf_token($request->input('_token'))) { + return $this->view('auth.login', [ + 'pageTitle' => 'Login', + 'error' => 'Invalid request. Please try again.', + ]); + } + + $credentials = [ + 'email' => (string) $request->input('email'), + 'password' => (string) $request->input('password'), + ]; + + $remember = (bool) $request->input('remember'); + + try { + if (Sentinel::authenticate($credentials, $remember)) { + return $this->redirect('/'); + } + } catch (ThrottlingException $e) { + return $this->view('auth.login', [ + 'pageTitle' => 'Login', + 'error' => 'Too many failed attempts. Please wait ' . $e->getDelay() . ' seconds.', + ]); + } + + return $this->view('auth.login', [ + 'pageTitle' => 'Login', + 'error' => 'Invalid email or password.', + ]); + } + + public function logout(): Response + { + $request = Request::capture(); + + if (!verify_csrf_token($request->input('_token'))) { + return $this->redirect('/'); + } + + Sentinel::logout(); + + return $this->redirect('/login'); + } +} diff --git a/app/Views/auth/login.php b/app/Views/auth/login.php new file mode 100644 index 0000000..3018ced --- /dev/null +++ b/app/Views/auth/login.php @@ -0,0 +1,52 @@ +
+
+
+ Welcome back +

Sign in

+

Enter your credentials to access your account.

+
+ + + + + +
+ + +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+
diff --git a/app/Views/partials/header.php b/app/Views/partials/header.php index fc9392a..698c75a 100644 --- a/app/Views/partials/header.php +++ b/app/Views/partials/header.php @@ -2,6 +2,8 @@ declare(strict_types=1); +use Cartalyst\Sentinel\Native\Facades\Sentinel; + $navigationItems = [ ['label' => 'Home', 'href' => '/'], ['label' => 'Example JSON', 'href' => '/users/123'], @@ -9,6 +11,8 @@ $navigationItems = [ $currentPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH); $currentPath = is_string($currentPath) && $currentPath !== '' ? $currentPath : '/'; + +$currentUser = Sentinel::check(); ?> @@ -37,6 +41,15 @@ $currentPath = is_string($currentPath) && $currentPath !== '' ? $currentPath : ' + + +
+ + +
+ + Sign in + diff --git a/bootstrap/sentinel.php b/bootstrap/sentinel.php new file mode 100644 index 0000000..a08af10 --- /dev/null +++ b/bootstrap/sentinel.php @@ -0,0 +1,21 @@ +addConnection([ + 'driver' => 'sqlite', + 'database' => realpath(__DIR__ . '/../database/app.sqlite') ?: __DIR__ . '/../database/app.sqlite', + 'prefix' => '', +]); +$capsule->setAsGlobal(); +$capsule->bootEloquent(); + +$bootstrapper = new SentinelBootstrapper(__DIR__ . '/../config/sentinel.php'); +Sentinel::instance($bootstrapper); diff --git a/composer.json b/composer.json index fa53963..ea1ec0b 100644 --- a/composer.json +++ b/composer.json @@ -18,5 +18,11 @@ "migrate:fresh": "php scripts/migrate.php fresh", "migrate:fresh-seed": "php scripts/migrate.php fresh --seed" }, - "require": {} + "require": { + "php": ">=8.2", + "cartalyst/sentinel": "^7.0", + "illuminate/database": "^10.0", + "illuminate/events": "^10.0", + "symfony/http-foundation": "^6.0" + } } diff --git a/composer.lock b/composer.lock index 75912c3..e7c7556 100644 --- a/composer.lock +++ b/composer.lock @@ -4,15 +4,1840 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a9e5ff0daf78f24b652c32b38b47d81b", - "packages": [], + "content-hash": "de4b60164384367176d2fa1cea77c9e1", + "packages": [ + { + "name": "brick/math", + "version": "0.12.3", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/866551da34e9a618e64a819ee1e01c20d8a588ba", + "reference": "866551da34e9a618e64a819ee1e01c20d8a588ba", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^10.1", + "vimeo/psalm": "6.8.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "bignumber", + "brick", + "decimal", + "integer", + "math", + "mathematics", + "rational" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.12.3" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2025-02-28T13:11:00+00:00" + }, + { + "name": "carbonphp/carbon-doctrine-types", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git", + "reference": "99f76ffa36cce3b70a4a6abce41dba15ca2e84cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/99f76ffa36cce3b70a4a6abce41dba15ca2e84cb", + "reference": "99f76ffa36cce3b70a4a6abce41dba15ca2e84cb", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "doctrine/dbal": "<3.7.0 || >=4.0.0" + }, + "require-dev": { + "doctrine/dbal": "^3.7.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/2.1.0" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2023-12-11T17:09:12+00:00" + }, + { + "name": "cartalyst/sentinel", + "version": "v7.0.2", + "source": { + "type": "git", + "url": "https://github.com/cartalyst/sentinel.git", + "reference": "a4221b5976f0a87ea814406f049b44df87c56d4b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cartalyst/sentinel/zipball/a4221b5976f0a87ea814406f049b44df87c56d4b", + "reference": "a4221b5976f0a87ea814406f049b44df87c56d4b", + "shasum": "" + }, + "require": { + "cartalyst/support": "^7.0", + "illuminate/support": "^10.0", + "php": "^8.1" + }, + "require-dev": { + "cartalyst/php-cs-fixer-config": "^2.0", + "illuminate/cookie": "^10.0", + "illuminate/database": "^10.0", + "illuminate/events": "^10.0", + "illuminate/http": "^10.0", + "illuminate/session": "^10.0", + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^9.0" + }, + "suggest": { + "illuminate/database": "By default, Sentinel utilizes the powerful Illuminate database layer.", + "illuminate/events": "To hook into various events across Sentinel, we recommend using Illuminate's event dispatcher.", + "symfony/http-foundation": "Required for native implementations." + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Reminder": "Cartalyst\\Sentinel\\Laravel\\Facades\\Reminder", + "Sentinel": "Cartalyst\\Sentinel\\Laravel\\Facades\\Sentinel", + "Activation": "Cartalyst\\Sentinel\\Laravel\\Facades\\Activation" + }, + "providers": [ + "Cartalyst\\Sentinel\\Laravel\\SentinelServiceProvider" + ] + }, + "component": "package", + "branch-alias": { + "dev-master": "7.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Cartalyst\\Sentinel\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Cartalyst LLC", + "email": "help@cartalyst.com", + "homepage": "https://cartalyst.com" + } + ], + "description": "PHP 7.3+ Fully-featured Authentication & Authorization System", + "keywords": [ + "auth", + "cartalyst", + "laravel", + "php", + "security" + ], + "support": { + "issues": "https://github.com/cartalyst/sentinel/issues", + "source": "https://github.com/cartalyst/sentinel/tree/v7.0.2" + }, + "time": "2023-08-10T19:54:17+00:00" + }, + { + "name": "cartalyst/support", + "version": "v7.0.0", + "source": { + "type": "git", + "url": "https://github.com/cartalyst/support.git", + "reference": "21668d0808f5c4698917e69ea1adbe098f9b4728" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cartalyst/support/zipball/21668d0808f5c4698917e69ea1adbe098f9b4728", + "reference": "21668d0808f5c4698917e69ea1adbe098f9b4728", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "cartalyst/php-cs-fixer-config": "^2.0", + "illuminate/mail": "^10.0", + "illuminate/validation": "^10.0", + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^9.0", + "symfony/translation": "^6.2" + }, + "type": "library", + "extra": { + "component": "package", + "branch-alias": { + "dev-master": "7.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Cartalyst\\Support\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Cartalyst LLC", + "email": "help@cartalyst.com", + "homepage": "https://cartalyst.com" + } + ], + "description": "Support helpers.", + "keywords": [ + "cartalyst", + "helper", + "laravel", + "support" + ], + "support": { + "issues": "https://github.com/cartalyst/support/issues", + "source": "https://github.com/cartalyst/support/tree/v7.0.0" + }, + "time": "2023-02-22T21:51:13+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.1.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2025-08-10T19:31:58+00:00" + }, + { + "name": "illuminate/bus", + "version": "v10.49.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/bus.git", + "reference": "053f902d546d719c3f2752f7d3805a466e317312" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/bus/zipball/053f902d546d719c3f2752f7d3805a466e317312", + "reference": "053f902d546d719c3f2752f7d3805a466e317312", + "shasum": "" + }, + "require": { + "illuminate/collections": "^10.0", + "illuminate/contracts": "^10.0", + "illuminate/pipeline": "^10.0", + "illuminate/support": "^10.0", + "php": "^8.1" + }, + "suggest": { + "illuminate/queue": "Required to use closures when chaining jobs (^7.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "10.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Bus\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Bus package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-03-24T11:47:24+00:00" + }, + { + "name": "illuminate/collections", + "version": "v10.49.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/collections.git", + "reference": "6ae9c74fa92d4e1824d1b346cd435e8eacdc3232" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/collections/zipball/6ae9c74fa92d4e1824d1b346cd435e8eacdc3232", + "reference": "6ae9c74fa92d4e1824d1b346cd435e8eacdc3232", + "shasum": "" + }, + "require": { + "illuminate/conditionable": "^10.0", + "illuminate/contracts": "^10.0", + "illuminate/macroable": "^10.0", + "php": "^8.1" + }, + "suggest": { + "symfony/var-dumper": "Required to use the dump method (^6.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "10.x-dev" + } + }, + "autoload": { + "files": [ + "helpers.php" + ], + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Collections package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-09-08T19:05:53+00:00" + }, + { + "name": "illuminate/conditionable", + "version": "v10.49.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/conditionable.git", + "reference": "47c700320b7a419f0d188d111f3bbed978fcbd3f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/conditionable/zipball/47c700320b7a419f0d188d111f3bbed978fcbd3f", + "reference": "47c700320b7a419f0d188d111f3bbed978fcbd3f", + "shasum": "" + }, + "require": { + "php": "^8.0.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "10.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Conditionable package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-03-24T11:47:24+00:00" + }, + { + "name": "illuminate/container", + "version": "v10.49.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/container.git", + "reference": "b4956de5de18524c21ef36221a8ffd7fa3b534db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/container/zipball/b4956de5de18524c21ef36221a8ffd7fa3b534db", + "reference": "b4956de5de18524c21ef36221a8ffd7fa3b534db", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^10.0", + "php": "^8.1", + "psr/container": "^1.1.1|^2.0.1" + }, + "provide": { + "psr/container-implementation": "1.1|2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "10.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Container\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Container package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-03-24T11:47:24+00:00" + }, + { + "name": "illuminate/contracts", + "version": "v10.49.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/contracts.git", + "reference": "2393ef579e020d88e24283913c815c3e2c143323" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/contracts/zipball/2393ef579e020d88e24283913c815c3e2c143323", + "reference": "2393ef579e020d88e24283913c815c3e2c143323", + "shasum": "" + }, + "require": { + "php": "^8.1", + "psr/container": "^1.1.1|^2.0.1", + "psr/simple-cache": "^1.0|^2.0|^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "10.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Contracts\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Contracts package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-03-24T11:47:24+00:00" + }, + { + "name": "illuminate/database", + "version": "v10.49.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/database.git", + "reference": "711519fa4eca9c55d4f3d6680ffca71b28317e7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/database/zipball/711519fa4eca9c55d4f3d6680ffca71b28317e7a", + "reference": "711519fa4eca9c55d4f3d6680ffca71b28317e7a", + "shasum": "" + }, + "require": { + "brick/math": "^0.9.3|^0.10.2|^0.11|^0.12", + "ext-pdo": "*", + "illuminate/collections": "^10.0", + "illuminate/container": "^10.0", + "illuminate/contracts": "^10.0", + "illuminate/macroable": "^10.0", + "illuminate/support": "^10.0", + "php": "^8.1" + }, + "conflict": { + "carbonphp/carbon-doctrine-types": ">=3.0", + "doctrine/dbal": ">=4.0" + }, + "suggest": { + "doctrine/dbal": "Required to rename columns and drop SQLite columns (^3.5.1).", + "ext-filter": "Required to use the Postgres database driver.", + "fakerphp/faker": "Required to use the eloquent factory builder (^1.21).", + "illuminate/console": "Required to use the database commands (^10.0).", + "illuminate/events": "Required to use the observers with Eloquent (^10.0).", + "illuminate/filesystem": "Required to use the migrations (^10.0).", + "illuminate/pagination": "Required to paginate the result set (^10.0).", + "symfony/finder": "Required to use Eloquent model factories (^6.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "10.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Database\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Database package.", + "homepage": "https://laravel.com", + "keywords": [ + "database", + "laravel", + "orm", + "sql" + ], + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-03-24T11:47:24+00:00" + }, + { + "name": "illuminate/events", + "version": "v10.49.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/events.git", + "reference": "4a8e4fbc95c7e46aa6152fd8c900d56e5ef538cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/events/zipball/4a8e4fbc95c7e46aa6152fd8c900d56e5ef538cf", + "reference": "4a8e4fbc95c7e46aa6152fd8c900d56e5ef538cf", + "shasum": "" + }, + "require": { + "illuminate/bus": "^10.0", + "illuminate/collections": "^10.0", + "illuminate/container": "^10.0", + "illuminate/contracts": "^10.0", + "illuminate/macroable": "^10.0", + "illuminate/support": "^10.0", + "php": "^8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "10.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php" + ], + "psr-4": { + "Illuminate\\Events\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Events package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-03-24T11:47:24+00:00" + }, + { + "name": "illuminate/macroable", + "version": "v10.49.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/macroable.git", + "reference": "dff667a46ac37b634dcf68909d9d41e94dc97c27" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/macroable/zipball/dff667a46ac37b634dcf68909d9d41e94dc97c27", + "reference": "dff667a46ac37b634dcf68909d9d41e94dc97c27", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "10.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Macroable package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2023-06-05T12:46:42+00:00" + }, + { + "name": "illuminate/pipeline", + "version": "v10.49.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/pipeline.git", + "reference": "c12e4f1d8a1fbecdc1e0fa4dc9fe17b4315832e9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/pipeline/zipball/c12e4f1d8a1fbecdc1e0fa4dc9fe17b4315832e9", + "reference": "c12e4f1d8a1fbecdc1e0fa4dc9fe17b4315832e9", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^10.0", + "illuminate/support": "^10.0", + "php": "^8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "10.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Pipeline\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Pipeline package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-03-24T11:47:24+00:00" + }, + { + "name": "illuminate/support", + "version": "v10.49.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/support.git", + "reference": "28b505e671dbe119e4e32a75c78f87189d046e39" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/support/zipball/28b505e671dbe119e4e32a75c78f87189d046e39", + "reference": "28b505e671dbe119e4e32a75c78f87189d046e39", + "shasum": "" + }, + "require": { + "doctrine/inflector": "^2.0", + "ext-ctype": "*", + "ext-filter": "*", + "ext-mbstring": "*", + "illuminate/collections": "^10.0", + "illuminate/conditionable": "^10.0", + "illuminate/contracts": "^10.0", + "illuminate/macroable": "^10.0", + "nesbot/carbon": "^2.67", + "php": "^8.1", + "voku/portable-ascii": "^2.0" + }, + "conflict": { + "tightenco/collect": "<5.5.33" + }, + "suggest": { + "illuminate/filesystem": "Required to use the composer class (^10.0).", + "league/commonmark": "Required to use Str::markdown() and Stringable::markdown() (^2.6).", + "ramsey/uuid": "Required to use Str::uuid() (^4.7).", + "symfony/process": "Required to use the composer class (^6.2).", + "symfony/uid": "Required to use Str::ulid() (^6.2).", + "symfony/var-dumper": "Required to use the dd function (^6.2).", + "vlucas/phpdotenv": "Required to use the Env class and env helper (^5.4.1)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "10.x-dev" + } + }, + "autoload": { + "files": [ + "helpers.php" + ], + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Support package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-09-08T19:05:53+00:00" + }, + { + "name": "nesbot/carbon", + "version": "2.73.0", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "9228ce90e1035ff2f0db84b40ec2e023ed802075" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/9228ce90e1035ff2f0db84b40ec2e023ed802075", + "reference": "9228ce90e1035ff2f0db84b40ec2e023ed802075", + "shasum": "" + }, + "require": { + "carbonphp/carbon-doctrine-types": "*", + "ext-json": "*", + "php": "^7.1.8 || ^8.0", + "psr/clock": "^1.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/polyfill-php80": "^1.16", + "symfony/translation": "^3.4 || ^4.0 || ^5.0 || ^6.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^2.0 || ^3.1.4 || ^4.0", + "doctrine/orm": "^2.7 || ^3.0", + "friendsofphp/php-cs-fixer": "^3.0", + "kylekatarnls/multi-tester": "^2.0", + "ondrejmirtes/better-reflection": "<6", + "phpmd/phpmd": "^2.9", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^0.12.99 || ^1.7.14", + "phpunit/php-file-iterator": "^2.0.5 || ^3.0.6", + "phpunit/phpunit": "^7.5.20 || ^8.5.26 || ^9.5.20", + "squizlabs/php_codesniffer": "^3.4" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbon.nesbot.com", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbon.nesbot.com/docs", + "issues": "https://github.com/briannesbitt/Carbon/issues", + "source": "https://github.com/briannesbitt/Carbon" + }, + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2025-01-08T20:10:23+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-13T15:52:40+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v6.4.35", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "cffffd0a2c037117b742b4f8b379a22a2a33f6d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/cffffd0a2c037117b742b4f8b379a22a2a33f6d2", + "reference": "cffffd0a2c037117b742b4f8b379a22a2a33f6d2", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php83": "^1.27" + }, + "conflict": { + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^2.13.1|^3|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/rate-limiter": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v6.4.35" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-06T11:15:58+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315", + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T17:25:58+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/3600c2cb22399e25bb226e4a135ce91eeb2a6149", + "reference": "3600c2cb22399e25bb226e4a135ce91eeb2a6149", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T17:25:58+00:00" + }, + { + "name": "symfony/translation", + "version": "v6.4.38", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "afaa31b0c12d9a659eed1ea97f268a614cc1299c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/afaa31b0c12d9a659eed1ea97f268a614cc1299c", + "reference": "afaa31b0c12d9a659eed1ea97f268a614cc1299c", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^2.5|^3.0" + }, + "conflict": { + "symfony/config": "<5.4", + "symfony/console": "<5.4", + "symfony/dependency-injection": "<5.4", + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<5.4", + "symfony/service-contracts": "<2.5", + "symfony/twig-bundle": "<5.4", + "symfony/yaml": "<5.4" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^4.18|^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/intl": "^5.4|^6.0|^7.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v6.4.38" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-06T08:55:54+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/0ab302977a952b42fd51475c4ebac81f8da0a95d", + "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-05T13:30:16+00:00" + }, + { + "name": "voku/portable-ascii", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/voku/portable-ascii.git", + "reference": "8e1051fe39379367aecf014f41744ce7539a856f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/8e1051fe39379367aecf014f41744ce7539a856f", + "reference": "8e1051fe39379367aecf014f41744ce7539a856f", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpunit/phpunit": "~8.5 || ~9.6 || ~10.5 || ~11.5" + }, + "suggest": { + "ext-intl": "Use Intl for transliterator_transliterate() support" + }, + "type": "library", + "autoload": { + "psr-4": { + "voku\\": "src/voku/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lars Moelleken", + "homepage": "https://www.moelleken.org/" + } + ], + "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", + "homepage": "https://github.com/voku/portable-ascii", + "keywords": [ + "ascii", + "clean", + "php" + ], + "support": { + "issues": "https://github.com/voku/portable-ascii/issues", + "source": "https://github.com/voku/portable-ascii/tree/2.1.1" + }, + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://opencollective.com/portable-ascii", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", + "type": "tidelift" + } + ], + "time": "2026-04-26T05:33:54+00:00" + } + ], "packages-dev": [], "aliases": [], "minimum-stability": "stable", "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, - "platform": {}, + "platform": { + "php": ">=8.2" + }, "platform-dev": {}, "plugin-api-version": "2.9.0" } diff --git a/config/sentinel.php b/config/sentinel.php new file mode 100644 index 0000000..fd387f9 --- /dev/null +++ b/config/sentinel.php @@ -0,0 +1,66 @@ + 'cartalyst_sentinel', + + 'cookie' => 'cartalyst_sentinel', + + 'users' => [ + 'model' => 'Cartalyst\Sentinel\Users\EloquentUser', + ], + + 'roles' => [ + 'model' => 'Cartalyst\Sentinel\Roles\EloquentRole', + ], + + 'permissions' => [ + 'class' => 'Cartalyst\Sentinel\Permissions\StandardPermissions', + ], + + 'persistences' => [ + 'model' => 'Cartalyst\Sentinel\Persistences\EloquentPersistence', + 'single' => false, + ], + + // Only throttle is enabled; activation can be re-added when a registration flow exists. + 'checkpoints' => [ + 'throttle', + ], + + 'activations' => [ + 'model' => 'Cartalyst\Sentinel\Activations\EloquentActivation', + 'expires' => 259200, + 'lottery' => [2, 100], + ], + + 'reminders' => [ + 'model' => 'Cartalyst\Sentinel\Reminders\EloquentReminder', + 'expires' => 14400, + 'lottery' => [2, 100], + ], + + 'throttling' => [ + 'model' => 'Cartalyst\Sentinel\Throttling\EloquentThrottle', + 'global' => [ + 'interval' => 900, + 'thresholds' => [ + 10 => 1, + 20 => 2, + 30 => 4, + 40 => 8, + 50 => 16, + 60 => 32, + ], + ], + 'ip' => [ + 'interval' => 900, + 'thresholds' => 5, + ], + 'user' => [ + 'interval' => 900, + 'thresholds' => 5, + ], + ], +]; diff --git a/core/helpers.php b/core/helpers.php index fffc7cb..b00f086 100644 --- a/core/helpers.php +++ b/core/helpers.php @@ -94,9 +94,11 @@ function prepareSqliteDatabase(string $dsn): void } } -function e(?string $value): string -{ - return htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); +if (!function_exists('e')) { + function e(?string $value): string + { + return htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + } } function asset(string $path): string diff --git a/database/migrations/20260517_000001_create_sentinel_tables.php b/database/migrations/20260517_000001_create_sentinel_tables.php new file mode 100644 index 0000000..4ba789c --- /dev/null +++ b/database/migrations/20260517_000001_create_sentinel_tables.php @@ -0,0 +1,99 @@ +execute(' + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email VARCHAR(255) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + permissions TEXT, + last_login DATETIME, + first_name VARCHAR(255), + last_name VARCHAR(255), + created_at DATETIME, + updated_at DATETIME + ) + '); + + $database->execute(' + CREATE TABLE IF NOT EXISTS roles ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + slug VARCHAR(255) NOT NULL UNIQUE, + name VARCHAR(255) NOT NULL, + permissions TEXT, + created_at DATETIME, + updated_at DATETIME + ) + '); + + $database->execute(' + CREATE TABLE IF NOT EXISTS role_users ( + user_id INTEGER NOT NULL, + role_id INTEGER NOT NULL, + created_at DATETIME, + updated_at DATETIME, + PRIMARY KEY (user_id, role_id) + ) + '); + + $database->execute(' + CREATE TABLE IF NOT EXISTS persistences ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + code VARCHAR(255) NOT NULL UNIQUE, + created_at DATETIME, + updated_at DATETIME + ) + '); + + $database->execute(' + CREATE TABLE IF NOT EXISTS activations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + code VARCHAR(255) NOT NULL, + completed INTEGER NOT NULL DEFAULT 0, + completed_at DATETIME, + created_at DATETIME, + updated_at DATETIME + ) + '); + + $database->execute(' + CREATE TABLE IF NOT EXISTS reminders ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + code VARCHAR(255) NOT NULL, + completed INTEGER NOT NULL DEFAULT 0, + completed_at DATETIME, + created_at DATETIME, + updated_at DATETIME + ) + '); + + $database->execute(' + CREATE TABLE IF NOT EXISTS throttle ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER, + type VARCHAR(255) NOT NULL, + ip VARCHAR(255), + created_at DATETIME, + updated_at DATETIME + ) + '); + } + + public function down(Database $database): void + { + foreach (['throttle', 'reminders', 'activations', 'persistences', 'role_users', 'roles', 'users'] as $table) { + $database->execute("DROP TABLE IF EXISTS {$table}"); + } + } +}; diff --git a/public/css/site.css b/public/css/site.css index c99ebd9..0809d55 100644 --- a/public/css/site.css +++ b/public/css/site.css @@ -466,3 +466,97 @@ code { font-size: 2.5rem; } } + +/* ── Auth / Login ─────────────────────────────────────── */ + +.auth-wrap { + display: flex; + justify-content: center; + align-items: flex-start; + padding: 2rem 0; +} + +.auth-card { + width: 100%; + max-width: 460px; + background: var(--surface); + border: 1px solid var(--surface-border); + box-shadow: var(--shadow-card); + border-radius: 2rem; + padding: 2.5rem 2.5rem 2rem; + display: grid; + gap: 1.5rem; +} + +.auth-card-header { + display: grid; + gap: 0.4rem; +} + +.auth-card-title { + margin: 0; + font-size: 2rem; + letter-spacing: -0.04em; + line-height: 1.1; +} + +.auth-card-subtitle { + margin: 0; + color: var(--text-secondary); + font-size: 0.97rem; + line-height: 1.6; +} + +.auth-form { + display: grid; + gap: 1.1rem; +} + +.auth-remember { + display: flex; + align-items: center; + gap: 0.55rem; + font-size: 0.93rem; + color: var(--text-secondary); + cursor: pointer; +} + +.auth-remember input[type="checkbox"] { + width: 1rem; + height: 1rem; + accent-color: var(--accent); + cursor: pointer; +} + +.auth-submit { + width: 100%; + justify-content: center; + padding: 0.95rem; + font-size: 1rem; + margin-top: 0.25rem; +} + +.auth-nav-form { + display: inline; + margin: 0; + padding: 0; +} + +.auth-nav-btn { + background: none; + border: none; + padding: 0.7rem 1rem; + font: inherit; + font-weight: 600; + color: var(--text-secondary); + border-radius: 999px; + cursor: pointer; + transition: background-color 160ms ease, color 160ms ease, transform 160ms ease; +} + +.auth-nav-btn:hover, +.auth-nav-btn:focus-visible { + color: var(--accent-strong); + background: rgba(29, 122, 109, 0.12); + transform: translateY(-1px); +} diff --git a/public/index.php b/public/index.php index 9d26c73..eceeea1 100644 --- a/public/index.php +++ b/public/index.php @@ -11,6 +11,8 @@ use Core\Router; ensureSessionStarted(); +require_once __DIR__ . '/../bootstrap/sentinel.php'; + $app = new App(); $router = new Router(); diff --git a/routes/web.php b/routes/web.php index 9a631e8..e11f652 100644 --- a/routes/web.php +++ b/routes/web.php @@ -3,6 +3,11 @@ declare(strict_types=1); use App\Controllers\HomeController; +use App\Controllers\AuthController; $router->get('/', [HomeController::class, 'index']); $router->get('/users/{id}', [HomeController::class, 'user']); + +$router->get('/login', [AuthController::class, 'showLogin']); +$router->post('/login', [AuthController::class, 'login']); +$router->post('/logout', [AuthController::class, 'logout']); diff --git a/scripts/create_user.php b/scripts/create_user.php new file mode 100644 index 0000000..a9c546d --- /dev/null +++ b/scripts/create_user.php @@ -0,0 +1,19 @@ + 'admin@example.com', + 'password' => 'secret123', + 'first_name' => 'Admin', + 'last_name' => 'User', +]); + +echo $user ? 'User created: ' . $user->email . PHP_EOL : 'Failed' . PHP_EOL;