Compare commits
No commits in common. "b7d1de44c16e4cc341c2db4ec5c447f3f90a1075" and "b8971552d7fbac1d807d93afce89c0dd888abb49" have entirely different histories.
b7d1de44c1
...
b8971552d7
11 changed files with 41 additions and 845 deletions
|
|
@ -21,7 +21,6 @@
|
||||||
"symfony/expression-language": "8.1.*",
|
"symfony/expression-language": "8.1.*",
|
||||||
"symfony/flex": "^2",
|
"symfony/flex": "^2",
|
||||||
"symfony/framework-bundle": "8.1.*",
|
"symfony/framework-bundle": "8.1.*",
|
||||||
"symfony/monolog-bundle": "^4.0",
|
|
||||||
"symfony/property-access": "8.1.*",
|
"symfony/property-access": "8.1.*",
|
||||||
"symfony/property-info": "8.1.*",
|
"symfony/property-info": "8.1.*",
|
||||||
"symfony/runtime": "8.1.*",
|
"symfony/runtime": "8.1.*",
|
||||||
|
|
|
||||||
261
composer.lock
generated
261
composer.lock
generated
|
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "1c5925d178f70d63abb028cf3a4e4aaa",
|
"content-hash": "8096a72d560f2d6e1c5aafb6175ba005",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "api-platform/doctrine-common",
|
"name": "api-platform/doctrine-common",
|
||||||
|
|
@ -2444,109 +2444,6 @@
|
||||||
},
|
},
|
||||||
"time": "2026-02-08T16:21:46+00:00"
|
"time": "2026-02-08T16:21:46+00:00"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "monolog/monolog",
|
|
||||||
"version": "3.10.0",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/Seldaek/monolog.git",
|
|
||||||
"reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0",
|
|
||||||
"reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"php": ">=8.1",
|
|
||||||
"psr/log": "^2.0 || ^3.0"
|
|
||||||
},
|
|
||||||
"provide": {
|
|
||||||
"psr/log-implementation": "3.0.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"aws/aws-sdk-php": "^3.0",
|
|
||||||
"doctrine/couchdb": "~1.0@dev",
|
|
||||||
"elasticsearch/elasticsearch": "^7 || ^8",
|
|
||||||
"ext-json": "*",
|
|
||||||
"graylog2/gelf-php": "^1.4.2 || ^2.0",
|
|
||||||
"guzzlehttp/guzzle": "^7.4.5",
|
|
||||||
"guzzlehttp/psr7": "^2.2",
|
|
||||||
"mongodb/mongodb": "^1.8 || ^2.0",
|
|
||||||
"php-amqplib/php-amqplib": "~2.4 || ^3",
|
|
||||||
"php-console/php-console": "^3.1.8",
|
|
||||||
"phpstan/phpstan": "^2",
|
|
||||||
"phpstan/phpstan-deprecation-rules": "^2",
|
|
||||||
"phpstan/phpstan-strict-rules": "^2",
|
|
||||||
"phpunit/phpunit": "^10.5.17 || ^11.0.7",
|
|
||||||
"predis/predis": "^1.1 || ^2",
|
|
||||||
"rollbar/rollbar": "^4.0",
|
|
||||||
"ruflin/elastica": "^7 || ^8",
|
|
||||||
"symfony/mailer": "^5.4 || ^6",
|
|
||||||
"symfony/mime": "^5.4 || ^6"
|
|
||||||
},
|
|
||||||
"suggest": {
|
|
||||||
"aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
|
|
||||||
"doctrine/couchdb": "Allow sending log messages to a CouchDB server",
|
|
||||||
"elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client",
|
|
||||||
"ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
|
|
||||||
"ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler",
|
|
||||||
"ext-mbstring": "Allow to work properly with unicode symbols",
|
|
||||||
"ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)",
|
|
||||||
"ext-openssl": "Required to send log messages using SSL",
|
|
||||||
"ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)",
|
|
||||||
"graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
|
|
||||||
"mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)",
|
|
||||||
"php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
|
|
||||||
"rollbar/rollbar": "Allow sending log messages to Rollbar",
|
|
||||||
"ruflin/elastica": "Allow sending log messages to an Elastic Search server"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"extra": {
|
|
||||||
"branch-alias": {
|
|
||||||
"dev-main": "3.x-dev"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"Monolog\\": "src/Monolog"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Jordi Boggiano",
|
|
||||||
"email": "j.boggiano@seld.be",
|
|
||||||
"homepage": "https://seld.be"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Sends your logs to files, sockets, inboxes, databases and various web services",
|
|
||||||
"homepage": "https://github.com/Seldaek/monolog",
|
|
||||||
"keywords": [
|
|
||||||
"log",
|
|
||||||
"logging",
|
|
||||||
"psr-3"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/Seldaek/monolog/issues",
|
|
||||||
"source": "https://github.com/Seldaek/monolog/tree/3.10.0"
|
|
||||||
},
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"url": "https://github.com/Seldaek",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://tidelift.com/funding/github/packagist/monolog/monolog",
|
|
||||||
"type": "tidelift"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"time": "2026-01-02T08:56:05+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "nelmio/cors-bundle",
|
"name": "nelmio/cors-bundle",
|
||||||
"version": "2.6.1",
|
"version": "2.6.1",
|
||||||
|
|
@ -4834,162 +4731,6 @@
|
||||||
],
|
],
|
||||||
"time": "2026-05-29T08:46:08+00:00"
|
"time": "2026-05-29T08:46:08+00:00"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "symfony/monolog-bridge",
|
|
||||||
"version": "v8.1.0",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/symfony/monolog-bridge.git",
|
|
||||||
"reference": "38563fac41ede8521e5e3dc139a4f2b097471c8c"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/38563fac41ede8521e5e3dc139a4f2b097471c8c",
|
|
||||||
"reference": "38563fac41ede8521e5e3dc139a4f2b097471c8c",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"monolog/monolog": "^3",
|
|
||||||
"php": ">=8.4.1",
|
|
||||||
"symfony/http-kernel": "^7.4|^8.0",
|
|
||||||
"symfony/service-contracts": "^2.5|^3"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"symfony/console": "^7.4|^8.0",
|
|
||||||
"symfony/http-client": "^7.4|^8.0",
|
|
||||||
"symfony/mailer": "^7.4|^8.0",
|
|
||||||
"symfony/messenger": "^7.4|^8.0",
|
|
||||||
"symfony/mime": "^7.4|^8.0",
|
|
||||||
"symfony/security-core": "^7.4|^8.0",
|
|
||||||
"symfony/var-dumper": "^7.4|^8.0"
|
|
||||||
},
|
|
||||||
"type": "symfony-bridge",
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"Symfony\\Bridge\\Monolog\\": ""
|
|
||||||
},
|
|
||||||
"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 integration for Monolog with various Symfony components",
|
|
||||||
"homepage": "https://symfony.com",
|
|
||||||
"support": {
|
|
||||||
"source": "https://github.com/symfony/monolog-bridge/tree/v8.1.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-05-29T05:06:50+00:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "symfony/monolog-bundle",
|
|
||||||
"version": "v4.0.2",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/symfony/monolog-bundle.git",
|
|
||||||
"reference": "c012c6aba13129eb02aa7dd61e66e720911d8598"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/c012c6aba13129eb02aa7dd61e66e720911d8598",
|
|
||||||
"reference": "c012c6aba13129eb02aa7dd61e66e720911d8598",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"composer-runtime-api": "^2.0",
|
|
||||||
"monolog/monolog": "^3.5",
|
|
||||||
"php": ">=8.2",
|
|
||||||
"symfony/config": "^7.3 || ^8.0",
|
|
||||||
"symfony/dependency-injection": "^7.3 || ^8.0",
|
|
||||||
"symfony/http-kernel": "^7.3 || ^8.0",
|
|
||||||
"symfony/monolog-bridge": "^7.3 || ^8.0",
|
|
||||||
"symfony/polyfill-php84": "^1.30"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"phpunit/phpunit": "^11.5.41 || ^12.3",
|
|
||||||
"symfony/console": "^7.3 || ^8.0",
|
|
||||||
"symfony/yaml": "^7.3 || ^8.0"
|
|
||||||
},
|
|
||||||
"type": "symfony-bundle",
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"Symfony\\Bundle\\MonologBundle\\": "src"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"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": "Symfony MonologBundle",
|
|
||||||
"homepage": "https://symfony.com",
|
|
||||||
"keywords": [
|
|
||||||
"log",
|
|
||||||
"logging"
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/symfony/monolog-bundle/issues",
|
|
||||||
"source": "https://github.com/symfony/monolog-bundle/tree/v4.0.2"
|
|
||||||
},
|
|
||||||
"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-02T18:27:21+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "symfony/password-hasher",
|
"name": "symfony/password-hasher",
|
||||||
"version": "v8.1.0",
|
"version": "v8.1.0",
|
||||||
|
|
|
||||||
|
|
@ -10,5 +10,4 @@ return [
|
||||||
ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
|
ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
|
||||||
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
|
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
|
||||||
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
|
DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
|
||||||
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,7 @@ framework:
|
||||||
secret: '%env(APP_SECRET)%'
|
secret: '%env(APP_SECRET)%'
|
||||||
|
|
||||||
# Note that the session will be started ONLY if you read or write from it.
|
# Note that the session will be started ONLY if you read or write from it.
|
||||||
session:
|
session: true
|
||||||
# Same-origin SPA cookie session: not readable by JS, sent on same-site
|
|
||||||
# navigations. cookie_secure: auto → Secure flag only over HTTPS (prod),
|
|
||||||
# so local HTTP dev still works.
|
|
||||||
cookie_httponly: true
|
|
||||||
cookie_samesite: lax
|
|
||||||
cookie_secure: auto
|
|
||||||
|
|
||||||
#esi: true
|
#esi: true
|
||||||
#fragments: true
|
#fragments: true
|
||||||
|
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
monolog:
|
|
||||||
channels:
|
|
||||||
- deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
|
|
||||||
|
|
||||||
when@dev:
|
|
||||||
monolog:
|
|
||||||
handlers:
|
|
||||||
main:
|
|
||||||
type: stream
|
|
||||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
|
||||||
level: debug
|
|
||||||
channels: ["!event"]
|
|
||||||
console:
|
|
||||||
type: console
|
|
||||||
process_psr_3_messages: false
|
|
||||||
channels: ["!event", "!doctrine", "!console"]
|
|
||||||
|
|
||||||
when@test:
|
|
||||||
monolog:
|
|
||||||
handlers:
|
|
||||||
main:
|
|
||||||
type: fingers_crossed
|
|
||||||
action_level: error
|
|
||||||
handler: nested
|
|
||||||
excluded_http_codes: [404, 405]
|
|
||||||
channels: ["!event"]
|
|
||||||
nested:
|
|
||||||
type: stream
|
|
||||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
|
||||||
level: debug
|
|
||||||
|
|
||||||
when@prod:
|
|
||||||
monolog:
|
|
||||||
handlers:
|
|
||||||
main:
|
|
||||||
type: fingers_crossed
|
|
||||||
action_level: error
|
|
||||||
handler: nested
|
|
||||||
excluded_http_codes: [404, 405]
|
|
||||||
channels: ["!deprecation"]
|
|
||||||
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
|
|
||||||
nested:
|
|
||||||
type: stream
|
|
||||||
path: php://stderr
|
|
||||||
level: debug
|
|
||||||
formatter: monolog.formatter.json
|
|
||||||
console:
|
|
||||||
type: console
|
|
||||||
process_psr_3_messages: false
|
|
||||||
channels: ["!event", "!doctrine"]
|
|
||||||
deprecation:
|
|
||||||
type: stream
|
|
||||||
channels: [deprecation]
|
|
||||||
path: php://stderr
|
|
||||||
formatter: monolog.formatter.json
|
|
||||||
|
|
@ -1,39 +1,43 @@
|
||||||
security:
|
security:
|
||||||
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
|
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
|
||||||
password_hashers:
|
password_hashers:
|
||||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
||||||
|
|
||||||
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
||||||
providers:
|
providers:
|
||||||
# used to reload user from session & other features (e.g. switch_user)
|
# used to reload user from session & other features (e.g. switch_user)
|
||||||
app_user_provider:
|
app_user_provider:
|
||||||
entity:
|
entity:
|
||||||
class: App\Entity\User
|
class: App\Entity\User
|
||||||
property: email
|
property: email
|
||||||
|
|
||||||
firewalls:
|
firewalls:
|
||||||
dev:
|
dev:
|
||||||
# Ensure dev tools and static assets are always allowed
|
# Ensure dev tools and static assets are always allowed
|
||||||
pattern: ^/(_profiler|_wdt|assets|build)/
|
pattern: ^/(_profiler|_wdt|assets|build)/
|
||||||
security: false
|
security: false
|
||||||
main:
|
main:
|
||||||
lazy: true
|
lazy: true
|
||||||
provider: app_user_provider
|
provider: app_user_provider
|
||||||
json_login:
|
|
||||||
check_path: api_login
|
|
||||||
logout:
|
|
||||||
path: /api/logout
|
|
||||||
|
|
||||||
access_control:
|
# Activate different ways to authenticate:
|
||||||
- {path: ^/api/me, roles: ROLE_USER}
|
# https://symfony.com/doc/current/security.html#the-firewall
|
||||||
|
|
||||||
|
# https://symfony.com/doc/current/security/impersonating_user.html
|
||||||
|
# switch_user: true
|
||||||
|
|
||||||
|
# Note: Only the *first* matching rule is applied
|
||||||
|
access_control:
|
||||||
|
# - { path: ^/admin, roles: ROLE_ADMIN }
|
||||||
|
# - { path: ^/profile, roles: ROLE_USER }
|
||||||
|
|
||||||
when@test:
|
when@test:
|
||||||
security:
|
security:
|
||||||
password_hashers:
|
password_hashers:
|
||||||
# Password hashers are resource-intensive by design to ensure security.
|
# Password hashers are resource-intensive by design to ensure security.
|
||||||
# In tests, it's safe to reduce their cost to improve performance.
|
# In tests, it's safe to reduce their cost to improve performance.
|
||||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
|
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
|
||||||
algorithm: auto
|
algorithm: auto
|
||||||
cost: 4 # Lowest possible value for bcrypt
|
cost: 4 # Lowest possible value for bcrypt
|
||||||
time_cost: 3 # Lowest possible value for argon
|
time_cost: 3 # Lowest possible value for argon
|
||||||
memory_cost: 10 # Lowest possible value for argon
|
memory_cost: 10 # Lowest possible value for argon
|
||||||
|
|
|
||||||
|
|
@ -1020,9 +1020,9 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||||
* lifetime?: int|Param, // Default: 31536000
|
* lifetime?: int|Param, // Default: 31536000
|
||||||
* path?: scalar|Param|null, // Default: "/"
|
* path?: scalar|Param|null, // Default: "/"
|
||||||
* domain?: scalar|Param|null, // Default: null
|
* domain?: scalar|Param|null, // Default: null
|
||||||
* secure?: true|false|"auto"|Param, // Default: null
|
* secure?: true|false|"auto"|Param, // Default: false
|
||||||
* httponly?: bool|Param, // Default: true
|
* httponly?: bool|Param, // Default: true
|
||||||
* samesite?: null|"lax"|"strict"|"none"|Param, // Default: "lax"
|
* samesite?: null|"lax"|"strict"|"none"|Param, // Default: null
|
||||||
* always_remember_me?: bool|Param, // Default: false
|
* always_remember_me?: bool|Param, // Default: false
|
||||||
* remember_me_parameter?: scalar|Param|null, // Default: "_remember_me"
|
* remember_me_parameter?: scalar|Param|null, // Default: "_remember_me"
|
||||||
* },
|
* },
|
||||||
|
|
@ -1586,149 +1586,6 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||||
* enable_static_query_cache?: bool|Param, // Default: true
|
* enable_static_query_cache?: bool|Param, // Default: true
|
||||||
* connection_keys?: list<mixed>,
|
* connection_keys?: list<mixed>,
|
||||||
* }
|
* }
|
||||||
* @psalm-type MonologConfig = array{
|
|
||||||
* use_microseconds?: scalar|Param|null, // Default: true
|
|
||||||
* channels?: list<scalar|Param|null>,
|
|
||||||
* handlers?: array<string, array{ // Default: []
|
|
||||||
* type?: scalar|Param|null,
|
|
||||||
* id?: scalar|Param|null,
|
|
||||||
* enabled?: bool|Param, // Default: true
|
|
||||||
* priority?: scalar|Param|null, // Default: 0
|
|
||||||
* level?: scalar|Param|null, // Default: "DEBUG"
|
|
||||||
* bubble?: bool|Param, // Default: true
|
|
||||||
* interactive_only?: bool|Param, // Default: false
|
|
||||||
* app_name?: scalar|Param|null, // Default: null
|
|
||||||
* include_stacktraces?: bool|Param, // Default: false
|
|
||||||
* process_psr_3_messages?: array{
|
|
||||||
* enabled?: bool|Param|null, // Default: null
|
|
||||||
* date_format?: scalar|Param|null,
|
|
||||||
* remove_used_context_fields?: bool|Param,
|
|
||||||
* },
|
|
||||||
* path?: scalar|Param|null, // Default: "%kernel.logs_dir%/%kernel.environment%.log"
|
|
||||||
* file_permission?: scalar|Param|null, // Default: null
|
|
||||||
* use_locking?: bool|Param, // Default: false
|
|
||||||
* filename_format?: scalar|Param|null, // Default: "{filename}-{date}"
|
|
||||||
* date_format?: scalar|Param|null, // Default: "Y-m-d"
|
|
||||||
* ident?: scalar|Param|null, // Default: false
|
|
||||||
* logopts?: scalar|Param|null, // Default: 1
|
|
||||||
* facility?: scalar|Param|null, // Default: "user"
|
|
||||||
* max_files?: scalar|Param|null, // Default: 0
|
|
||||||
* action_level?: scalar|Param|null, // Default: "WARNING"
|
|
||||||
* activation_strategy?: scalar|Param|null, // Default: null
|
|
||||||
* stop_buffering?: bool|Param, // Default: true
|
|
||||||
* passthru_level?: scalar|Param|null, // Default: null
|
|
||||||
* excluded_http_codes?: list<array{ // Default: []
|
|
||||||
* code?: scalar|Param|null,
|
|
||||||
* urls?: list<scalar|Param|null>,
|
|
||||||
* }>,
|
|
||||||
* accepted_levels?: list<scalar|Param|null>,
|
|
||||||
* min_level?: scalar|Param|null, // Default: "DEBUG"
|
|
||||||
* max_level?: scalar|Param|null, // Default: "EMERGENCY"
|
|
||||||
* buffer_size?: scalar|Param|null, // Default: 0
|
|
||||||
* flush_on_overflow?: bool|Param, // Default: false
|
|
||||||
* handler?: scalar|Param|null,
|
|
||||||
* url?: scalar|Param|null,
|
|
||||||
* exchange?: scalar|Param|null,
|
|
||||||
* exchange_name?: scalar|Param|null, // Default: "log"
|
|
||||||
* channel?: scalar|Param|null, // Default: null
|
|
||||||
* bot_name?: scalar|Param|null, // Default: "Monolog"
|
|
||||||
* use_attachment?: scalar|Param|null, // Default: true
|
|
||||||
* use_short_attachment?: scalar|Param|null, // Default: false
|
|
||||||
* include_extra?: scalar|Param|null, // Default: false
|
|
||||||
* icon_emoji?: scalar|Param|null, // Default: null
|
|
||||||
* webhook_url?: scalar|Param|null,
|
|
||||||
* exclude_fields?: list<scalar|Param|null>,
|
|
||||||
* token?: scalar|Param|null,
|
|
||||||
* region?: scalar|Param|null,
|
|
||||||
* source?: scalar|Param|null,
|
|
||||||
* use_ssl?: bool|Param, // Default: true
|
|
||||||
* user?: mixed,
|
|
||||||
* title?: scalar|Param|null, // Default: null
|
|
||||||
* host?: scalar|Param|null, // Default: null
|
|
||||||
* port?: scalar|Param|null, // Default: 514
|
|
||||||
* config?: list<scalar|Param|null>,
|
|
||||||
* members?: list<scalar|Param|null>,
|
|
||||||
* connection_string?: scalar|Param|null,
|
|
||||||
* timeout?: scalar|Param|null,
|
|
||||||
* time?: scalar|Param|null, // Default: 60
|
|
||||||
* deduplication_level?: scalar|Param|null, // Default: 400
|
|
||||||
* store?: scalar|Param|null, // Default: null
|
|
||||||
* connection_timeout?: scalar|Param|null,
|
|
||||||
* persistent?: bool|Param,
|
|
||||||
* message_type?: scalar|Param|null, // Default: 0
|
|
||||||
* parse_mode?: scalar|Param|null, // Default: null
|
|
||||||
* disable_webpage_preview?: bool|Param|null, // Default: null
|
|
||||||
* disable_notification?: bool|Param|null, // Default: null
|
|
||||||
* split_long_messages?: bool|Param, // Default: false
|
|
||||||
* delay_between_messages?: bool|Param, // Default: false
|
|
||||||
* topic?: int|Param, // Default: null
|
|
||||||
* factor?: int|Param, // Default: 1
|
|
||||||
* tags?: string|list<scalar|Param|null>,
|
|
||||||
* console_formatter_options?: mixed, // Default: []
|
|
||||||
* formatter?: scalar|Param|null,
|
|
||||||
* nested?: bool|Param, // Default: false
|
|
||||||
* publisher?: string|array{
|
|
||||||
* id?: scalar|Param|null,
|
|
||||||
* hostname?: scalar|Param|null,
|
|
||||||
* port?: scalar|Param|null, // Default: 12201
|
|
||||||
* chunk_size?: scalar|Param|null, // Default: 1420
|
|
||||||
* encoder?: "json"|"compressed_json"|Param,
|
|
||||||
* },
|
|
||||||
* mongodb?: string|array{
|
|
||||||
* id?: scalar|Param|null, // ID of a MongoDB\Client service
|
|
||||||
* uri?: scalar|Param|null,
|
|
||||||
* username?: scalar|Param|null,
|
|
||||||
* password?: scalar|Param|null,
|
|
||||||
* database?: scalar|Param|null, // Default: "monolog"
|
|
||||||
* collection?: scalar|Param|null, // Default: "logs"
|
|
||||||
* },
|
|
||||||
* elasticsearch?: string|array{
|
|
||||||
* id?: scalar|Param|null,
|
|
||||||
* hosts?: list<scalar|Param|null>,
|
|
||||||
* host?: scalar|Param|null,
|
|
||||||
* port?: scalar|Param|null, // Default: 9200
|
|
||||||
* transport?: scalar|Param|null, // Default: "Http"
|
|
||||||
* user?: scalar|Param|null, // Default: null
|
|
||||||
* password?: scalar|Param|null, // Default: null
|
|
||||||
* },
|
|
||||||
* index?: scalar|Param|null, // Default: "monolog"
|
|
||||||
* document_type?: scalar|Param|null, // Default: "logs"
|
|
||||||
* ignore_error?: scalar|Param|null, // Default: false
|
|
||||||
* redis?: string|array{
|
|
||||||
* id?: scalar|Param|null,
|
|
||||||
* host?: scalar|Param|null,
|
|
||||||
* password?: scalar|Param|null, // Default: null
|
|
||||||
* port?: scalar|Param|null, // Default: 6379
|
|
||||||
* database?: scalar|Param|null, // Default: 0
|
|
||||||
* key_name?: scalar|Param|null, // Default: "monolog_redis"
|
|
||||||
* },
|
|
||||||
* predis?: string|array{
|
|
||||||
* id?: scalar|Param|null,
|
|
||||||
* host?: scalar|Param|null,
|
|
||||||
* },
|
|
||||||
* from_email?: scalar|Param|null,
|
|
||||||
* to_email?: string|list<scalar|Param|null>,
|
|
||||||
* subject?: scalar|Param|null,
|
|
||||||
* content_type?: scalar|Param|null, // Default: null
|
|
||||||
* headers?: list<scalar|Param|null>,
|
|
||||||
* mailer?: scalar|Param|null, // Default: null
|
|
||||||
* email_prototype?: string|array{
|
|
||||||
* id?: scalar|Param|null,
|
|
||||||
* method?: scalar|Param|null, // Default: null
|
|
||||||
* },
|
|
||||||
* verbosity_levels?: array{
|
|
||||||
* VERBOSITY_QUIET?: scalar|Param|null, // Default: "ERROR"
|
|
||||||
* VERBOSITY_NORMAL?: scalar|Param|null, // Default: "WARNING"
|
|
||||||
* VERBOSITY_VERBOSE?: scalar|Param|null, // Default: "NOTICE"
|
|
||||||
* VERBOSITY_VERY_VERBOSE?: scalar|Param|null, // Default: "INFO"
|
|
||||||
* VERBOSITY_DEBUG?: scalar|Param|null, // Default: "DEBUG"
|
|
||||||
* },
|
|
||||||
* channels?: string|array{
|
|
||||||
* type?: scalar|Param|null,
|
|
||||||
* elements?: list<scalar|Param|null>,
|
|
||||||
* },
|
|
||||||
* }>,
|
|
||||||
* }
|
|
||||||
* @psalm-type ConfigType = array{
|
* @psalm-type ConfigType = array{
|
||||||
* imports?: ImportsConfig,
|
* imports?: ImportsConfig,
|
||||||
* parameters?: ParametersConfig,
|
* parameters?: ParametersConfig,
|
||||||
|
|
@ -1740,7 +1597,6 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||||
* doctrine_migrations?: DoctrineMigrationsConfig,
|
* doctrine_migrations?: DoctrineMigrationsConfig,
|
||||||
* nelmio_cors?: NelmioCorsConfig,
|
* nelmio_cors?: NelmioCorsConfig,
|
||||||
* api_platform?: ApiPlatformConfig,
|
* api_platform?: ApiPlatformConfig,
|
||||||
* monolog?: MonologConfig,
|
|
||||||
* "when@dev"?: array{
|
* "when@dev"?: array{
|
||||||
* imports?: ImportsConfig,
|
* imports?: ImportsConfig,
|
||||||
* parameters?: ParametersConfig,
|
* parameters?: ParametersConfig,
|
||||||
|
|
@ -1753,7 +1609,6 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||||
* nelmio_cors?: NelmioCorsConfig,
|
* nelmio_cors?: NelmioCorsConfig,
|
||||||
* api_platform?: ApiPlatformConfig,
|
* api_platform?: ApiPlatformConfig,
|
||||||
* maker?: MakerConfig,
|
* maker?: MakerConfig,
|
||||||
* monolog?: MonologConfig,
|
|
||||||
* },
|
* },
|
||||||
* "when@prod"?: array{
|
* "when@prod"?: array{
|
||||||
* imports?: ImportsConfig,
|
* imports?: ImportsConfig,
|
||||||
|
|
@ -1766,7 +1621,6 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||||
* doctrine_migrations?: DoctrineMigrationsConfig,
|
* doctrine_migrations?: DoctrineMigrationsConfig,
|
||||||
* nelmio_cors?: NelmioCorsConfig,
|
* nelmio_cors?: NelmioCorsConfig,
|
||||||
* api_platform?: ApiPlatformConfig,
|
* api_platform?: ApiPlatformConfig,
|
||||||
* monolog?: MonologConfig,
|
|
||||||
* },
|
* },
|
||||||
* "when@test"?: array{
|
* "when@test"?: array{
|
||||||
* imports?: ImportsConfig,
|
* imports?: ImportsConfig,
|
||||||
|
|
@ -1780,7 +1634,6 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
|
||||||
* nelmio_cors?: NelmioCorsConfig,
|
* nelmio_cors?: NelmioCorsConfig,
|
||||||
* api_platform?: ApiPlatformConfig,
|
* api_platform?: ApiPlatformConfig,
|
||||||
* dama_doctrine_test?: DamaDoctrineTestConfig,
|
* dama_doctrine_test?: DamaDoctrineTestConfig,
|
||||||
* monolog?: MonologConfig,
|
|
||||||
* },
|
* },
|
||||||
* ...<string, ExtensionType|array{ // extra keys must follow the when@%env% pattern or match an extension alias
|
* ...<string, ExtensionType|array{ // extra keys must follow the when@%env% pattern or match an extension alias
|
||||||
* imports?: ImportsConfig,
|
* imports?: ImportsConfig,
|
||||||
|
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Controller;
|
|
||||||
|
|
||||||
use App\Entity\User;
|
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
|
||||||
use Symfony\Component\Routing\Attribute\Route;
|
|
||||||
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
|
||||||
|
|
||||||
final class ApiLoginController extends AbstractController
|
|
||||||
{
|
|
||||||
#[Route('/api/login', name: 'api_login', methods: ['POST'])]
|
|
||||||
public function login(#[CurrentUser] ?User $user): JsonResponse
|
|
||||||
{
|
|
||||||
if (null === $user) {
|
|
||||||
return $this->json([], JsonResponse::HTTP_UNAUTHORIZED);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->json([
|
|
||||||
'id' => $user->getId(),
|
|
||||||
'email' => $user->getEmail(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[Route('/api/me', name: 'api_me', methods: ['GET'])]
|
|
||||||
public function me(#[CurrentUser] User $user): JsonResponse
|
|
||||||
{
|
|
||||||
return $this->json([
|
|
||||||
'id' => $user->getId(),
|
|
||||||
'email' => $user->getEmail(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Security;
|
|
||||||
|
|
||||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
|
||||||
use Symfony\Component\Security\Http\Event\LogoutEvent;
|
|
||||||
|
|
||||||
final class LogoutSubscriber implements EventSubscriberInterface
|
|
||||||
{
|
|
||||||
public static function getSubscribedEvents(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
LogoutEvent::class => 'onLogout',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function onLogout(LogoutEvent $event): void
|
|
||||||
{
|
|
||||||
$event->setResponse(new Response(null, Response::HTTP_NO_CONTENT));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
12
symfony.lock
12
symfony.lock
|
|
@ -157,18 +157,6 @@
|
||||||
"ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
|
"ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"symfony/monolog-bundle": {
|
|
||||||
"version": "4.0",
|
|
||||||
"recipe": {
|
|
||||||
"repo": "github.com/symfony/recipes",
|
|
||||||
"branch": "main",
|
|
||||||
"version": "3.7",
|
|
||||||
"ref": "1b9efb10c54cb51c713a9391c9300ff8bceda459"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"config/packages/monolog.yaml"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"symfony/property-info": {
|
"symfony/property-info": {
|
||||||
"version": "8.1",
|
"version": "8.1",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
|
|
|
||||||
|
|
@ -1,271 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Tests\Functional;
|
|
||||||
|
|
||||||
use App\Entity\User;
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
|
||||||
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
|
|
||||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
|
||||||
use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher;
|
|
||||||
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory;
|
|
||||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasher;
|
|
||||||
|
|
||||||
final class AuthenticationTest extends WebTestCase
|
|
||||||
{
|
|
||||||
private const EMAIL = 'auth-test@example.com';
|
|
||||||
private const PASSWORD = 'correct-horse-battery-staple';
|
|
||||||
|
|
||||||
private KernelBrowser $client;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
|
||||||
{
|
|
||||||
$this->client = static::createClient();
|
|
||||||
|
|
||||||
// Persist a user with a properly hashed password before each test.
|
|
||||||
// Reuse the direct-factory pattern from UserPersistenceTest — avoids
|
|
||||||
// the container chicken-and-egg during Kernel compile, and cost:4
|
|
||||||
// keeps the test suite fast.
|
|
||||||
$factory = new PasswordHasherFactory([
|
|
||||||
User::class => new NativePasswordHasher(cost: 4),
|
|
||||||
]);
|
|
||||||
$hasher = new UserPasswordHasher($factory);
|
|
||||||
|
|
||||||
$user = new User();
|
|
||||||
$user->setEmail(self::EMAIL);
|
|
||||||
$user->setPassword($hasher->hashPassword($user, self::PASSWORD));
|
|
||||||
|
|
||||||
/** @var EntityManagerInterface $em */
|
|
||||||
$em = static::getContainer()->get(EntityManagerInterface::class);
|
|
||||||
$em->persist($user);
|
|
||||||
$em->flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Test 2: json_login happy path — valid credentials yield a session cookie
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
public function testItLogsInWithValidCredentialsAndSetsASessionCookie(): void
|
|
||||||
{
|
|
||||||
$this->client->jsonRequest('POST', '/api/login', [
|
|
||||||
'username' => self::EMAIL,
|
|
||||||
'password' => self::PASSWORD,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$loginResponse = $this->client->getResponse();
|
|
||||||
|
|
||||||
self::assertLessThan(
|
|
||||||
300,
|
|
||||||
$loginResponse->getStatusCode(),
|
|
||||||
'A successful login must return a 2xx response.',
|
|
||||||
);
|
|
||||||
|
|
||||||
$setCookieHeader = $loginResponse->headers->get('Set-Cookie');
|
|
||||||
|
|
||||||
self::assertNotNull(
|
|
||||||
$setCookieHeader,
|
|
||||||
'A successful login must set a Set-Cookie header.',
|
|
||||||
);
|
|
||||||
|
|
||||||
self::assertMatchesRegularExpression(
|
|
||||||
'/HttpOnly/i',
|
|
||||||
$setCookieHeader,
|
|
||||||
'The session cookie must be HttpOnly.',
|
|
||||||
);
|
|
||||||
|
|
||||||
self::assertMatchesRegularExpression(
|
|
||||||
'/SameSite=Lax/i',
|
|
||||||
$setCookieHeader,
|
|
||||||
'The session cookie must carry SameSite=Lax.',
|
|
||||||
);
|
|
||||||
|
|
||||||
// --- Login response body contract ---
|
|
||||||
// Capture before the follow-up /api/me request replaces the client's response.
|
|
||||||
$loginBody = json_decode((string) $loginResponse->getContent(), true);
|
|
||||||
|
|
||||||
self::assertIsArray($loginBody, 'The login response must be a JSON object.');
|
|
||||||
|
|
||||||
self::assertArrayHasKey('email', $loginBody, 'The login response must include the email field.');
|
|
||||||
self::assertSame(self::EMAIL, $loginBody['email'], 'The login response must return the authenticated user\'s email.');
|
|
||||||
|
|
||||||
self::assertArrayHasKey('id', $loginBody, 'The login response must include the id (uuid) field.');
|
|
||||||
self::assertMatchesRegularExpression(
|
|
||||||
'/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i',
|
|
||||||
(string) $loginBody['id'],
|
|
||||||
'The login response id must be a valid UUIDv7.',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Password hash must NEVER appear in the login response.
|
|
||||||
$loginResponseText = (string) $loginResponse->getContent();
|
|
||||||
self::assertStringNotContainsString(
|
|
||||||
'$2',
|
|
||||||
$loginResponseText,
|
|
||||||
'The login response must never expose a bcrypt hash (starts with $2).',
|
|
||||||
);
|
|
||||||
self::assertStringNotContainsString(
|
|
||||||
'$argon',
|
|
||||||
$loginResponseText,
|
|
||||||
'The login response must never expose an Argon2 hash (starts with $argon).',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Exact shape: only 'id' and 'email' — no extra fields (kills 'message'/'path' junk keys).
|
|
||||||
$keys = array_keys($loginBody);
|
|
||||||
sort($keys);
|
|
||||||
self::assertSame(
|
|
||||||
['email', 'id'],
|
|
||||||
$keys,
|
|
||||||
'The login response body must contain EXACTLY the keys [email, id] — no more, no less.',
|
|
||||||
);
|
|
||||||
|
|
||||||
// The cookie issued must actually authenticate — not just exist.
|
|
||||||
// Same client carries the cookie jar; no re-login needed.
|
|
||||||
$this->client->jsonRequest('GET', '/api/me');
|
|
||||||
|
|
||||||
self::assertSame(
|
|
||||||
200,
|
|
||||||
$this->client->getResponse()->getStatusCode(),
|
|
||||||
'The session cookie set by login must authenticate a follow-up request to /api/me.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Test 3: json_login sad path — wrong password yields 401, no session
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
public function testItRejectsLoginWithInvalidCredentials(): void
|
|
||||||
{
|
|
||||||
$this->client->jsonRequest('POST', '/api/login', [
|
|
||||||
'username' => self::EMAIL,
|
|
||||||
'password' => 'wrong-password',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response = $this->client->getResponse();
|
|
||||||
|
|
||||||
self::assertSame(
|
|
||||||
401,
|
|
||||||
$response->getStatusCode(),
|
|
||||||
'An invalid password must return 401 Unauthorized.',
|
|
||||||
);
|
|
||||||
|
|
||||||
// A failed login must NOT leave the client holding an authenticating session.
|
|
||||||
// We assert this at the causal level: the browser cookie jar must be empty
|
|
||||||
// after a failed login attempt on a fresh client. If a buggy implementation
|
|
||||||
// issued a real session cookie on bad credentials, the jar would be non-empty
|
|
||||||
// and this assertion would catch it — something the /api/me 401 check alone
|
|
||||||
// cannot do (a 401 there could be explained by other reasons even with a
|
|
||||||
// session cookie present, e.g. a cookie that exists but is invalid).
|
|
||||||
$cookies = $this->client->getCookieJar()->all();
|
|
||||||
|
|
||||||
self::assertEmpty(
|
|
||||||
$cookies,
|
|
||||||
'A failed login must not set any cookies — the browser cookie jar must remain empty.',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Belt-and-suspenders: even if the jar were somehow populated, /api/me
|
|
||||||
// must still refuse. This check is now meaningful because it follows the
|
|
||||||
// causal assertion above rather than standing alone.
|
|
||||||
$this->client->jsonRequest('GET', '/api/me');
|
|
||||||
|
|
||||||
self::assertSame(
|
|
||||||
401,
|
|
||||||
$this->client->getResponse()->getStatusCode(),
|
|
||||||
'After a failed login the client must not hold an authenticated session.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Test 5: /api/me is behind the firewall — unauthenticated access → 401
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
public function testItReturns401OnMeWhenNotAuthenticated(): void
|
|
||||||
{
|
|
||||||
// Fresh client — no login, no cookie.
|
|
||||||
$this->client->jsonRequest('GET', '/api/me');
|
|
||||||
|
|
||||||
self::assertSame(
|
|
||||||
401,
|
|
||||||
$this->client->getResponse()->getStatusCode(),
|
|
||||||
'/api/me must return 401 when no session is present.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Test 6: authenticated /api/me returns identity (no password hash);
|
|
||||||
// logout then invalidates the session
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
public function testItReturnsCurrentUserOnMeAndLogoutClearsTheSession(): void
|
|
||||||
{
|
|
||||||
// --- Step 1: log in ---
|
|
||||||
$this->client->jsonRequest('POST', '/api/login', [
|
|
||||||
'username' => self::EMAIL,
|
|
||||||
'password' => self::PASSWORD,
|
|
||||||
]);
|
|
||||||
|
|
||||||
self::assertLessThan(
|
|
||||||
300,
|
|
||||||
$this->client->getResponse()->getStatusCode(),
|
|
||||||
'Login must succeed before testing /api/me.',
|
|
||||||
);
|
|
||||||
|
|
||||||
// --- Step 2: GET /api/me while authenticated ---
|
|
||||||
$this->client->jsonRequest('GET', '/api/me');
|
|
||||||
|
|
||||||
$meResponse = $this->client->getResponse();
|
|
||||||
|
|
||||||
self::assertSame(200, $meResponse->getStatusCode(), '/api/me must return 200 for an authenticated user.');
|
|
||||||
|
|
||||||
$body = json_decode((string) $meResponse->getContent(), true);
|
|
||||||
|
|
||||||
self::assertIsArray($body, '/api/me must return a JSON object.');
|
|
||||||
|
|
||||||
self::assertArrayHasKey('email', $body, '/api/me response must include the email field.');
|
|
||||||
self::assertSame(self::EMAIL, $body['email'], '/api/me must return the authenticated user\'s email.');
|
|
||||||
|
|
||||||
self::assertArrayHasKey('id', $body, '/api/me response must include the id (uuid) field.');
|
|
||||||
self::assertMatchesRegularExpression(
|
|
||||||
'/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i',
|
|
||||||
(string) $body['id'],
|
|
||||||
'/api/me id must be a valid UUIDv7.',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Password hash must NEVER appear in the response — check all values recursively.
|
|
||||||
$responseText = (string) $meResponse->getContent();
|
|
||||||
self::assertStringNotContainsString(
|
|
||||||
'$2',
|
|
||||||
$responseText,
|
|
||||||
'/api/me must never expose a bcrypt hash (starts with $2).',
|
|
||||||
);
|
|
||||||
self::assertStringNotContainsString(
|
|
||||||
'$argon',
|
|
||||||
$responseText,
|
|
||||||
'/api/me must never expose an Argon2 hash (starts with $argon).',
|
|
||||||
);
|
|
||||||
|
|
||||||
// --- Step 3: logout ---
|
|
||||||
$this->client->jsonRequest('POST', '/api/logout');
|
|
||||||
|
|
||||||
// Capture the logout response immediately — before the next request overwrites it.
|
|
||||||
$logoutResponse = $this->client->getResponse();
|
|
||||||
|
|
||||||
self::assertSame(
|
|
||||||
204,
|
|
||||||
$logoutResponse->getStatusCode(),
|
|
||||||
'POST /api/logout must return 204 No Content — not a redirect.',
|
|
||||||
);
|
|
||||||
|
|
||||||
self::assertSame(
|
|
||||||
'',
|
|
||||||
$logoutResponse->getContent(),
|
|
||||||
'A 204 response must have an empty body.',
|
|
||||||
);
|
|
||||||
|
|
||||||
// --- Step 4: /api/me must be 401 again ---
|
|
||||||
$this->client->jsonRequest('GET', '/api/me');
|
|
||||||
|
|
||||||
self::assertSame(
|
|
||||||
401,
|
|
||||||
$this->client->getResponse()->getStatusCode(),
|
|
||||||
'/api/me must return 401 after logout — the session must be invalidated.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue